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

Upload 1.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 1.py +1487 -0
1.py ADDED
@@ -0,0 +1,1487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bot_concours_unified.py - Bot de concours unifié et optimisé
2
+ # Version complète avec toutes les optimisations
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
+ from contextlib import contextmanager
30
+ from dataclasses import dataclass
31
+ from typing import List, Dict, Optional, Tuple
32
+ from urllib.parse import urljoin, urlparse
33
+
34
+ # =====================================================
35
+ # CONFIGURATION ET DATACLASSES
36
+ # =====================================================
37
+
38
+ @dataclass
39
+ class PersonalInfo:
40
+ prenom: str = "Valentin"
41
+ nom: str = "Cora"
42
+ email: str = "valouassol@outlook.com"
43
+ email_derivee: str = "valouassol+concours@outlook.com"
44
+ telephone: str = "+41791234567"
45
+ adresse: str = "Av Chantemerle 9"
46
+ code_postal: str = "1009"
47
+ ville: str = "Pully"
48
+ pays: str = "Suisse"
49
+
50
+ @dataclass
51
+ class APIConfig:
52
+ hf_token: Optional[str] = None
53
+ x_token: Optional[str] = None
54
+ google_api_key: Optional[str] = None
55
+ google_cx: Optional[str] = None
56
+ gemini_key: Optional[str] = None
57
+ email_app_password: Optional[str] = None
58
+ telegram_bot_token: Optional[str] = None
59
+ telegram_chat_id: Optional[str] = None
60
+
61
+ @classmethod
62
+ def from_env(cls):
63
+ return cls(
64
+ hf_token=os.getenv("HF_TOKEN"),
65
+ x_token=os.getenv("X_TOKEN"),
66
+ google_api_key=os.getenv("GOOGLE_API_KEY"),
67
+ google_cx=os.getenv("GOOGLE_CX"),
68
+ gemini_key=os.getenv("GEMINI_KEY"),
69
+ email_app_password=os.getenv("EMAIL_APP_PASSWORD"),
70
+ telegram_bot_token=os.getenv("TELEGRAM_BOT_TOKEN"),
71
+ telegram_chat_id=os.getenv("TELEGRAM_CHAT_ID")
72
+ )
73
+
74
+ @dataclass
75
+ class Contest:
76
+ title: str
77
+ url: str
78
+ description: str
79
+ source: str
80
+ deadline: Optional[str] = None
81
+ prize: Optional[str] = None
82
+ difficulty_score: int = 0
83
+
84
+ @dataclass
85
+ class FormField:
86
+ selector: str
87
+ field_type: str
88
+ label: str
89
+ required: bool
90
+ current_value: str = ""
91
+ ai_context: str = ""
92
+
93
+ @dataclass
94
+ class FormAnalysis:
95
+ fields: List[FormField]
96
+ complexity_score: int
97
+ estimated_success_rate: float
98
+ requires_captcha: bool
99
+ requires_social_media: bool
100
+ form_url: str
101
+
102
+ # =====================================================
103
+ # CONFIGURATION GLOBALE
104
+ # =====================================================
105
+
106
+ # Configuration
107
+ PERSONAL_INFO = PersonalInfo()
108
+ API_CONFIG = APIConfig.from_env()
109
+
110
+ # Sites suisses à scraper
111
+ SITES_CH = [
112
+ 'https://www.concours.ch/concours/tous',
113
+ 'https://www.jeu-concours.biz/concours-pays_suisse.html',
114
+ 'https://www.loisirs.ch/concours/',
115
+ 'https://www.radin.ch/',
116
+ 'https://win4win.ch/fr/',
117
+ 'https://www.concours-suisse.ch/',
118
+ 'https://corporate.migros.ch/fr/concours',
119
+ 'https://www.20min.ch/fr/concours-et-jeux',
120
+ 'https://dein-gewinnspiel.ch/en',
121
+ 'https://www.myswitzerland.com/fr/planification/vie-pratique/concours/'
122
+ ]
123
+
124
+ # Proxies et User Agents
125
+ PROXY_LIST = ["http://20.206.106.192:80", "http://51.15.242.202:8888"]
126
+ USER_AGENTS = [
127
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
128
+ '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',
129
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
130
+ ]
131
+
132
+ # Logging setup
133
+ logging.basicConfig(
134
+ level=logging.INFO,
135
+ format='%(asctime)s - %(levelname)s - %(message)s',
136
+ handlers=[
137
+ logging.FileHandler('concours_bot.log'),
138
+ logging.StreamHandler()
139
+ ]
140
+ )
141
+
142
+ # =====================================================
143
+ # GESTIONNAIRE DE BASE DE DONNÉES
144
+ # =====================================================
145
+
146
+ class DatabaseManager:
147
+ def __init__(self, db_path: str = 'concours_optimized.sqlite'):
148
+ self.db_path = db_path
149
+ self.local = threading.local()
150
+ self._init_db()
151
+
152
+ def _get_connection(self):
153
+ if not hasattr(self.local, 'conn'):
154
+ self.local.conn = sqlite3.connect(self.db_path)
155
+ self.local.conn.row_factory = sqlite3.Row
156
+ return self.local.conn
157
+
158
+ @contextmanager
159
+ def transaction(self):
160
+ conn = self._get_connection()
161
+ try:
162
+ yield conn
163
+ conn.commit()
164
+ except Exception:
165
+ conn.rollback()
166
+ raise
167
+
168
+ def _init_db(self):
169
+ with self.transaction() as conn:
170
+ conn.execute('''
171
+ CREATE TABLE IF NOT EXISTS participations (
172
+ url TEXT PRIMARY KEY,
173
+ title TEXT,
174
+ source TEXT,
175
+ status TEXT,
176
+ difficulty_score INTEGER,
177
+ success_rate REAL,
178
+ date TEXT,
179
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
180
+ )
181
+ ''')
182
+ conn.execute('''
183
+ CREATE TABLE IF NOT EXISTS victories (
184
+ email_id TEXT PRIMARY KEY,
185
+ date TEXT,
186
+ lot TEXT,
187
+ source TEXT,
188
+ confirmed BOOLEAN DEFAULT FALSE,
189
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
190
+ )
191
+ ''')
192
+ conn.execute('''
193
+ CREATE TABLE IF NOT EXISTS contest_cache (
194
+ url TEXT PRIMARY KEY,
195
+ content TEXT,
196
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
197
+ )
198
+ ''')
199
+ # Index pour performances
200
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_participations_date ON participations(date)')
201
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_cache_timestamp ON contest_cache(timestamp)')
202
+
203
+ def add_participation(self, contest: Contest, status: str = 'pending', success_rate: float = 0.0):
204
+ with self.transaction() as conn:
205
+ conn.execute('''
206
+ INSERT OR REPLACE INTO participations
207
+ (url, title, source, status, difficulty_score, success_rate, date)
208
+ VALUES (?, ?, ?, ?, ?, ?, date('now'))
209
+ ''', (contest.url, contest.title, contest.source, status, contest.difficulty_score, success_rate))
210
+
211
+ def participation_exists(self, url: str) -> bool:
212
+ conn = self._get_connection()
213
+ result = conn.execute("SELECT 1 FROM participations WHERE url = ?", (url,)).fetchone()
214
+ return result is not None
215
+
216
+ def add_victory(self, email_id: str, lot: str, source: str):
217
+ with self.transaction() as conn:
218
+ conn.execute('''
219
+ INSERT OR IGNORE INTO victories (email_id, date, lot, source)
220
+ VALUES (?, date('now'), ?, ?)
221
+ ''', (email_id, lot, source))
222
+
223
+ def get_stats(self) -> Dict:
224
+ conn = self._get_connection()
225
+ stats = {}
226
+
227
+ # Statistiques de participations
228
+ stats['total_participations'] = conn.execute("SELECT COUNT(*) FROM participations").fetchone()[0]
229
+ stats['successful_participations'] = conn.execute("SELECT COUNT(*) FROM participations WHERE status='success'").fetchone()[0]
230
+ stats['total_victories'] = conn.execute("SELECT COUNT(*) FROM victories").fetchone()[0]
231
+
232
+ # Participations par source
233
+ source_stats = conn.execute('''
234
+ SELECT source, COUNT(*) as count
235
+ FROM participations
236
+ GROUP BY source
237
+ ORDER BY count DESC
238
+ ''').fetchall()
239
+ stats['by_source'] = {row[0]: row[1] for row in source_stats}
240
+
241
+ return stats
242
+
243
+ # =====================================================
244
+ # MOTEUR IA AMÉLIORÉ
245
+ # =====================================================
246
+
247
+ class AIEngine:
248
+ def __init__(self, api_config: APIConfig):
249
+ self.api_config = api_config
250
+ self.cache = {}
251
+
252
+ def generate_response(self, question: str, context: str = "", response_type: str = "qa") -> str:
253
+ """Génère une réponse IA avec fallback entre Gemini et HuggingFace"""
254
+ cache_key = hashlib.md5(f"{question}{context}{response_type}".encode()).hexdigest()
255
+
256
+ if cache_key in self.cache:
257
+ return self.cache[cache_key]
258
+
259
+ response = self._try_gemini(question, context, response_type)
260
+ if not response:
261
+ response = self._try_huggingface(question, context, response_type)
262
+
263
+ if not response:
264
+ response = self._generate_fallback_response(question, response_type)
265
+
266
+ # Cache la réponse
267
+ self.cache[cache_key] = response
268
+ return response
269
+
270
+ def _try_gemini(self, question: str, context: str, response_type: str) -> Optional[str]:
271
+ # Gemini supprimé - utilisation d'alternatives locales uniquement
272
+ return None
273
+
274
+ def _try_huggingface(self, question: str, context: str, response_type: str) -> Optional[str]:
275
+ if not self.api_config.hf_token:
276
+ return None
277
+
278
+ try:
279
+ client = InferenceClient(token=self.api_config.hf_token)
280
+
281
+ if response_type == "qa" and context:
282
+ result = client.question_answering(
283
+ question=question,
284
+ context=context,
285
+ model="distilbert-base-cased-distilled-squad"
286
+ )
287
+ return result.answer
288
+ else:
289
+ prompt = f"Question: {question}\nContexte: {context}\nRéponse:"
290
+ result = client.text_generation(
291
+ prompt,
292
+ model="gpt2",
293
+ max_new_tokens=100,
294
+ temperature=0.7
295
+ )
296
+ return result[0]['generated_text'].split('Réponse:')[-1].strip()
297
+
298
+ except Exception as e:
299
+ logging.warning(f"HuggingFace API error: {e}")
300
+ return None
301
+
302
+ def _generate_fallback_response(self, question: str, response_type: str) -> str:
303
+ """Système de réponses intelligentes sans API"""
304
+ question_lower = question.lower()
305
+
306
+ if response_type == "motivation":
307
+ # Réponses de motivation personnalisées selon le contexte
308
+ if any(word in question_lower for word in ["voyage", "vacances", "séjour"]):
309
+ return random.choice([
310
+ "J'adore voyager et découvrir de nouveaux horizons. Ce prix serait une opportunité fantastique pour moi de vivre une expérience inoubliable.",
311
+ "Voyager est ma passion et ce concours représente le voyage de mes rêves. J'espère avoir la chance de le remporter.",
312
+ "En tant que passionné de voyages, ce prix m'offrirait l'occasion parfaite de découvrir de nouveaux paysages et cultures."
313
+ ])
314
+ elif any(word in question_lower for word in ["produit", "cosmétique", "beauté"]):
315
+ return random.choice([
316
+ "Je suis toujours à la recherche de nouveaux produits de qualité et j'aimerais beaucoup tester cette gamme.",
317
+ "Ces produits m'intéressent énormément et je serais ravi de pouvoir les découvrir.",
318
+ "J'ai entendu beaucoup de bien de cette marque et j'aimerais avoir l'opportunité de l'essayer."
319
+ ])
320
+ elif any(word in question_lower for word in ["technologie", "smartphone", "ordinateur"]):
321
+ return random.choice([
322
+ "En tant que passionné de technologie, ce prix m'intéresse beaucoup et m'aiderait dans mes projets.",
323
+ "J'ai besoin de ce type d'équipement pour mes études et ce serait formidable de le gagner.",
324
+ "La technologie fait partie de ma vie quotidienne et ce prix serait très utile."
325
+ ])
326
+ else:
327
+ return random.choice([
328
+ "Je participe avec enthousiasme à ce concours car le prix m'intéresse vraiment et correspond à mes besoins.",
329
+ "Ce concours m'attire particulièrement et je serais très heureux de remporter ce magnifique prix.",
330
+ "J'espère avoir la chance de gagner car ce prix me ferait énormément plaisir.",
331
+ "Je suis motivé à participer car cette opportunité pourrait vraiment changer ma journée."
332
+ ])
333
+
334
+ elif response_type == "quiz":
335
+ # Système de réponses intelligentes pour les quiz
336
+ if "suisse" in question_lower:
337
+ if "capitale" in question_lower:
338
+ return "Berne"
339
+ elif "langue" in question_lower:
340
+ return "Français, Allemand, Italien, Romanche"
341
+ elif "monnaie" in question_lower:
342
+ return "Franc suisse"
343
+ elif "population" in question_lower:
344
+ return "8.7 millions"
345
+
346
+ if "couleur" in question_lower:
347
+ return random.choice(["Rouge", "Bleu", "Vert", "Jaune"])
348
+
349
+ if any(word in question_lower for word in ["combien", "nombre", "quantité"]):
350
+ return random.choice(["3", "5", "10", "12", "20"])
351
+
352
+ if any(word in question_lower for word in ["année", "date", "quand"]):
353
+ return random.choice(["2024", "2023", "2025"])
354
+
355
+ # Réponses par défaut pour quiz
356
+ return random.choice(["A", "B", "C", "Oui", "Non", "Vrai", "Faux"])
357
+
358
+ # Autres types de réponses
359
+ response_mapping = {
360
+ "age": random.choice(["25", "28", "30", "32"]),
361
+ "profession": random.choice(["Étudiant", "Employé", "Consultant"]),
362
+ "ville": "Pully",
363
+ "pays": "Suisse",
364
+ "default": "Merci"
365
+ }
366
+
367
+ return response_mapping.get(response_type, response_mapping["default"])
368
+
369
+ # =====================================================
370
+ # SCRAPER INTELLIGENT
371
+ # =====================================================
372
+
373
+ class IntelligentScraper:
374
+ def __init__(self, db_manager: DatabaseManager, cache_duration_hours: int = 6):
375
+ self.db = db_manager
376
+ self.cache_duration = timedelta(hours=cache_duration_hours)
377
+ self.session = None
378
+ self.field_patterns = {
379
+ 'prenom': r'prenom|prénom|first.*name|nom.*prenom',
380
+ 'nom': r'nom(?!.*prenom)|last.*name|family.*name|surname',
381
+ 'email': r'email|e-mail|courriel',
382
+ 'telephone': r'tel|phone|telephone|téléphone',
383
+ 'adresse': r'adresse|address|rue|street',
384
+ 'code_postal': r'code.postal|zip|postal',
385
+ 'ville': r'ville|city|localité',
386
+ 'pays': r'pays|country|nation',
387
+ 'motivation': r'motivation|pourquoi|why|reason|raison',
388
+ 'quiz': r'question|quiz|réponse|answer'
389
+ }
390
+
391
+ async def __aenter__(self):
392
+ connector = aiohttp.TCPConnector(limit=10, limit_per_host=3)
393
+ timeout = aiohttp.ClientTimeout(total=30, connect=10)
394
+ self.session = aiohttp.ClientSession(
395
+ connector=connector,
396
+ timeout=timeout,
397
+ headers={'User-Agent': random.choice(USER_AGENTS)}
398
+ )
399
+ return self
400
+
401
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
402
+ if self.session:
403
+ await self.session.close()
404
+
405
+ async def scrape_all_sources(self) -> List[Contest]:
406
+ """Scrape tous les sites et sources"""
407
+ all_contests = []
408
+
409
+ # Scraper les sites web
410
+ web_contests = await self._scrape_websites()
411
+ all_contests.extend(web_contests)
412
+
413
+ # Scraper Google Search
414
+ google_contests = await self._scrape_google_search()
415
+ all_contests.extend(google_contests)
416
+
417
+ # Scraper Twitter/X
418
+ twitter_contests = await self._scrape_twitter()
419
+ all_contests.extend(twitter_contests)
420
+
421
+ # Filtrer les doublons et concours déjà traités
422
+ unique_contests = self._filter_unique_contests(all_contests)
423
+
424
+ logging.info(f"Total contests found: {len(all_contests)}, unique new: {len(unique_contests)}")
425
+ return unique_contests
426
+
427
+ async def _scrape_websites(self) -> List[Contest]:
428
+ """Scrape les sites web en parallèle"""
429
+ batch_size = 3
430
+ all_contests = []
431
+
432
+ for i in range(0, len(SITES_CH), batch_size):
433
+ batch = SITES_CH[i:i + batch_size]
434
+ tasks = [self._scrape_single_site(site) for site in batch]
435
+
436
+ batch_results = await asyncio.gather(*tasks, return_exceptions=True)
437
+
438
+ for result in batch_results:
439
+ if isinstance(result, list):
440
+ all_contests.extend(result)
441
+ elif isinstance(result, Exception):
442
+ logging.error(f"Batch scraping error: {result}")
443
+
444
+ await asyncio.sleep(2) # Pause entre batches
445
+
446
+ return all_contests
447
+
448
+ async def _scrape_single_site(self, url: str) -> List[Contest]:
449
+ """Scrape un site web spécifique"""
450
+ try:
451
+ content = await self._fetch_with_retry(url)
452
+ if not content:
453
+ return []
454
+
455
+ soup = BeautifulSoup(content, 'html.parser')
456
+ contests = self._extract_contests_from_soup(soup, url)
457
+
458
+ logging.info(f"Found {len(contests)} contests on {url}")
459
+ return contests
460
+
461
+ except Exception as e:
462
+ logging.error(f"Error scraping {url}: {e}")
463
+ return []
464
+
465
+ async def _fetch_with_retry(self, url: str, max_retries: int = 3) -> Optional[str]:
466
+ """Fetch avec retry et gestion d'erreurs"""
467
+ for attempt in range(max_retries):
468
+ try:
469
+ proxy = random.choice(PROXY_LIST) if PROXY_LIST else None
470
+ async with self.session.get(url, proxy=proxy) as response:
471
+ if response.status == 200:
472
+ return await response.text()
473
+ elif response.status == 429:
474
+ wait_time = 2 ** attempt * 5
475
+ logging.warning(f"Rate limited on {url}, waiting {wait_time}s")
476
+ await asyncio.sleep(wait_time)
477
+ else:
478
+ logging.warning(f"HTTP {response.status} for {url}")
479
+
480
+ except Exception as e:
481
+ logging.error(f"Attempt {attempt+1} failed for {url}: {e}")
482
+ if attempt < max_retries - 1:
483
+ await asyncio.sleep(2 ** attempt)
484
+
485
+ return None
486
+
487
+ def _extract_contests_from_soup(self, soup: BeautifulSoup, base_url: str) -> List[Contest]:
488
+ """Extrait les concours d'une page HTML"""
489
+ contests = []
490
+
491
+ # Sélecteurs pour différents types de conteneurs
492
+ selectors = [
493
+ '.contest', '.concours', '.jeu', '.competition', '.giveaway',
494
+ '[data-contest]', '[data-concours]', '.prize', '.lot',
495
+ 'article[class*="concours"]', '.entry', '.participate'
496
+ ]
497
+
498
+ containers = []
499
+ for selector in selectors:
500
+ containers.extend(soup.select(selector))
501
+
502
+ # Fallback: chercher des liens avec mots-clés
503
+ if not containers:
504
+ containers = soup.find_all('a', href=re.compile(r'concours|jeu|contest|participate', re.I))
505
+
506
+ for container in containers[:20]: # Limiter pour éviter le spam
507
+ try:
508
+ contest = self._parse_contest_container(container, base_url)
509
+ if contest and self._is_valid_contest(contest):
510
+ contests.append(contest)
511
+ except Exception as e:
512
+ logging.debug(f"Error parsing container: {e}")
513
+
514
+ return contests
515
+
516
+ def _parse_contest_container(self, container, base_url: str) -> Optional[Contest]:
517
+ """Parse un conteneur de concours"""
518
+ # Extraire le titre
519
+ title_selectors = ['h1', 'h2', 'h3', '.title', '.titre', '.contest-title']
520
+ title = ""
521
+ for selector in title_selectors:
522
+ title_elem = container.select_one(selector)
523
+ if title_elem:
524
+ title = title_elem.get_text(strip=True)
525
+ break
526
+
527
+ if not title:
528
+ title = container.get_text(strip=True)[:100]
529
+
530
+ # Extraire le lien
531
+ url = ""
532
+ link_elem = container if container.name == 'a' else container.find('a')
533
+ if link_elem and link_elem.get('href'):
534
+ url = urljoin(base_url, link_elem['href'])
535
+
536
+ # Extraire la description
537
+ description = container.get_text(strip=True)[:500]
538
+
539
+ # Extraire deadline et prix
540
+ deadline = self._extract_deadline(description)
541
+ prize = self._extract_prize(description)
542
+
543
+ if not title or not url:
544
+ return None
545
+
546
+ return Contest(
547
+ title=title[:200],
548
+ url=url,
549
+ description=description,
550
+ source=base_url,
551
+ deadline=deadline,
552
+ prize=prize,
553
+ difficulty_score=self._estimate_difficulty(description)
554
+ )
555
+
556
+ def _extract_deadline(self, text: str) -> Optional[str]:
557
+ """Extrait la date limite du texte"""
558
+ patterns = [
559
+ r"jusqu[\'']?au (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
560
+ r"avant le (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
561
+ r"fin le (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})"
562
+ ]
563
+
564
+ for pattern in patterns:
565
+ match = re.search(pattern, text, re.I)
566
+ if match:
567
+ return match.group(1)
568
+ return None
569
+
570
+ def _extract_prize(self, text: str) -> Optional[str]:
571
+ """Extrait le prix du texte"""
572
+ patterns = [
573
+ r"gagne[rz]?\s+([^.!?]{1,50})",
574
+ r"prix[:\s]+([^.!?]{1,50})",
575
+ r"lot[:\s]+([^.!?]{1,50})",
576
+ r"(\d+\s*CHF|\d+\s*euros?|\d+\s*francs?)"
577
+ ]
578
+
579
+ for pattern in patterns:
580
+ match = re.search(pattern, text, re.I)
581
+ if match:
582
+ return match.group(1).strip()
583
+ return None
584
+
585
+ def _estimate_difficulty(self, description: str) -> int:
586
+ """Estime la difficulté de participation (0-10)"""
587
+ difficulty = 0
588
+
589
+ if re.search(r'justifi|motivation|pourquoi|essay', description, re.I):
590
+ difficulty += 3
591
+ if re.search(r'photo|image|créat', description, re.I):
592
+ difficulty += 2
593
+ if re.search(r'quiz|question|répond', description, re.I):
594
+ difficulty += 1
595
+ if re.search(r'partag|social|facebook|twitter', description, re.I):
596
+ difficulty += 1
597
+ if re.search(r'inscription|compte|profil', description, re.I):
598
+ difficulty += 1
599
+
600
+ return min(difficulty, 10)
601
+
602
+ def _is_valid_contest(self, contest: Contest) -> bool:
603
+ """Valide qu'un concours est légitime"""
604
+ swiss_indicators = [
605
+ 'suisse', 'switzerland', 'ch', 'romandie', 'genève', 'lausanne',
606
+ 'zurich', 'bern', 'ouvert en suisse', 'résidents suisses'
607
+ ]
608
+
609
+ full_text = (contest.title + " " + contest.description).lower()
610
+ has_swiss_access = any(indicator in full_text for indicator in swiss_indicators)
611
+
612
+ excluded_terms = [
613
+ 'payant', 'payment', 'carte bancaire', 'spam', 'phishing',
614
+ 'adult', 'casino', 'bitcoin', 'crypto', 'investment'
615
+ ]
616
+
617
+ has_excluded = any(term in full_text for term in excluded_terms)
618
+ valid_url = contest.url.startswith(('http://', 'https://'))
619
+
620
+ return has_swiss_access and not has_excluded and valid_url
621
+
622
+ async def _scrape_google_search(self) -> List[Contest]:
623
+ """Scrape via Google Custom Search API"""
624
+ if not API_CONFIG.google_api_key or not API_CONFIG.google_cx:
625
+ return []
626
+
627
+ try:
628
+ query = "concours gratuit Suisse 2025 site:.ch"
629
+ response = requests.get(
630
+ "https://www.googleapis.com/customsearch/v1",
631
+ params={
632
+ "key": API_CONFIG.google_api_key,
633
+ "cx": API_CONFIG.google_cx,
634
+ "q": query,
635
+ "num": 10
636
+ },
637
+ timeout=10
638
+ )
639
+
640
+ results = response.json().get('items', [])
641
+ contests = []
642
+
643
+ for res in results:
644
+ title = res.get('title', '')
645
+ url = res.get('link', '')
646
+ description = res.get('snippet', '')
647
+
648
+ contest = Contest(
649
+ title=title,
650
+ url=url,
651
+ description=description,
652
+ source='Google Search',
653
+ difficulty_score=self._estimate_difficulty(description)
654
+ )
655
+
656
+ if self._is_valid_contest(contest):
657
+ contests.append(contest)
658
+
659
+ logging.info(f"Found {len(contests)} contests via Google Search")
660
+ return contests
661
+
662
+ except Exception as e:
663
+ logging.error(f"Google Search API error: {e}")
664
+ return []
665
+
666
+ async def _scrape_twitter(self) -> List[Contest]:
667
+ """Scrape Twitter/X pour les concours"""
668
+ if not API_CONFIG.x_token:
669
+ return []
670
+
671
+ try:
672
+ client = tweepy.Client(bearer_token=API_CONFIG.x_token)
673
+ tweets = client.search_recent_tweets(
674
+ query="concours gratuit Suisse lang:fr",
675
+ max_results=10
676
+ )
677
+
678
+ contests = []
679
+ if tweets.data:
680
+ for tweet in tweets.data:
681
+ if self._is_swiss_accessible(tweet.text):
682
+ contest = Contest(
683
+ title=tweet.text[:50] + "...",
684
+ url=f"https://x.com/i/status/{tweet.id}",
685
+ description=tweet.text,
686
+ source='Twitter/X',
687
+ difficulty_score=5 # Score moyen pour les tweets
688
+ )
689
+ contests.append(contest)
690
+
691
+ logging.info(f"Found {len(contests)} contests on Twitter/X")
692
+ return contests
693
+
694
+ except Exception as e:
695
+ logging.error(f"Twitter/X API error: {e}")
696
+ return []
697
+
698
+ def _is_swiss_accessible(self, text: str) -> bool:
699
+ """Vérifie si accessible depuis la Suisse"""
700
+ swiss_pattern = r"(suisse|ch|romandie|ouvert\s+a\s+la\s+suisse|geneve|lausanne)"
701
+ return bool(re.search(swiss_pattern, text.lower(), re.IGNORECASE))
702
+
703
+ def _filter_unique_contests(self, contests: List[Contest]) -> List[Contest]:
704
+ """Filtre les concours uniques et non déjà traités"""
705
+ unique_contests = []
706
+ seen_urls = set()
707
+
708
+ for contest in contests:
709
+ if contest.url not in seen_urls and not self.db.participation_exists(contest.url):
710
+ unique_contests.append(contest)
711
+ seen_urls.add(contest.url)
712
+
713
+ return unique_contests
714
+
715
+ # =====================================================
716
+ # SYSTÈME DE PARTICIPATION INTELLIGENT
717
+ # =====================================================
718
+
719
+ class SmartParticipator:
720
+ def __init__(self, db_manager: DatabaseManager, ai_engine: AIEngine):
721
+ self.db = db_manager
722
+ self.ai = ai_engine
723
+ self.personal_info = PERSONAL_INFO
724
+
725
+ self.field_patterns = {
726
+ 'prenom': [r'prenom|prénom|first.*name'],
727
+ 'nom': [r'nom(?!.*prenom)|last.*name|family.*name'],
728
+ 'email': [r'email|e-mail|courriel'],
729
+ 'telephone': [r'tel|phone|telephone|téléphone'],
730
+ 'adresse': [r'adresse|address|rue|street'],
731
+ 'code_postal': [r'code.postal|zip|postal'],
732
+ 'ville': [r'ville|city|localité'],
733
+ 'pays': [r'pays|country|nation'],
734
+ 'motivation': [r'motivation|pourquoi|why|reason'],
735
+ 'quiz': [r'question|quiz|réponse|answer']
736
+ }
737
+
738
+ async def participate_in_contest(self, contest: Contest) -> bool:
739
+ """Participe à un concours de manière intelligente"""
740
+ async with async_playwright() as p:
741
+ browser = await p.chromium.launch(
742
+ headless=True,
743
+ args=['--no-sandbox', '--disable-blink-features=AutomationControlled']
744
+ )
745
+
746
+ context = await browser.new_context(
747
+ user_agent=random.choice(USER_AGENTS),
748
+ viewport={'width': 1920, 'height': 1080}
749
+ )
750
+
751
+ page = await context.new_page()
752
+
753
+ try:
754
+ # Analyser le formulaire
755
+ analysis = await self._analyze_form(page, contest.url)
756
+
757
+ if analysis.estimated_success_rate < 0.3:
758
+ logging.warning(f"Low success rate for {contest.url}: {analysis.estimated_success_rate}")
759
+ self.db.add_participation(contest, 'skipped_low_success', analysis.estimated_success_rate)
760
+ return False
761
+
762
+ if analysis.requires_captcha:
763
+ logging.warning(f"CAPTCHA detected for {contest.url}, skipping")
764
+ self.db.add_participation(contest, 'skipped_captcha', analysis.estimated_success_rate)
765
+ return False
766
+
767
+ # Remplir et soumettre le formulaire
768
+ success = await self._fill_and_submit_form(page, analysis, contest)
769
+
770
+ status = 'success' if success else 'failed'
771
+ self.db.add_participation(contest, status, analysis.estimated_success_rate)
772
+
773
+ logging.info(f"Participation {'successful' if success else 'failed'} for {contest.title}")
774
+ return success
775
+
776
+ except Exception as e:
777
+ logging.error(f"Participation error for {contest.url}: {e}")
778
+ self.db.add_participation(contest, 'error', 0.0)
779
+ return False
780
+
781
+ finally:
782
+ await browser.close()
783
+
784
+ async def _analyze_form(self, page: Page, url: str) -> FormAnalysis:
785
+ """Analyse un formulaire de concours"""
786
+ try:
787
+ await page.goto(url, wait_until='networkidle', timeout=15000)
788
+
789
+ # Détecter les champs
790
+ fields = await self._detect_form_fields(page)
791
+
792
+ # Calculer la complexité
793
+ complexity = sum(self._calculate_field_complexity(field) for field in fields)
794
+
795
+ # Détecter les éléments bloquants
796
+ has_captcha = await self._detect_captcha(page)
797
+ has_social_requirements = await self._detect_social_requirements(page)
798
+
799
+ # Estimer le taux de succès
800
+ success_rate = self._estimate_success_rate(fields, complexity, has_captcha)
801
+
802
+ return FormAnalysis(
803
+ fields=fields,
804
+ complexity_score=complexity,
805
+ estimated_success_rate=success_rate,
806
+ requires_captcha=has_captcha,
807
+ requires_social_media=has_social_requirements,
808
+ form_url=url
809
+ )
810
+
811
+ except Exception as e:
812
+ logging.error(f"Form analysis error: {e}")
813
+ return FormAnalysis([], 10, 0.0, True, True, url)
814
+
815
+ async def _detect_form_fields(self, page: Page) -> List[FormField]:
816
+ """Détecte tous les champs de formulaire"""
817
+ fields = []
818
+
819
+ selectors = [
820
+ 'input[type="text"]', 'input[type="email"]', 'input[type="tel"]',
821
+ 'input[type="number"]', 'input:not([type])', 'textarea', 'select'
822
+ ]
823
+
824
+ for selector in selectors:
825
+ elements = await page.query_selector_all(selector)
826
+
827
+ for element in elements:
828
+ try:
829
+ field = await self._analyze_single_field(element, page)
830
+ if field:
831
+ fields.append(field)
832
+ except Exception:
833
+ continue
834
+
835
+ return fields
836
+
837
+ async def _analyze_single_field(self, element, page: Page) -> Optional[FormField]:
838
+ """Analyse un champ individuel"""
839
+ try:
840
+ tag_name = await element.evaluate('el => el.tagName.toLowerCase()')
841
+ field_type = await element.evaluate('el => el.type || el.tagName.toLowerCase()')
842
+ name = await element.evaluate('el => el.name || el.id || ""')
843
+ placeholder = await element.evaluate('el => el.placeholder || ""')
844
+ required = await element.evaluate('el => el.required')
845
+
846
+ # Trouver le label
847
+ label_text = await self._find_field_label(element, page)
848
+
849
+ # Créer un sélecteur unique
850
+ selector = await self._create_unique_selector(element)
851
+
852
+ return FormField(
853
+ selector=selector,
854
+ field_type=field_type,
855
+ label=label_text,
856
+ required=required,
857
+ ai_context=f"Name: {name}, Placeholder: {placeholder}, Label: {label_text}"
858
+ )
859
+
860
+ except Exception:
861
+ return None
862
+
863
+ async def _find_field_label(self, element, page: Page) -> str:
864
+ """Trouve le label associé à un champ"""
865
+ try:
866
+ # Méthode 1: label[for]
867
+ element_id = await element.evaluate('el => el.id')
868
+ if element_id:
869
+ label = await page.query_selector(f'label[for="{element_id}"]')
870
+ if label:
871
+ return await label.inner_text()
872
+
873
+ # Méthode 2: parent label
874
+ parent_label = await element.evaluate('''
875
+ el => {
876
+ let parent = el.parentElement;
877
+ while (parent && parent.tagName !== 'BODY') {
878
+ if (parent.tagName === 'LABEL') {
879
+ return parent.innerText;
880
+ }
881
+ parent = parent.parentElement;
882
+ }
883
+ return '';
884
+ }
885
+ ''')
886
+
887
+ if parent_label:
888
+ return parent_label.strip()
889
+
890
+ # Méthode 3: texte précédent
891
+ prev_text = await element.evaluate('''
892
+ el => {
893
+ const prev = el.previousElementSibling;
894
+ return prev ? prev.innerText : '';
895
+ }
896
+ ''')
897
+
898
+ return prev_text.strip()
899
+
900
+ except Exception:
901
+ return ""
902
+
903
+ async def _create_unique_selector(self, element) -> str:
904
+ """Crée un sélecteur CSS unique"""
905
+ # Priorité: ID > Name > Class > Position
906
+ element_id = await element.evaluate('el => el.id')
907
+ if element_id:
908
+ return f'#{element_id}'
909
+
910
+ name = await element.evaluate('el => el.name')
911
+ if name:
912
+ return f'[name="{name}"]'
913
+
914
+ class_name = await element.evaluate('el => el.className')
915
+ tag_name = await element.evaluate('el => el.tagName.toLowerCase()')
916
+
917
+ if class_name:
918
+ return f'{tag_name}.{class_name.split()[0]}'
919
+
920
+ # Fallback
921
+ return f'{tag_name}:nth-of-type(1)'
922
+
923
+ def _calculate_field_complexity(self, field: FormField) -> int:
924
+ """Calcule la complexité d'un champ"""
925
+ complexity = 1
926
+
927
+ if field.field_type == 'textarea':
928
+ complexity += 3
929
+ elif field.field_type == 'select':
930
+ complexity += 2
931
+ elif field.required:
932
+ complexity += 1
933
+
934
+ if re.search(r'motivation|justifi|pourquoi', field.label, re.I):
935
+ complexity += 3
936
+ elif re.search(r'quiz|question', field.label, re.I):
937
+ complexity += 2
938
+
939
+ return complexity
940
+
941
+ async def _detect_captcha(self, page: Page) -> bool:
942
+ """Détecte la présence de CAPTCHA"""
943
+ captcha_selectors = [
944
+ '.g-recaptcha', '.h-captcha', '#captcha', '.captcha',
945
+ 'iframe[src*="recaptcha"]', 'iframe[src*="hcaptcha"]'
946
+ ]
947
+
948
+ for selector in captcha_selectors:
949
+ element = await page.query_selector(selector)
950
+ if element:
951
+ return True
952
+
953
+ return False
954
+
955
+ async def _detect_social_requirements(self, page: Page) -> bool:
956
+ """Détecte les exigences de réseaux sociaux"""
957
+ content = await page.content()
958
+ social_patterns = [
959
+ r'follow.*us', r'partag.*facebook', r'retweet',
960
+ r'like.*page', r'subscribe.*channel'
961
+ ]
962
+
963
+ for pattern in social_patterns:
964
+ if re.search(pattern, content, re.I):
965
+ return True
966
+
967
+ return False
968
+
969
+ def _estimate_success_rate(self, fields: List[FormField], complexity: int, has_captcha: bool) -> float:
970
+ """Estime le taux de succès"""
971
+ base_rate = 0.8
972
+
973
+ if has_captcha:
974
+ base_rate *= 0.1
975
+
976
+ if complexity > 15:
977
+ base_rate *= 0.4
978
+ elif complexity > 10:
979
+ base_rate *= 0.6
980
+ elif complexity > 5:
981
+ base_rate *= 0.8
982
+
983
+ required_fields = [f for f in fields if f.required]
984
+ if len(required_fields) <= 3:
985
+ base_rate *= 1.1
986
+
987
+ return min(base_rate, 1.0)
988
+
989
+ async def _fill_and_submit_form(self, page: Page, analysis: FormAnalysis, contest: Contest) -> bool:
990
+ """Remplit et soumet le formulaire"""
991
+ try:
992
+ filled_fields = 0
993
+
994
+ for field in analysis.fields:
995
+ try:
996
+ # Attendre l'élément
997
+ await page.wait_for_selector(field.selector, timeout=3000)
998
+ element = await page.query_selector(field.selector)
999
+
1000
+ if not element:
1001
+ continue
1002
+
1003
+ # Générer la valeur
1004
+ value = await self._generate_field_value(field, contest, page)
1005
+ if not value:
1006
+ continue
1007
+
1008
+ # Remplir selon le type
1009
+ if field.field_type == 'select':
1010
+ await self._fill_select_field(element, value, page)
1011
+ else:
1012
+ await element.fill(value)
1013
+
1014
+ filled_fields += 1
1015
+ await asyncio.sleep(random.uniform(0.3, 0.8))
1016
+
1017
+ except Exception as e:
1018
+ logging.debug(f"Error filling field {field.selector}: {e}")
1019
+ continue
1020
+
1021
+ # Soumettre le formulaire
1022
+ submit_success = await self._submit_form(page)
1023
+
1024
+ logging.info(f"Filled {filled_fields}/{len(analysis.fields)} fields, submitted: {submit_success}")
1025
+ return filled_fields > 0 and submit_success
1026
+
1027
+ except Exception as e:
1028
+ logging.error(f"Form filling error: {e}")
1029
+ return False
1030
+
1031
+ async def _generate_field_value(self, field: FormField, contest: Contest, page: Page) -> Optional[str]:
1032
+ """Génère une valeur pour un champ"""
1033
+ # Identifier le type de champ
1034
+ field_type = self._identify_field_type(field)
1035
+
1036
+ # Mapping des valeurs personnelles
1037
+ personal_mapping = {
1038
+ 'prenom': self.personal_info.prenom,
1039
+ 'nom': self.personal_info.nom,
1040
+ 'email': self.personal_info.email_derivee,
1041
+ 'telephone': self.personal_info.telephone,
1042
+ 'adresse': self.personal_info.adresse,
1043
+ 'code_postal': self.personal_info.code_postal,
1044
+ 'ville': self.personal_info.ville,
1045
+ 'pays': self.personal_info.pays
1046
+ }
1047
+
1048
+ if field_type in personal_mapping:
1049
+ return personal_mapping[field_type]
1050
+
1051
+ # Cas spéciaux nécessitant l'IA
1052
+ if field_type == 'motivation':
1053
+ return self.ai.generate_response(
1054
+ field.label,
1055
+ contest.description,
1056
+ "motivation"
1057
+ )
1058
+ elif field_type == 'quiz':
1059
+ return self.ai.generate_response(
1060
+ field.label,
1061
+ contest.description,
1062
+ "quiz"
1063
+ )
1064
+
1065
+ # Valeurs par défaut
1066
+ default_values = {
1067
+ 'age': '25',
1068
+ 'genre': 'Monsieur',
1069
+ 'profession': 'Étudiant'
1070
+ }
1071
+
1072
+ for key, value in default_values.items():
1073
+ if key in field.label.lower():
1074
+ return value
1075
+
1076
+ return None
1077
+
1078
+ def _identify_field_type(self, field: FormField) -> str:
1079
+ """Identifie le type de champ"""
1080
+ combined_text = f"{field.ai_context} {field.label}".lower()
1081
+
1082
+ for field_type, patterns in self.field_patterns.items():
1083
+ for pattern in patterns:
1084
+ if re.search(pattern, combined_text, re.I):
1085
+ return field_type
1086
+
1087
+ return 'unknown'
1088
+
1089
+ async def _fill_select_field(self, element, value: str, page: Page):
1090
+ """Remplit un champ select"""
1091
+ try:
1092
+ options = await element.query_selector_all('option')
1093
+
1094
+ for option in options:
1095
+ option_text = await option.inner_text()
1096
+ option_value = await option.get_attribute('value')
1097
+
1098
+ if (value.lower() in option_text.lower() or
1099
+ value.lower() in (option_value or "").lower()):
1100
+ await element.select_option(value=option_value)
1101
+ return
1102
+
1103
+ # Fallback: sélectionner la première option valide
1104
+ if options and len(options) > 1:
1105
+ first_option = await options[1].get_attribute('value')
1106
+ await element.select_option(value=first_option)
1107
+
1108
+ except Exception as e:
1109
+ logging.debug(f"Select field error: {e}")
1110
+
1111
+ async def _submit_form(self, page: Page) -> bool:
1112
+ """Soumet le formulaire"""
1113
+ submit_selectors = [
1114
+ 'input[type="submit"]',
1115
+ 'button[type="submit"]',
1116
+ 'button:has-text("Participer")',
1117
+ 'button:has-text("Envoyer")',
1118
+ 'button:has-text("Valider")',
1119
+ '.submit-btn',
1120
+ '.participate-btn'
1121
+ ]
1122
+
1123
+ for selector in submit_selectors:
1124
+ try:
1125
+ element = await page.query_selector(selector)
1126
+ if element:
1127
+ is_visible = await element.is_visible()
1128
+ is_enabled = await element.is_enabled()
1129
+
1130
+ if is_visible and is_enabled:
1131
+ await element.click()
1132
+ await page.wait_for_timeout(3000)
1133
+ return True
1134
+
1135
+ except Exception:
1136
+ continue
1137
+
1138
+ return False
1139
+
1140
+ # =====================================================
1141
+ # GESTIONNAIRE D'EMAILS ET ALERTES
1142
+ # =====================================================
1143
+
1144
+ class EmailManager:
1145
+ def __init__(self, api_config: APIConfig, ai_engine: AIEngine, db_manager: DatabaseManager):
1146
+ self.api_config = api_config
1147
+ self.ai = ai_engine
1148
+ self.db = db_manager
1149
+ self.personal_info = PERSONAL_INFO
1150
+
1151
+ def check_and_analyze_emails(self):
1152
+ """Vérifie et analyse les emails pour détecter les victoires"""
1153
+ if not self.api_config.email_app_password:
1154
+ logging.warning("Email app password not configured")
1155
+ return
1156
+
1157
+ try:
1158
+ mail = imaplib.IMAP4_SSL('outlook.office365.com')
1159
+ mail.login(self.personal_info.email, self.api_config.email_app_password)
1160
+ mail.select('inbox')
1161
+
1162
+ # Chercher les emails récents
1163
+ since_date = (datetime.now() - timedelta(days=7)).strftime('%d-%b-%Y')
1164
+ status, messages = mail.search(None, f'(SINCE "{since_date}")')
1165
+
1166
+ victories = []
1167
+ email_count = 0
1168
+
1169
+ for num in messages[0].split()[-20:]: # Limiter aux 20 derniers
1170
+ try:
1171
+ email_count += 1
1172
+ _, msg = mail.fetch(num, '(RFC822)')
1173
+ email_msg = email.message_from_bytes(msg[0][1])
1174
+
1175
+ subject = email_msg['Subject'] or ""
1176
+ from_addr = email_msg['From'] or ""
1177
+
1178
+ # Extraire le corps de l'email
1179
+ body = self._extract_email_body(email_msg)
1180
+
1181
+ # Analyser avec l'IA
1182
+ analysis = self.ai.generate_response(
1183
+ f"Cet email indique-t-il une victoire dans un concours ? Sujet: {subject}",
1184
+ body[:1000],
1185
+ "quiz"
1186
+ )
1187
+
1188
+ if 'oui' in analysis.lower() or 'gagne' in analysis.lower():
1189
+ prize = self._extract_prize_from_email(body, subject)
1190
+ victories.append({
1191
+ 'email_id': num.decode(),
1192
+ 'date': datetime.now().strftime('%Y-%m-%d'),
1193
+ 'prize': prize,
1194
+ 'source': from_addr,
1195
+ 'subject': subject
1196
+ })
1197
+
1198
+ # Enregistrer en base
1199
+ self.db.add_victory(num.decode(), prize, from_addr)
1200
+
1201
+ # Envoyer alerte
1202
+ self._send_victory_alert(prize, from_addr, subject)
1203
+
1204
+ logging.info(f"Victory detected: {prize} from {from_addr}")
1205
+
1206
+ except Exception as e:
1207
+ logging.error(f"Error processing email {num}: {e}")
1208
+ continue
1209
+
1210
+ mail.logout()
1211
+ logging.info(f"Processed {email_count} emails, found {len(victories)} victories")
1212
+
1213
+ except Exception as e:
1214
+ logging.error(f"Email checking error: {e}")
1215
+
1216
+ def _extract_email_body(self, email_msg) -> str:
1217
+ """Extrait le corps de l'email"""
1218
+ body = ""
1219
+
1220
+ if email_msg.is_multipart():
1221
+ for part in email_msg.walk():
1222
+ if part.get_content_type() == 'text/plain':
1223
+ try:
1224
+ body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
1225
+ break
1226
+ except:
1227
+ continue
1228
+ else:
1229
+ try:
1230
+ body = email_msg.get_payload(decode=True).decode('utf-8', errors='ignore')
1231
+ except:
1232
+ body = str(email_msg.get_payload())
1233
+
1234
+ return body
1235
+
1236
+ def _extract_prize_from_email(self, body: str, subject: str) -> str:
1237
+ """Extrait le prix gagné de l'email"""
1238
+ text = f"{subject} {body}"
1239
+
1240
+ prize_patterns = [
1241
+ r"vous avez gagné\s+([^.!?\n]{1,100})",
1242
+ r"prix[:\s]+([^.!?\n]{1,100})",
1243
+ r"lot[:\s]+([^.!?\n]{1,100})",
1244
+ r"remporté\s+([^.!?\n]{1,100})",
1245
+ r"(\d+\s*CHF|\d+\s*euros?|\d+\s*francs?)"
1246
+ ]
1247
+
1248
+ for pattern in prize_patterns:
1249
+ match = re.search(pattern, text, re.I)
1250
+ if match:
1251
+ return match.group(1).strip()
1252
+
1253
+ return "Prix non spécifié"
1254
+
1255
+ def _send_victory_alert(self, prize: str, source: str, subject: str):
1256
+ """Envoie une alerte de victoire"""
1257
+ if not self.api_config.telegram_bot_token or not self.api_config.telegram_chat_id:
1258
+ return
1259
+
1260
+ message = f"""
1261
+ 🎉 **VICTOIRE DÉTECTÉE !**
1262
+
1263
+ 🏆 **Prix**: {prize}
1264
+ 📧 **Source**: {source}
1265
+ 📋 **Sujet**: {subject}
1266
+ 📅 **Date**: {datetime.now().strftime('%d/%m/%Y %H:%M')}
1267
+
1268
+ Félicitations ! 🎊
1269
+ """
1270
+
1271
+ try:
1272
+ url = f"https://api.telegram.org/bot{self.api_config.telegram_bot_token}/sendMessage"
1273
+ payload = {
1274
+ 'chat_id': self.api_config.telegram_chat_id,
1275
+ 'text': message,
1276
+ 'parse_mode': 'Markdown'
1277
+ }
1278
+
1279
+ response = requests.post(url, json=payload, timeout=10)
1280
+ if response.status_code == 200:
1281
+ logging.info("Victory alert sent successfully")
1282
+ else:
1283
+ logging.error(f"Telegram alert failed: {response.text}")
1284
+
1285
+ except Exception as e:
1286
+ logging.error(f"Telegram alert error: {e}")
1287
+
1288
+ # =====================================================
1289
+ # SYSTÈME DE MONITORING ET STATISTIQUES
1290
+ # =====================================================
1291
+
1292
+ class MonitoringSystem:
1293
+ def __init__(self, db_manager: DatabaseManager):
1294
+ self.db = db_manager
1295
+
1296
+ def generate_daily_report(self) -> str:
1297
+ """Génère un rapport quotidien"""
1298
+ stats = self.db.get_stats()
1299
+
1300
+ # Statistiques du jour
1301
+ today = datetime.now().strftime('%Y-%m-%d')
1302
+ conn = self.db._get_connection()
1303
+
1304
+ today_participations = conn.execute(
1305
+ "SELECT COUNT(*) FROM participations WHERE date = ?", (today,)
1306
+ ).fetchone()[0]
1307
+
1308
+ today_successes = conn.execute(
1309
+ "SELECT COUNT(*) FROM participations WHERE date = ? AND status = 'success'", (today,)
1310
+ ).fetchone()[0]
1311
+
1312
+ success_rate = (today_successes / max(today_participations, 1)) * 100
1313
+
1314
+ report = f"""
1315
+ 📊 **RAPPORT QUOTIDIEN - {today}**
1316
+
1317
+ 🎯 **Aujourd'hui**:
1318
+ • Participations: {today_participations}
1319
+ • Succès: {today_successes}
1320
+ • Taux de succès: {success_rate:.1f}%
1321
+
1322
+ 📈 **Total**:
1323
+ • Participations totales: {stats['total_participations']}
1324
+ • Participations réussies: {stats['successful_participations']}
1325
+ • Victoires détectées: {stats['total_victories']}
1326
+
1327
+ 🌐 **Par source**:
1328
+ """
1329
+
1330
+ for source, count in stats['by_source'].items():
1331
+ report += f" • {source}: {count}\n"
1332
+
1333
+ return report
1334
+
1335
+ def send_daily_report(self):
1336
+ """Envoie le rapport quotidien"""
1337
+ if not API_CONFIG.telegram_bot_token or not API_CONFIG.telegram_chat_id:
1338
+ return
1339
+
1340
+ report = self.generate_daily_report()
1341
+
1342
+ try:
1343
+ url = f"https://api.telegram.org/bot{API_CONFIG.telegram_bot_token}/sendMessage"
1344
+ payload = {
1345
+ 'chat_id': API_CONFIG.telegram_chat_id,
1346
+ 'text': report,
1347
+ 'parse_mode': 'Markdown'
1348
+ }
1349
+
1350
+ response = requests.post(url, json=payload, timeout=10)
1351
+ if response.status_code == 200:
1352
+ logging.info("Daily report sent successfully")
1353
+ else:
1354
+ logging.error(f"Daily report failed: {response.text}")
1355
+
1356
+ except Exception as e:
1357
+ logging.error(f"Daily report error: {e}")
1358
+
1359
+ # =====================================================
1360
+ # ORCHESTRATEUR PRINCIPAL
1361
+ # =====================================================
1362
+
1363
+ class ContestBotOrchestrator:
1364
+ def __init__(self):
1365
+ self.db = DatabaseManager()
1366
+ self.ai = AIEngine(API_CONFIG)
1367
+ self.scraper = None # Initialisé dans le contexte async
1368
+ self.participator = SmartParticipator(self.db, self.ai)
1369
+ self.email_manager = EmailManager(API_CONFIG, self.ai, self.db)
1370
+ self.monitor = MonitoringSystem(self.db)
1371
+
1372
+ async def run_full_cycle(self):
1373
+ """Execute un cycle complet de scraping et participation"""
1374
+ logging.info("Starting full contest bot cycle")
1375
+
1376
+ try:
1377
+ # 1. Scraping des concours
1378
+ async with IntelligentScraper(self.db) as scraper:
1379
+ self.scraper = scraper
1380
+ contests = await scraper.scrape_all_sources()
1381
+
1382
+ if not contests:
1383
+ logging.info("No new contests found")
1384
+ return
1385
+
1386
+ # 2. Trier par score de difficulté (plus faciles en premier)
1387
+ contests.sort(key=lambda x: x.difficulty_score)
1388
+
1389
+ # 3. Participer aux concours (limiter à 20 par jour)
1390
+ participation_count = 0
1391
+ max_daily_participations = 20
1392
+
1393
+ for contest in contests[:max_daily_participations]:
1394
+ try:
1395
+ # Pause entre participations pour éviter la détection
1396
+ if participation_count > 0:
1397
+ wait_time = random.uniform(30, 120) # 30s à 2min
1398
+ logging.info(f"Waiting {wait_time:.0f}s before next participation")
1399
+ await asyncio.sleep(wait_time)
1400
+
1401
+ success = await self.participator.participate_in_contest(contest)
1402
+ participation_count += 1
1403
+
1404
+ if success:
1405
+ logging.info(f"✅ Successfully participated in: {contest.title}")
1406
+ else:
1407
+ logging.warning(f"❌ Failed to participate in: {contest.title}")
1408
+
1409
+ # Pause plus longue après succès
1410
+ if success:
1411
+ await asyncio.sleep(random.uniform(60, 180))
1412
+
1413
+ except Exception as e:
1414
+ logging.error(f"Error participating in {contest.title}: {e}")
1415
+ continue
1416
+
1417
+ logging.info(f"Participation cycle completed: {participation_count} attempts")
1418
+
1419
+ except Exception as e:
1420
+ logging.error(f"Full cycle error: {e}")
1421
+
1422
+ def run_email_check(self):
1423
+ """Vérifie les emails pour les victoires"""
1424
+ try:
1425
+ logging.info("Checking emails for victories")
1426
+ self.email_manager.check_and_analyze_emails()
1427
+ except Exception as e:
1428
+ logging.error(f"Email check error: {e}")
1429
+
1430
+ def run_daily_report(self):
1431
+ """Génère et envoie le rapport quotidien"""
1432
+ try:
1433
+ logging.info("Generating daily report")
1434
+ self.monitor.send_daily_report()
1435
+ except Exception as e:
1436
+ logging.error(f"Daily report error: {e}")
1437
+
1438
+ # =====================================================
1439
+ # SCHEDULER ET POINT D'ENTRÉE
1440
+ # =====================================================
1441
+
1442
+ def run_bot_cycle():
1443
+ """Point d'entrée pour le scheduler"""
1444
+ bot = ContestBotOrchestrator()
1445
+
1446
+ # Cycle principal
1447
+ asyncio.run(bot.run_full_cycle())
1448
+
1449
+ # Vérification des emails
1450
+ bot.run_email_check()
1451
+
1452
+ def run_daily_report():
1453
+ """Point d'entrée pour le rapport quotidien"""
1454
+ bot = ContestBotOrchestrator()
1455
+ bot.run_daily_report()
1456
+
1457
+ def main():
1458
+ """Fonction principale avec scheduler"""
1459
+ logging.info("Starting Contest Bot with scheduler")
1460
+
1461
+ # Programmer les tâches
1462
+ schedule.every().day.at("08:00").do(run_bot_cycle)
1463
+ schedule.every().day.at("14:00").do(run_bot_cycle) # Deux fois par jour
1464
+ schedule.every().day.at("20:00").do(run_daily_report)
1465
+
1466
+ # Exécution immédiate pour test
1467
+ if len(sys.argv) > 1 and sys.argv[1] == "--run-now":
1468
+ logging.info("Running immediate cycle")
1469
+ run_bot_cycle()
1470
+ return
1471
+
1472
+ # Boucle principale du scheduler
1473
+ logging.info("Scheduler started. Waiting for scheduled tasks...")
1474
+
1475
+ while True:
1476
+ try:
1477
+ schedule.run_pending()
1478
+ time.sleep(60) # Vérifier chaque minute
1479
+ except KeyboardInterrupt:
1480
+ logging.info("Bot stopped by user")
1481
+ break
1482
+ except Exception as e:
1483
+ logging.error(f"Scheduler error: {e}")
1484
+ time.sleep(300) # Attendre 5 minutes en cas d'erreur
1485
+
1486
+ if __name__ == "__main__":
1487
+ main()