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

Upload bot_concours_sans_api.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. bot_concours_sans_api.py +1148 -0
bot_concours_sans_api.py ADDED
@@ -0,0 +1,1148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bot_concours_sans_api.py - Bot de concours 100% sans API externe
2
+ # Version simplifiée utilisant uniquement des bibliothèques locales
3
+
4
+ import asyncio
5
+ from playwright.async_api import async_playwright, Page
6
+ from bs4 import BeautifulSoup
7
+ import requests
8
+ import pandas as pd
9
+ import time
10
+ import re
11
+ from datetime import datetime, timedelta
12
+ import os
13
+ import sys
14
+ import random
15
+ import sqlite3
16
+ import logging
17
+ import schedule
18
+ import hashlib
19
+ import json
20
+ import threading
21
+ from contextlib import contextmanager
22
+ from dataclasses import dataclass
23
+ from typing import List, Dict, Optional, Tuple
24
+ from urllib.parse import urljoin, urlparse
25
+
26
+ # =====================================================
27
+ # CONFIGURATION ET DATACLASSES
28
+ # =====================================================
29
+
30
+ @dataclass
31
+ class PersonalInfo:
32
+ prenom: str = "Valentin"
33
+ nom: str = "Cora"
34
+ email: str = "valouassol@outlook.com"
35
+ email_derivee: str = "valouassol+concours@outlook.com"
36
+ telephone: str = "+41791234567"
37
+ adresse: str = "Av Chantemerle 9"
38
+ code_postal: str = "1009"
39
+ ville: str = "Pully"
40
+ pays: str = "Suisse"
41
+
42
+ @dataclass
43
+ class Contest:
44
+ title: str
45
+ url: str
46
+ description: str
47
+ source: str
48
+ deadline: Optional[str] = None
49
+ prize: Optional[str] = None
50
+ difficulty_score: int = 0
51
+
52
+ @dataclass
53
+ class FormField:
54
+ selector: str
55
+ field_type: str
56
+ label: str
57
+ required: bool
58
+ current_value: str = ""
59
+ ai_context: str = ""
60
+
61
+ @dataclass
62
+ class FormAnalysis:
63
+ fields: List[FormField]
64
+ complexity_score: int
65
+ estimated_success_rate: float
66
+ requires_captcha: bool
67
+ requires_social_media: bool
68
+ form_url: str
69
+
70
+ # =====================================================
71
+ # CONFIGURATION GLOBALE
72
+ # =====================================================
73
+
74
+ # Configuration
75
+ PERSONAL_INFO = PersonalInfo()
76
+
77
+ # Sites suisses à scraper
78
+ SITES_CH = [
79
+ 'https://www.concours.ch/concours/tous',
80
+ 'https://www.jeu-concours.biz/concours-pays_suisse.html',
81
+ 'https://www.loisirs.ch/concours/',
82
+ 'https://www.radin.ch/',
83
+ 'https://win4win.ch/fr/',
84
+ 'https://www.concours-suisse.ch/',
85
+ 'https://corporate.migros.ch/fr/concours',
86
+ 'https://www.20min.ch/fr/concours-et-jeux',
87
+ 'https://dein-gewinnspiel.ch/en',
88
+ 'https://www.myswitzerland.com/fr/planification/vie-pratique/concours/'
89
+ ]
90
+
91
+ # User Agents
92
+ USER_AGENTS = [
93
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
94
+ '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',
95
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
96
+ ]
97
+
98
+ # Logging setup
99
+ logging.basicConfig(
100
+ level=logging.INFO,
101
+ format='%(asctime)s - %(levelname)s - %(message)s',
102
+ handlers=[
103
+ logging.FileHandler('concours_bot_sans_api.log'),
104
+ logging.StreamHandler()
105
+ ]
106
+ )
107
+
108
+ # =====================================================
109
+ # GESTIONNAIRE DE BASE DE DONNÉES
110
+ # =====================================================
111
+
112
+ class DatabaseManager:
113
+ def __init__(self, db_path: str = 'concours_sans_api.sqlite'):
114
+ self.db_path = db_path
115
+ self.local = threading.local()
116
+ self._init_db()
117
+
118
+ def _get_connection(self):
119
+ if not hasattr(self.local, 'conn'):
120
+ self.local.conn = sqlite3.connect(self.db_path)
121
+ self.local.conn.row_factory = sqlite3.Row
122
+ return self.local.conn
123
+
124
+ @contextmanager
125
+ def transaction(self):
126
+ conn = self._get_connection()
127
+ try:
128
+ yield conn
129
+ conn.commit()
130
+ except Exception:
131
+ conn.rollback()
132
+ raise
133
+
134
+ def _init_db(self):
135
+ with self.transaction() as conn:
136
+ conn.execute('''
137
+ CREATE TABLE IF NOT EXISTS participations (
138
+ url TEXT PRIMARY KEY,
139
+ title TEXT,
140
+ source TEXT,
141
+ status TEXT,
142
+ difficulty_score INTEGER,
143
+ success_rate REAL,
144
+ date TEXT,
145
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
146
+ )
147
+ ''')
148
+ conn.execute('''
149
+ CREATE TABLE IF NOT EXISTS victories (
150
+ email_id TEXT PRIMARY KEY,
151
+ date TEXT,
152
+ lot TEXT,
153
+ source TEXT,
154
+ confirmed BOOLEAN DEFAULT FALSE,
155
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
156
+ )
157
+ ''')
158
+ # Index pour performances
159
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_participations_date ON participations(date)')
160
+
161
+ def add_participation(self, contest: Contest, status: str = 'pending', success_rate: float = 0.0):
162
+ with self.transaction() as conn:
163
+ conn.execute('''
164
+ INSERT OR REPLACE INTO participations
165
+ (url, title, source, status, difficulty_score, success_rate, date)
166
+ VALUES (?, ?, ?, ?, ?, ?, date('now'))
167
+ ''', (contest.url, contest.title, contest.source, status, contest.difficulty_score, success_rate))
168
+
169
+ def participation_exists(self, url: str) -> bool:
170
+ conn = self._get_connection()
171
+ result = conn.execute("SELECT 1 FROM participations WHERE url = ?", (url,)).fetchone()
172
+ return result is not None
173
+
174
+ def get_stats(self) -> Dict:
175
+ conn = self._get_connection()
176
+ stats = {}
177
+
178
+ stats['total_participations'] = conn.execute("SELECT COUNT(*) FROM participations").fetchone()[0]
179
+ stats['successful_participations'] = conn.execute("SELECT COUNT(*) FROM participations WHERE status='success'").fetchone()[0]
180
+ stats['total_victories'] = conn.execute("SELECT COUNT(*) FROM victories").fetchone()[0]
181
+
182
+ source_stats = conn.execute('''
183
+ SELECT source, COUNT(*) as count
184
+ FROM participations
185
+ GROUP BY source
186
+ ORDER BY count DESC
187
+ ''').fetchall()
188
+ stats['by_source'] = {row[0]: row[1] for row in source_stats}
189
+
190
+ return stats
191
+
192
+ # =====================================================
193
+ # MOTEUR DE RÉPONSES LOCALES
194
+ # =====================================================
195
+
196
+ class LocalResponseEngine:
197
+ def __init__(self):
198
+ self.cache = {}
199
+
200
+ # Base de connaissances locale
201
+ self.knowledge_base = {
202
+ "suisse": {
203
+ "capitale": "Berne",
204
+ "langues": "Français, Allemand, Italien, Romanche",
205
+ "monnaie": "Franc suisse",
206
+ "population": "8.7 millions",
207
+ "villes": ["Zurich", "Genève", "Bâle", "Lausanne", "Berne", "Winterthour"],
208
+ "cantons": ["Vaud", "Genève", "Valais", "Fribourg", "Neuchâtel", "Jura"]
209
+ },
210
+ "general": {
211
+ "couleurs": ["Rouge", "Bleu", "Vert", "Jaune", "Orange", "Violet", "Rose", "Noir", "Blanc"],
212
+ "nombres": ["1", "2", "3", "4", "5", "10", "12", "15", "20", "25", "50", "100"],
213
+ "annees": ["2023", "2024", "2025"]
214
+ }
215
+ }
216
+
217
+ def generate_response(self, question: str, context: str = "", response_type: str = "qa") -> str:
218
+ """Génère une réponse intelligente sans API"""
219
+ cache_key = hashlib.md5(f"{question}{context}{response_type}".encode()).hexdigest()
220
+
221
+ if cache_key in self.cache:
222
+ return self.cache[cache_key]
223
+
224
+ response = self._generate_intelligent_response(question, context, response_type)
225
+
226
+ # Cache la réponse
227
+ self.cache[cache_key] = response
228
+ return response
229
+
230
+ def _generate_intelligent_response(self, question: str, context: str, response_type: str) -> str:
231
+ """Génère une réponse basée sur l'analyse du texte"""
232
+ question_lower = question.lower()
233
+ context_lower = context.lower()
234
+
235
+ if response_type == "motivation":
236
+ return self._generate_motivation(question_lower, context_lower)
237
+ elif response_type == "quiz":
238
+ return self._generate_quiz_answer(question_lower, context_lower)
239
+ else:
240
+ return self._generate_general_response(question_lower, context_lower)
241
+
242
+ def _generate_motivation(self, question: str, context: str) -> str:
243
+ """Génère une motivation personnalisée"""
244
+ # Analyser le contexte pour adapter la réponse
245
+ if any(word in context for word in ["voyage", "vacances", "séjour", "destination"]):
246
+ return random.choice([
247
+ "J'adore voyager et découvrir de nouveaux horizons. Ce prix serait une opportunité fantastique pour moi de vivre une expérience inoubliable en Suisse ou ailleurs.",
248
+ "Voyager est ma passion et ce concours représente le voyage de mes rêves. J'espère avoir la chance de le remporter pour découvrir de nouveaux paysages.",
249
+ "En tant que passionné de voyages, ce prix m'offrirait l'occasion parfaite de découvrir de nouveaux lieux et cultures, ce qui m'enrichirait énormément."
250
+ ])
251
+ elif any(word in context for word in ["produit", "cosmétique", "beauté", "soin"]):
252
+ return random.choice([
253
+ "Je suis toujours à la recherche de nouveaux produits de qualité et j'aimerais beaucoup tester cette gamme qui semble très prometteuse.",
254
+ "Ces produits m'intéressent énormément et je serais ravi de pouvoir les découvrir et partager mon expérience avec mes proches.",
255
+ "J'ai entendu beaucoup de bien de cette marque et j'aimerais avoir l'opportunité de l'essayer pour me faire ma propre opinion."
256
+ ])
257
+ elif any(word in context for word in ["technologie", "smartphone", "ordinateur", "électronique"]):
258
+ return random.choice([
259
+ "En tant que passionné de technologie, ce prix m'intéresse beaucoup et m'aiderait dans mes projets personnels et professionnels.",
260
+ "J'ai besoin de ce type d'équipement pour mes études et mes loisirs, ce serait formidable de le gagner dans ce concours.",
261
+ "La technologie fait partie de ma vie quotidienne et ce prix serait très utile pour mes activités créatives."
262
+ ])
263
+ elif any(word in context for word in ["nourriture", "restaurant", "gastronomie", "cuisine"]):
264
+ return random.choice([
265
+ "J'adore découvrir de nouvelles saveurs et expériences culinaires. Ce prix me permettrait de vivre un moment gastronomique exceptionnel.",
266
+ "La cuisine est une de mes passions et ce concours m'offrirait l'opportunité de découvrir de nouveaux goûts et techniques.",
267
+ "En tant qu'amateur de bonne cuisine, je serais ravi de remporter ce prix pour explorer de nouvelles expériences gastronomiques."
268
+ ])
269
+ else:
270
+ return random.choice([
271
+ "Je participe avec enthousiasme à ce concours car le prix m'intéresse vraiment et correspond parfaitement à mes centres d'intérêt actuels.",
272
+ "Ce concours m'attire particulièrement et je serais très heureux de remporter ce magnifique prix qui me ferait énormément plaisir.",
273
+ "J'espère avoir la chance de gagner car ce prix représente une belle opportunité pour moi et ma famille.",
274
+ "Je suis motivé à participer car cette opportunité pourrait vraiment améliorer mon quotidien de manière positive."
275
+ ])
276
+
277
+ def _generate_quiz_answer(self, question: str, context: str) -> str:
278
+ """Génère une réponse de quiz intelligente"""
279
+ # Questions sur la Suisse
280
+ if "suisse" in question:
281
+ if any(word in question for word in ["capitale", "capital"]):
282
+ return self.knowledge_base["suisse"]["capitale"]
283
+ elif any(word in question for word in ["langue", "langues", "language"]):
284
+ return random.choice(["Français", "Allemand", "Italien", "Romanche"])
285
+ elif any(word in question for word in ["monnaie", "currency", "franc"]):
286
+ return self.knowledge_base["suisse"]["monnaie"]
287
+ elif any(word in question for word in ["population", "habitants"]):
288
+ return self.knowledge_base["suisse"]["population"]
289
+ elif any(word in question for word in ["ville", "city", "cities"]):
290
+ return random.choice(self.knowledge_base["suisse"]["villes"])
291
+ elif any(word in question for word in ["canton", "cantons"]):
292
+ return random.choice(self.knowledge_base["suisse"]["cantons"])
293
+
294
+ # Questions générales
295
+ if any(word in question for word in ["couleur", "color", "couleurs"]):
296
+ return random.choice(self.knowledge_base["general"]["couleurs"])
297
+
298
+ if any(word in question for word in ["combien", "nombre", "quantité", "how many"]):
299
+ return random.choice(self.knowledge_base["general"]["nombres"])
300
+
301
+ if any(word in question for word in ["année", "date", "quand", "when", "year"]):
302
+ return random.choice(self.knowledge_base["general"]["annees"])
303
+
304
+ # Questions oui/non
305
+ if any(word in question for word in ["est-ce", "is", "are", "do", "does"]):
306
+ return random.choice(["Oui", "Non"])
307
+
308
+ # Questions vrai/faux
309
+ if any(word in question for word in ["vrai", "faux", "true", "false"]):
310
+ return random.choice(["Vrai", "Faux"])
311
+
312
+ # Réponses par défaut pour quiz à choix multiples
313
+ return random.choice(["A", "B", "C", "D", "1", "2", "3"])
314
+
315
+ def _generate_general_response(self, question: str, context: str) -> str:
316
+ """Génère une réponse générale"""
317
+ if "age" in question or "âge" in question:
318
+ return random.choice(["25", "28", "30", "32"])
319
+ elif "profession" in question or "métier" in question:
320
+ return random.choice(["Étudiant", "Employé", "Consultant", "Développeur"])
321
+ elif "ville" in question:
322
+ return PERSONAL_INFO.ville
323
+ elif "pays" in question:
324
+ return PERSONAL_INFO.pays
325
+ else:
326
+ return "Merci"
327
+
328
+ # =====================================================
329
+ # SCRAPER INTELLIGENT SANS API
330
+ # =====================================================
331
+
332
+ class LocalScraper:
333
+ def __init__(self, db_manager: DatabaseManager):
334
+ self.db = db_manager
335
+ self.session = None
336
+
337
+ async def __aenter__(self):
338
+ import aiohttp
339
+ connector = aiohttp.TCPConnector(limit=10, limit_per_host=3)
340
+ timeout = aiohttp.ClientTimeout(total=30, connect=10)
341
+ self.session = aiohttp.ClientSession(
342
+ connector=connector,
343
+ timeout=timeout,
344
+ headers={'User-Agent': random.choice(USER_AGENTS)}
345
+ )
346
+ return self
347
+
348
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
349
+ if self.session:
350
+ await self.session.close()
351
+
352
+ async def scrape_all_sources(self) -> List[Contest]:
353
+ """Scrape tous les sites web localement"""
354
+ all_contests = []
355
+
356
+ # Scraper les sites web
357
+ web_contests = await self._scrape_websites()
358
+ all_contests.extend(web_contests)
359
+
360
+ # Filtrer les doublons et concours déjà traités
361
+ unique_contests = self._filter_unique_contests(all_contests)
362
+
363
+ logging.info(f"Total contests found: {len(all_contests)}, unique new: {len(unique_contests)}")
364
+ return unique_contests
365
+
366
+ async def _scrape_websites(self) -> List[Contest]:
367
+ """Scrape les sites web en parallèle"""
368
+ batch_size = 3
369
+ all_contests = []
370
+
371
+ for i in range(0, len(SITES_CH), batch_size):
372
+ batch = SITES_CH[i:i + batch_size]
373
+ tasks = [self._scrape_single_site(site) for site in batch]
374
+
375
+ batch_results = await asyncio.gather(*tasks, return_exceptions=True)
376
+
377
+ for result in batch_results:
378
+ if isinstance(result, list):
379
+ all_contests.extend(result)
380
+ elif isinstance(result, Exception):
381
+ logging.error(f"Batch scraping error: {result}")
382
+
383
+ await asyncio.sleep(2) # Pause entre batches
384
+
385
+ return all_contests
386
+
387
+ async def _scrape_single_site(self, url: str) -> List[Contest]:
388
+ """Scrape un site web spécifique"""
389
+ try:
390
+ content = await self._fetch_with_retry(url)
391
+ if not content:
392
+ return []
393
+
394
+ soup = BeautifulSoup(content, 'html.parser')
395
+ contests = self._extract_contests_from_soup(soup, url)
396
+
397
+ logging.info(f"Found {len(contests)} contests on {url}")
398
+ return contests
399
+
400
+ except Exception as e:
401
+ logging.error(f"Error scraping {url}: {e}")
402
+ return []
403
+
404
+ async def _fetch_with_retry(self, url: str, max_retries: int = 3) -> Optional[str]:
405
+ """Fetch avec retry et gestion d'erreurs"""
406
+ for attempt in range(max_retries):
407
+ try:
408
+ async with self.session.get(url) as response:
409
+ if response.status == 200:
410
+ return await response.text()
411
+ elif response.status == 429:
412
+ wait_time = 2 ** attempt * 5
413
+ logging.warning(f"Rate limited on {url}, waiting {wait_time}s")
414
+ await asyncio.sleep(wait_time)
415
+ else:
416
+ logging.warning(f"HTTP {response.status} for {url}")
417
+
418
+ except Exception as e:
419
+ logging.error(f"Attempt {attempt+1} failed for {url}: {e}")
420
+ if attempt < max_retries - 1:
421
+ await asyncio.sleep(2 ** attempt)
422
+
423
+ return None
424
+
425
+ def _extract_contests_from_soup(self, soup: BeautifulSoup, base_url: str) -> List[Contest]:
426
+ """Extrait les concours d'une page HTML"""
427
+ contests = []
428
+
429
+ # Sélecteurs pour différents types de conteneurs
430
+ selectors = [
431
+ '.contest', '.concours', '.jeu', '.competition', '.giveaway',
432
+ '[data-contest]', '[data-concours]', '.prize', '.lot',
433
+ 'article[class*="concours"]', '.entry', '.participate'
434
+ ]
435
+
436
+ containers = []
437
+ for selector in selectors:
438
+ containers.extend(soup.select(selector))
439
+
440
+ # Fallback: chercher des liens avec mots-clés
441
+ if not containers:
442
+ containers = soup.find_all('a', href=re.compile(r'concours|jeu|contest|participate', re.I))
443
+
444
+ for container in containers[:20]: # Limiter pour éviter le spam
445
+ try:
446
+ contest = self._parse_contest_container(container, base_url)
447
+ if contest and self._is_valid_contest(contest):
448
+ contests.append(contest)
449
+ except Exception as e:
450
+ logging.debug(f"Error parsing container: {e}")
451
+
452
+ return contests
453
+
454
+ def _parse_contest_container(self, container, base_url: str) -> Optional[Contest]:
455
+ """Parse un conteneur de concours"""
456
+ # Extraire le titre
457
+ title_selectors = ['h1', 'h2', 'h3', '.title', '.titre', '.contest-title']
458
+ title = ""
459
+ for selector in title_selectors:
460
+ title_elem = container.select_one(selector)
461
+ if title_elem:
462
+ title = title_elem.get_text(strip=True)
463
+ break
464
+
465
+ if not title:
466
+ title = container.get_text(strip=True)[:100]
467
+
468
+ # Extraire le lien
469
+ url = ""
470
+ link_elem = container if container.name == 'a' else container.find('a')
471
+ if link_elem and link_elem.get('href'):
472
+ url = urljoin(base_url, link_elem['href'])
473
+
474
+ # Extraire la description
475
+ description = container.get_text(strip=True)[:500]
476
+
477
+ # Extraire deadline et prix
478
+ deadline = self._extract_deadline(description)
479
+ prize = self._extract_prize(description)
480
+
481
+ if not title or not url:
482
+ return None
483
+
484
+ return Contest(
485
+ title=title[:200],
486
+ url=url,
487
+ description=description,
488
+ source=base_url,
489
+ deadline=deadline,
490
+ prize=prize,
491
+ difficulty_score=self._estimate_difficulty(description)
492
+ )
493
+
494
+ def _extract_deadline(self, text: str) -> Optional[str]:
495
+ """Extrait la date limite du texte"""
496
+ patterns = [
497
+ r"jusqu[\'']?au (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
498
+ r"avant le (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
499
+ r"fin le (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})"
500
+ ]
501
+
502
+ for pattern in patterns:
503
+ match = re.search(pattern, text, re.I)
504
+ if match:
505
+ return match.group(1)
506
+ return None
507
+
508
+ def _extract_prize(self, text: str) -> Optional[str]:
509
+ """Extrait le prix du texte"""
510
+ patterns = [
511
+ r"gagne[rz]?\s+([^.!?]{1,50})",
512
+ r"prix[:\s]+([^.!?]{1,50})",
513
+ r"lot[:\s]+([^.!?]{1,50})",
514
+ r"(\d+\s*CHF|\d+\s*euros?|\d+\s*francs?)"
515
+ ]
516
+
517
+ for pattern in patterns:
518
+ match = re.search(pattern, text, re.I)
519
+ if match:
520
+ return match.group(1).strip()
521
+ return None
522
+
523
+ def _estimate_difficulty(self, description: str) -> int:
524
+ """Estime la difficulté de participation (0-10)"""
525
+ difficulty = 0
526
+
527
+ if re.search(r'justifi|motivation|pourquoi|essay', description, re.I):
528
+ difficulty += 3
529
+ if re.search(r'photo|image|créat', description, re.I):
530
+ difficulty += 2
531
+ if re.search(r'quiz|question|répond', description, re.I):
532
+ difficulty += 1
533
+ if re.search(r'partag|social|facebook|twitter', description, re.I):
534
+ difficulty += 1
535
+ if re.search(r'inscription|compte|profil', description, re.I):
536
+ difficulty += 1
537
+
538
+ return min(difficulty, 10)
539
+
540
+ def _is_valid_contest(self, contest: Contest) -> bool:
541
+ """Valide qu'un concours est légitime"""
542
+ swiss_indicators = [
543
+ 'suisse', 'switzerland', 'ch', 'romandie', 'genève', 'lausanne',
544
+ 'zurich', 'bern', 'ouvert en suisse', 'résidents suisses'
545
+ ]
546
+
547
+ full_text = (contest.title + " " + contest.description).lower()
548
+ has_swiss_access = any(indicator in full_text for indicator in swiss_indicators)
549
+
550
+ excluded_terms = [
551
+ 'payant', 'payment', 'carte bancaire', 'spam', 'phishing',
552
+ 'adult', 'casino', 'bitcoin', 'crypto', 'investment'
553
+ ]
554
+
555
+ has_excluded = any(term in full_text for term in excluded_terms)
556
+ valid_url = contest.url.startswith(('http://', 'https://'))
557
+
558
+ return has_swiss_access and not has_excluded and valid_url
559
+
560
+ def _filter_unique_contests(self, contests: List[Contest]) -> List[Contest]:
561
+ """Filtre les concours uniques et non déjà traités"""
562
+ unique_contests = []
563
+ seen_urls = set()
564
+
565
+ for contest in contests:
566
+ if contest.url not in seen_urls and not self.db.participation_exists(contest.url):
567
+ unique_contests.append(contest)
568
+ seen_urls.add(contest.url)
569
+
570
+ return unique_contests
571
+
572
+ # =====================================================
573
+ # SYSTÈME DE PARTICIPATION INTELLIGENT
574
+ # =====================================================
575
+
576
+ class SmartParticipator:
577
+ def __init__(self, db_manager: DatabaseManager, response_engine: LocalResponseEngine):
578
+ self.db = db_manager
579
+ self.response_engine = response_engine
580
+ self.personal_info = PERSONAL_INFO
581
+
582
+ self.field_patterns = {
583
+ 'prenom': [r'prenom|prénom|first.*name'],
584
+ 'nom': [r'nom(?!.*prenom)|last.*name|family.*name'],
585
+ 'email': [r'email|e-mail|courriel'],
586
+ 'telephone': [r'tel|phone|telephone|téléphone'],
587
+ 'adresse': [r'adresse|address|rue|street'],
588
+ 'code_postal': [r'code.postal|zip|postal'],
589
+ 'ville': [r'ville|city|localité'],
590
+ 'pays': [r'pays|country|nation'],
591
+ 'motivation': [r'motivation|pourquoi|why|reason'],
592
+ 'quiz': [r'question|quiz|réponse|answer']
593
+ }
594
+
595
+ async def participate_in_contest(self, contest: Contest) -> bool:
596
+ """Participe à un concours de manière intelligente"""
597
+ async with async_playwright() as p:
598
+ browser = await p.chromium.launch(
599
+ headless=True,
600
+ args=['--no-sandbox', '--disable-blink-features=AutomationControlled']
601
+ )
602
+
603
+ context = await browser.new_context(
604
+ user_agent=random.choice(USER_AGENTS),
605
+ viewport={'width': 1920, 'height': 1080}
606
+ )
607
+
608
+ page = await context.new_page()
609
+
610
+ try:
611
+ # Analyser le formulaire
612
+ analysis = await self._analyze_form(page, contest.url)
613
+
614
+ if analysis.estimated_success_rate < 0.3:
615
+ logging.warning(f"Low success rate for {contest.url}: {analysis.estimated_success_rate}")
616
+ self.db.add_participation(contest, 'skipped_low_success', analysis.estimated_success_rate)
617
+ return False
618
+
619
+ if analysis.requires_captcha:
620
+ logging.warning(f"CAPTCHA detected for {contest.url}, skipping")
621
+ self.db.add_participation(contest, 'skipped_captcha', analysis.estimated_success_rate)
622
+ return False
623
+
624
+ # Remplir et soumettre le formulaire
625
+ success = await self._fill_and_submit_form(page, analysis, contest)
626
+
627
+ status = 'success' if success else 'failed'
628
+ self.db.add_participation(contest, status, analysis.estimated_success_rate)
629
+
630
+ logging.info(f"Participation {'successful' if success else 'failed'} for {contest.title}")
631
+ return success
632
+
633
+ except Exception as e:
634
+ logging.error(f"Participation error for {contest.url}: {e}")
635
+ self.db.add_participation(contest, 'error', 0.0)
636
+ return False
637
+
638
+ finally:
639
+ await browser.close()
640
+
641
+ async def _analyze_form(self, page: Page, url: str) -> FormAnalysis:
642
+ """Analyse un formulaire de concours"""
643
+ try:
644
+ await page.goto(url, wait_until='networkidle', timeout=15000)
645
+
646
+ # Détecter les champs
647
+ fields = await self._detect_form_fields(page)
648
+
649
+ # Calculer la complexité
650
+ complexity = sum(self._calculate_field_complexity(field) for field in fields)
651
+
652
+ # Détecter les éléments bloquants
653
+ has_captcha = await self._detect_captcha(page)
654
+ has_social_requirements = await self._detect_social_requirements(page)
655
+
656
+ # Estimer le taux de succès
657
+ success_rate = self._estimate_success_rate(fields, complexity, has_captcha)
658
+
659
+ return FormAnalysis(
660
+ fields=fields,
661
+ complexity_score=complexity,
662
+ estimated_success_rate=success_rate,
663
+ requires_captcha=has_captcha,
664
+ requires_social_media=has_social_requirements,
665
+ form_url=url
666
+ )
667
+
668
+ except Exception as e:
669
+ logging.error(f"Form analysis error: {e}")
670
+ return FormAnalysis([], 10, 0.0, True, True, url)
671
+
672
+ async def _detect_form_fields(self, page: Page) -> List[FormField]:
673
+ """Détecte tous les champs de formulaire"""
674
+ fields = []
675
+
676
+ selectors = [
677
+ 'input[type="text"]', 'input[type="email"]', 'input[type="tel"]',
678
+ 'input[type="number"]', 'input:not([type])', 'textarea', 'select'
679
+ ]
680
+
681
+ for selector in selectors:
682
+ elements = await page.query_selector_all(selector)
683
+
684
+ for element in elements:
685
+ try:
686
+ field = await self._analyze_single_field(element, page)
687
+ if field:
688
+ fields.append(field)
689
+ except Exception:
690
+ continue
691
+
692
+ return fields
693
+
694
+ async def _analyze_single_field(self, element, page: Page) -> Optional[FormField]:
695
+ """Analyse un champ individuel"""
696
+ try:
697
+ field_type = await element.evaluate('el => el.type || el.tagName.toLowerCase()')
698
+ name = await element.evaluate('el => el.name || el.id || ""')
699
+ placeholder = await element.evaluate('el => el.placeholder || ""')
700
+ required = await element.evaluate('el => el.required')
701
+
702
+ # Trouver le label
703
+ label_text = await self._find_field_label(element, page)
704
+
705
+ # Créer un sélecteur unique
706
+ selector = await self._create_unique_selector(element)
707
+
708
+ return FormField(
709
+ selector=selector,
710
+ field_type=field_type,
711
+ label=label_text,
712
+ required=required,
713
+ ai_context=f"Name: {name}, Placeholder: {placeholder}, Label: {label_text}"
714
+ )
715
+
716
+ except Exception:
717
+ return None
718
+
719
+ async def _find_field_label(self, element, page: Page) -> str:
720
+ """Trouve le label associé à un champ"""
721
+ try:
722
+ # Méthode 1: label[for]
723
+ element_id = await element.evaluate('el => el.id')
724
+ if element_id:
725
+ label = await page.query_selector(f'label[for="{element_id}"]')
726
+ if label:
727
+ return await label.inner_text()
728
+
729
+ # Méthode 2: parent label
730
+ parent_label = await element.evaluate('''
731
+ el => {
732
+ let parent = el.parentElement;
733
+ while (parent && parent.tagName !== 'BODY') {
734
+ if (parent.tagName === 'LABEL') {
735
+ return parent.innerText;
736
+ }
737
+ parent = parent.parentElement;
738
+ }
739
+ return '';
740
+ }
741
+ ''')
742
+
743
+ if parent_label:
744
+ return parent_label.strip()
745
+
746
+ # Méthode 3: texte précédent
747
+ prev_text = await element.evaluate('''
748
+ el => {
749
+ const prev = el.previousElementSibling;
750
+ return prev ? prev.innerText : '';
751
+ }
752
+ ''')
753
+
754
+ return prev_text.strip()
755
+
756
+ except Exception:
757
+ return ""
758
+
759
+ async def _create_unique_selector(self, element) -> str:
760
+ """Crée un sélecteur CSS unique"""
761
+ # Priorité: ID > Name > Class > Position
762
+ element_id = await element.evaluate('el => el.id')
763
+ if element_id:
764
+ return f'#{element_id}'
765
+
766
+ name = await element.evaluate('el => el.name')
767
+ if name:
768
+ return f'[name="{name}"]'
769
+
770
+ class_name = await element.evaluate('el => el.className')
771
+ tag_name = await element.evaluate('el => el.tagName.toLowerCase()')
772
+
773
+ if class_name:
774
+ return f'{tag_name}.{class_name.split()[0]}'
775
+
776
+ # Fallback
777
+ return f'{tag_name}:nth-of-type(1)'
778
+
779
+ def _calculate_field_complexity(self, field: FormField) -> int:
780
+ """Calcule la complexité d'un champ"""
781
+ complexity = 1
782
+
783
+ if field.field_type == 'textarea':
784
+ complexity += 3
785
+ elif field.field_type == 'select':
786
+ complexity += 2
787
+ elif field.required:
788
+ complexity += 1
789
+
790
+ if re.search(r'motivation|justifi|pourquoi', field.label, re.I):
791
+ complexity += 3
792
+ elif re.search(r'quiz|question', field.label, re.I):
793
+ complexity += 2
794
+
795
+ return complexity
796
+
797
+ async def _detect_captcha(self, page: Page) -> bool:
798
+ """Détecte la présence de CAPTCHA"""
799
+ captcha_selectors = [
800
+ '.g-recaptcha', '.h-captcha', '#captcha', '.captcha',
801
+ 'iframe[src*="recaptcha"]', 'iframe[src*="hcaptcha"]'
802
+ ]
803
+
804
+ for selector in captcha_selectors:
805
+ element = await page.query_selector(selector)
806
+ if element:
807
+ return True
808
+
809
+ return False
810
+
811
+ async def _detect_social_requirements(self, page: Page) -> bool:
812
+ """Détecte les exigences de réseaux sociaux"""
813
+ content = await page.content()
814
+ social_patterns = [
815
+ r'follow.*us', r'partag.*facebook', r'retweet',
816
+ r'like.*page', r'subscribe.*channel'
817
+ ]
818
+
819
+ for pattern in social_patterns:
820
+ if re.search(pattern, content, re.I):
821
+ return True
822
+
823
+ return False
824
+
825
+ def _estimate_success_rate(self, fields: List[FormField], complexity: int, has_captcha: bool) -> float:
826
+ """Estime le taux de succès"""
827
+ base_rate = 0.8
828
+
829
+ if has_captcha:
830
+ base_rate *= 0.1
831
+
832
+ if complexity > 15:
833
+ base_rate *= 0.4
834
+ elif complexity > 10:
835
+ base_rate *= 0.6
836
+ elif complexity > 5:
837
+ base_rate *= 0.8
838
+
839
+ required_fields = [f for f in fields if f.required]
840
+ if len(required_fields) <= 3:
841
+ base_rate *= 1.1
842
+
843
+ return min(base_rate, 1.0)
844
+
845
+ async def _fill_and_submit_form(self, page: Page, analysis: FormAnalysis, contest: Contest) -> bool:
846
+ """Remplit et soumet le formulaire"""
847
+ try:
848
+ filled_fields = 0
849
+
850
+ for field in analysis.fields:
851
+ try:
852
+ # Attendre l'élément
853
+ await page.wait_for_selector(field.selector, timeout=3000)
854
+ element = await page.query_selector(field.selector)
855
+
856
+ if not element:
857
+ continue
858
+
859
+ # Générer la valeur
860
+ value = await self._generate_field_value(field, contest, page)
861
+ if not value:
862
+ continue
863
+
864
+ # Remplir selon le type
865
+ if field.field_type == 'select':
866
+ await self._fill_select_field(element, value, page)
867
+ else:
868
+ await element.fill(value)
869
+
870
+ filled_fields += 1
871
+ await asyncio.sleep(random.uniform(0.3, 0.8))
872
+
873
+ except Exception as e:
874
+ logging.debug(f"Error filling field {field.selector}: {e}")
875
+ continue
876
+
877
+ # Soumettre le formulaire
878
+ submit_success = await self._submit_form(page)
879
+
880
+ logging.info(f"Filled {filled_fields}/{len(analysis.fields)} fields, submitted: {submit_success}")
881
+ return filled_fields > 0 and submit_success
882
+
883
+ except Exception as e:
884
+ logging.error(f"Form filling error: {e}")
885
+ return False
886
+
887
+ async def _generate_field_value(self, field: FormField, contest: Contest, page: Page) -> Optional[str]:
888
+ """Génère une valeur pour un champ"""
889
+ # Identifier le type de champ
890
+ field_type = self._identify_field_type(field)
891
+
892
+ # Mapping des valeurs personnelles
893
+ personal_mapping = {
894
+ 'prenom': self.personal_info.prenom,
895
+ 'nom': self.personal_info.nom,
896
+ 'email': self.personal_info.email_derivee,
897
+ 'telephone': self.personal_info.telephone,
898
+ 'adresse': self.personal_info.adresse,
899
+ 'code_postal': self.personal_info.code_postal,
900
+ 'ville': self.personal_info.ville,
901
+ 'pays': self.personal_info.pays
902
+ }
903
+
904
+ if field_type in personal_mapping:
905
+ return personal_mapping[field_type]
906
+
907
+ # Cas spéciaux nécessitant le moteur de réponses
908
+ if field_type == 'motivation':
909
+ return self.response_engine.generate_response(
910
+ field.label,
911
+ contest.description,
912
+ "motivation"
913
+ )
914
+ elif field_type == 'quiz':
915
+ return self.response_engine.generate_response(
916
+ field.label,
917
+ contest.description,
918
+ "quiz"
919
+ )
920
+
921
+ # Valeurs par défaut
922
+ default_values = {
923
+ 'age': '25',
924
+ 'genre': 'Monsieur',
925
+ 'profession': 'Étudiant'
926
+ }
927
+
928
+ for key, value in default_values.items():
929
+ if key in field.label.lower():
930
+ return value
931
+
932
+ return None
933
+
934
+ def _identify_field_type(self, field: FormField) -> str:
935
+ """Identifie le type de champ"""
936
+ combined_text = f"{field.ai_context} {field.label}".lower()
937
+
938
+ for field_type, patterns in self.field_patterns.items():
939
+ for pattern in patterns:
940
+ if re.search(pattern, combined_text, re.I):
941
+ return field_type
942
+
943
+ return 'unknown'
944
+
945
+ async def _fill_select_field(self, element, value: str, page: Page):
946
+ """Remplit un champ select"""
947
+ try:
948
+ options = await element.query_selector_all('option')
949
+
950
+ for option in options:
951
+ option_text = await option.inner_text()
952
+ option_value = await option.get_attribute('value')
953
+
954
+ if (value.lower() in option_text.lower() or
955
+ value.lower() in (option_value or "").lower()):
956
+ await element.select_option(value=option_value)
957
+ return
958
+
959
+ # Fallback: sélectionner la première option valide
960
+ if options and len(options) > 1:
961
+ first_option = await options[1].get_attribute('value')
962
+ await element.select_option(value=first_option)
963
+
964
+ except Exception as e:
965
+ logging.debug(f"Select field error: {e}")
966
+
967
+ async def _submit_form(self, page: Page) -> bool:
968
+ """Soumet le formulaire"""
969
+ submit_selectors = [
970
+ 'input[type="submit"]',
971
+ 'button[type="submit"]',
972
+ 'button:has-text("Participer")',
973
+ 'button:has-text("Envoyer")',
974
+ 'button:has-text("Valider")',
975
+ '.submit-btn',
976
+ '.participate-btn'
977
+ ]
978
+
979
+ for selector in submit_selectors:
980
+ try:
981
+ element = await page.query_selector(selector)
982
+ if element:
983
+ is_visible = await element.is_visible()
984
+ is_enabled = await element.is_enabled()
985
+
986
+ if is_visible and is_enabled:
987
+ await element.click()
988
+ await page.wait_for_timeout(3000)
989
+ return True
990
+
991
+ except Exception:
992
+ continue
993
+
994
+ return False
995
+
996
+ # =====================================================
997
+ # ORCHESTRATEUR PRINCIPAL
998
+ # =====================================================
999
+
1000
+ class ContestBotOrchestrator:
1001
+ def __init__(self):
1002
+ self.db = DatabaseManager()
1003
+ self.response_engine = LocalResponseEngine()
1004
+ self.scraper = None # Initialisé dans le contexte async
1005
+ self.participator = SmartParticipator(self.db, self.response_engine)
1006
+
1007
+ async def run_full_cycle(self):
1008
+ """Execute un cycle complet de scraping et participation"""
1009
+ logging.info("Starting full contest bot cycle (sans API)")
1010
+
1011
+ try:
1012
+ # 1. Scraping des concours
1013
+ async with LocalScraper(self.db) as scraper:
1014
+ self.scraper = scraper
1015
+ contests = await scraper.scrape_all_sources()
1016
+
1017
+ if not contests:
1018
+ logging.info("No new contests found")
1019
+ return
1020
+
1021
+ # 2. Trier par score de difficulté (plus faciles en premier)
1022
+ contests.sort(key=lambda x: x.difficulty_score)
1023
+
1024
+ # 3. Participer aux concours (limiter à 15 par jour)
1025
+ participation_count = 0
1026
+ max_daily_participations = 15
1027
+
1028
+ for contest in contests[:max_daily_participations]:
1029
+ try:
1030
+ # Pause entre participations pour éviter la détection
1031
+ if participation_count > 0:
1032
+ wait_time = random.uniform(30, 120) # 30s à 2min
1033
+ logging.info(f"Waiting {wait_time:.0f}s before next participation")
1034
+ await asyncio.sleep(wait_time)
1035
+
1036
+ success = await self.participator.participate_in_contest(contest)
1037
+ participation_count += 1
1038
+
1039
+ if success:
1040
+ logging.info(f"✅ Successfully participated in: {contest.title}")
1041
+ else:
1042
+ logging.warning(f"❌ Failed to participate in: {contest.title}")
1043
+
1044
+ # Pause plus longue après succès
1045
+ if success:
1046
+ await asyncio.sleep(random.uniform(60, 180))
1047
+
1048
+ except Exception as e:
1049
+ logging.error(f"Error participating in {contest.title}: {e}")
1050
+ continue
1051
+
1052
+ logging.info(f"Participation cycle completed: {participation_count} attempts")
1053
+
1054
+ except Exception as e:
1055
+ logging.error(f"Full cycle error: {e}")
1056
+
1057
+ def generate_report(self) -> str:
1058
+ """Génère un rapport simple"""
1059
+ stats = self.db.get_stats()
1060
+
1061
+ today = datetime.now().strftime('%Y-%m-%d')
1062
+ conn = self.db._get_connection()
1063
+
1064
+ today_participations = conn.execute(
1065
+ "SELECT COUNT(*) FROM participations WHERE date = ?", (today,)
1066
+ ).fetchone()[0]
1067
+
1068
+ today_successes = conn.execute(
1069
+ "SELECT COUNT(*) FROM participations WHERE date = ? AND status = 'success'", (today,)
1070
+ ).fetchone()[0]
1071
+
1072
+ success_rate = (today_successes / max(today_participations, 1)) * 100
1073
+
1074
+ report = f"""
1075
+ 📊 RAPPORT QUOTIDIEN - {today}
1076
+
1077
+ 🎯 Aujourd'hui:
1078
+ • Participations: {today_participations}
1079
+ • Succès: {today_successes}
1080
+ • Taux de succès: {success_rate:.1f}%
1081
+
1082
+ 📈 Total:
1083
+ • Participations totales: {stats['total_participations']}
1084
+ • Participations réussies: {stats['successful_participations']}
1085
+
1086
+ 🌐 Par source:
1087
+ """
1088
+
1089
+ for source, count in stats['by_source'].items():
1090
+ report += f" • {source}: {count}\n"
1091
+
1092
+ return report
1093
+
1094
+ # =====================================================
1095
+ # SCHEDULER ET POINT D'ENTRÉE
1096
+ # =====================================================
1097
+
1098
+ def run_bot_cycle():
1099
+ """Point d'entrée pour le scheduler"""
1100
+ bot = ContestBotOrchestrator()
1101
+
1102
+ # Cycle principal
1103
+ asyncio.run(bot.run_full_cycle())
1104
+
1105
+ # Afficher le rapport
1106
+ report = bot.generate_report()
1107
+ print(report)
1108
+
1109
+ def main():
1110
+ """Fonction principale avec scheduler"""
1111
+ print("🎰 BOT DE CONCOURS SUISSE - VERSION SANS API")
1112
+ print("=" * 50)
1113
+ print("✅ Système de réponses locales activé")
1114
+ print("✅ Scraping direct des sites web")
1115
+ print("✅ Aucune API externe requise")
1116
+ print()
1117
+
1118
+ # Vérifier les arguments de ligne de commande
1119
+ if len(sys.argv) > 1 and sys.argv[1] == "--run-now":
1120
+ logging.info("Running immediate cycle")
1121
+ run_bot_cycle()
1122
+ return
1123
+
1124
+ # Programmer les tâches
1125
+ logging.info("Starting Contest Bot with scheduler")
1126
+ schedule.every().day.at("08:00").do(run_bot_cycle)
1127
+ schedule.every().day.at("14:00").do(run_bot_cycle) # Deux fois par jour
1128
+
1129
+ # Exécution immédiate pour test
1130
+ print("🚀 Lancement immédiat pour test...")
1131
+ run_bot_cycle()
1132
+
1133
+ # Boucle principale du scheduler
1134
+ logging.info("Scheduler started. Waiting for scheduled tasks...")
1135
+
1136
+ while True:
1137
+ try:
1138
+ schedule.run_pending()
1139
+ time.sleep(60) # Vérifier chaque minute
1140
+ except KeyboardInterrupt:
1141
+ logging.info("Bot stopped by user")
1142
+ break
1143
+ except Exception as e:
1144
+ logging.error(f"Scheduler error: {e}")
1145
+ time.sleep(300) # Attendre 5 minutes en cas d'erreur
1146
+
1147
+ if __name__ == "__main__":
1148
+ main()