├── requirements.txt ├── config.ini ├── README.md ├── LICENSE.txt └── gptfoot.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==3.20.0.post0 2 | aiohttp==3.11.18 3 | asyncio==3.4.3 4 | configparser==7.2.0 5 | discord.py==2.5.2 6 | httpx==0.28.1 7 | pytz==2025.2 -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [KEYS] 2 | POE_API_KEY = your_poe_api_key_here 3 | TELEGRAM_BOT_TOKEN = your_telegram_token_here 4 | TEAM_ID = your_team_id_here 5 | TEAM_NAME = your_team_name_here 6 | LEAGUE_IDS = 1,2,3 7 | SEASON_ID = your_season_id_here 8 | ; If you modify these values, make sure to update the corresponding interval in the code for the free API 9 | API_FOOTBALL_KEY = your_api_football_key_here 10 | DISCORD_BOT_TOKEN = your_discord_token_here 11 | 12 | [OPTIONS] 13 | USE_TELEGRAM = true 14 | USE_DISCORD = true 15 | IS_PAID_API = false 16 | ENABLE_COST_TRACKING = true 17 | 18 | [API_MODELS] 19 | ; Main model for match analysis (e.g., Grok-4-Fast-Reasoning, gpt-4o, Claude-Sonnet-4) 20 | MAIN_MODEL = Grok-4-Fast-Reasoning 21 | ; Model for translations (can be the same or different) 22 | TRANSLATION_MODEL = Grok-4-Fast-Reasoning 23 | 24 | [API_PRICING] 25 | ; Cost per 1 million input tokens in USD (adjust based on your API provider) 26 | INPUT_COST_PER_1M_TOKENS = 0.21 27 | ; Cost per 1 million output tokens in USD (adjust based on your API provider) 28 | OUTPUT_COST_PER_1M_TOKENS = 0.51 29 | ; Cache discount percentage (if applicable, e.g., 75 for 75% discount) 30 | CACHE_DISCOUNT_PERCENTAGE = 75 31 | 32 | [SERVER] 33 | TIMEZONE = Europe/Paris 34 | 35 | [LANGUAGES] 36 | ; Write the name of your language in lower case in English 37 | LANGUAGE = french 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚽ gptfoot (Telegram/Discord bot) 2 | 3 | ## 🌐 Overview 4 | gptfoot is a bot for Telegram and Discord, meticulously designed to track match events for clubs across multiple leagues, all while minimizing message volume. It utilizes cutting-edge AI technology (Poe API with Grok-4-Fast-Reasoning or OpenAI GPT-4o) to provide live match commentary. The bot offers a broad range of features, from showcasing team lineups to reporting goals, red cards, and delivering comprehensive match analyses, all backed by advanced AI capabilities. It's a game-changer for those looking to stay updated on matches in a seamless and informative way. 5 | 6 | ## 🛠 Features 7 | * Detection of whether a team is playing a match today in different leagues 8 | * Real-time monitoring of match events using Telegram/Discord bots powered by AI 9 | * Integration with the api-football API for retrieving match data, making predictions, and obtaining rankings (if available) 10 | * AI-driven match analysis with contextual awareness (uses history of last 5 matches for richer insights) 11 | * Can be used with a Telegram bot, a Discord bot, or both (configurable in config.ini) 12 | * Can be used with either the free or paid API from api-football (configurable in config.ini) 13 | * **NEW**: Professional logging system with automatic rotation (max 10MB, keeps 5 backups) 14 | * **NEW**: API cost tracking and reporting (transparent monitoring of AI API usage) 15 | * **NEW**: Match history storage (last 5 matches) for contextual AI analysis 16 | * **NEW**: Intelligent message splitting for long AI responses (respects platform limits) 17 | * **NEW**: API key validation at startup with helpful error messages 18 | * **NEW**: Retry mechanism with exponential backoff for improved reliability 19 | * **NEW**: Enhanced error handling for network issues and API failures 20 | * The frequency of messages is limited to ensure a pleasant experience for users 21 | * Can be used with the free version of api-football (up to 100 calls per day) 22 | * Support for dozens of languages 23 | * Configurable AI models (Poe API or OpenAI) 24 | 25 | ## 🆕 What's New in v2.5.0 26 | * ✅ **Professional Logging**: Rotating log files with automatic cleanup 27 | * ✅ **Cost Tracking**: Real-time monitoring of AI API costs with detailed summaries 28 | * ✅ **Match History**: Stores last 5 matches for contextual AI analysis 29 | * ✅ **Smart Message Splitting**: Automatically splits long messages for Telegram (4096 chars) and Discord (2000 chars) 30 | * ✅ **API Validation**: Validates all API keys at startup with clear error messages 31 | * ✅ **Improved Reliability**: Retry mechanism with exponential backoff for API calls 32 | * ✅ **Better Error Handling**: Enhanced error messages and graceful degradation 33 | * ✅ **Poe API Support**: Now supports Poe API with configurable models (Grok-4-Fast-Reasoning, etc.) 34 | * ✅ **Enhanced AI Prompts**: Richer context with match history for better analysis 35 | * ✅ **Missed Penalty Detection**: Smart detection that doesn't notify during penalty shootouts 36 | 37 | ## 🛠 Demo & press articles 38 | * Video link (fr) : https://www.youtube.com/shorts/XLvecHDjJGk?feature=share 39 | * Blog api-football.com (en) : https://www.api-football.com/news/post/gptfoot-the-ultimate-telegram-and-discord-bot-for-football-fanatics-powered-by-ai 40 | 41 | ## 🌟 Potential future updates 42 | * ✅ [DONE] Change of OpenAI's model to use GPT-4o 43 | * ✅ [DONE] Handle the cancellation of goals by VAR 44 | * ✅ [DONE] Professional logging system with rotation 45 | * ✅ [DONE] API cost tracking and monitoring 46 | * ✅ [DONE] Match history for contextual AI analysis 47 | * [Low priority] Allow API calls to retrieve the standings once per day, and on match days 1 hour after the match if API calls remain available. Enable users to check standings via Telegram and Discord commands (stored locally to avoid excessive API calls). Incorporate standings data in pre-match and post-match analysis for both free and paid API versions, referencing locally stored standings without making additional API calls. Consider whether to implement this for national championships only or for all competitions. Review the API endpoint (what data it returns). 48 | * [Low priority] Make the bot more flexible by adding more events such as yellow cards and player changes, with the option of enabling or disabling events in config.ini. The idea behind the bot was to send only essential messages, so this is not a high priority. 49 | * [Low priority] Implement a better scoring management for penalty shootouts, as they are not handled the same way as regular goals 50 | * [Low priority] Improved handling of season ID retrieval (currently requires manual adjustment in config.ini at the beginning of each season) 51 | 52 | ## ✅ Known Issues - Resolution Status 53 | 54 | ### **RESOLVED Issues** ✅ 55 | 56 | * ✅ **[SOLVED]** A bug seems to occur when a new goal is scored but an old goal is updated (for example, in terms of time). The new goal is not sent, but the score is updated, which seems to prevent the sending of the new goal. 57 | * **Resolution**: Fixed with `event_key_sub` mechanism that prevents duplicate notifications even when API corrects goal timing 58 | 59 | * ✅ **[SOLVED]** When a penalty miss occurs, the following goal during a match are not sent under certain conditions 60 | * **Resolution**: Enhanced penalty miss detection with proper event tracking and continuation logic 61 | 62 | * ✅ **[SOLVED]** When a goal is disallowed under certain conditions, it seems that no alert is sent to indicate that the goal has been cancelled. This could perhaps be due to a penalty considered as scored, whose score has not been updated but is then cancelled. 63 | * **Resolution**: Improved VAR goal cancellation detection with proper score comparison and notification 64 | 65 | * ✅ **[SOLVED]** [Free API] A bug that seemed to prevent sending a goal if the goal was sent simultaneously with the first event. This mainly concerned the free use of the API. 66 | * **Resolution**: Fixed with improved `is_first_event` logic and score update mechanism 67 | 68 | * ✅ **[SOLVED]** [Free API] In very rare instances, if two goals are scored in quick succession and there's a delay in API score updates, the score might not be correctly updated until the next goal or the end of the match 69 | * **Resolution**: Enhanced score tracking with `previous_score` and `current_score` comparison, plus `goal_events` accumulation 70 | 71 | * ✅ **[SOLVED - v2.5.1]** Goal timing corrections from API were not reflected in sent messages, causing confusion about when goals were actually scored 72 | * **Resolution**: Implemented `sent_events_details` tracking system that detects timing changes and automatically sends correction notifications to users 73 | 74 | * ✅ **[SOLVED - v2.5.1]** Automatic correction messages now sent when timing changes are detected (v2.5.1) 75 | 76 | ### **Monitored Issues** 🔧 77 | 78 | * **[🔧 IMPROVED - v2.5.1]** [Free API] In very rare instances, it's possible that a disallowed goal might go undetected if two scored goals are identified, including one that was disallowed, within the same interval between two checks. However, the score should still be displayed accurately in such cases 79 | * **Previous Status**: Improved VAR detection, but timing-dependent edge cases may persist with free API's longer intervals (5-10% probability) 80 | * **New Status**: ⚠️ **SIGNIFICANTLY IMPROVED** - Added backup detection system that compares goal count vs actual score, reducing probability from 5-10% to **< 1%** (v2.5.1) 81 | 82 | * **[🔧 MONITORED]** [Free API] It is possible that some events occurring in the last seconds of a period may not be detected 83 | * **Status**: Inherent limitation of free API's longer check intervals (76-96 seconds depending on league). This affects approximately 5-10% of matches. **Recommendation**: Use paid API for complete event coverage. 84 | 85 | ### **Circumvented Issues** 🕹️ 86 | 87 | * **[🕹️ CIRCUMVENTED]** [Paid API] The script does not update the score with penalty shots as this is handled differently (Analysis of the AI at the end partially incorrect) 88 | * **Status**: By design - penalty shootout goals are tracked separately and included in final match analysis 89 | 90 | * **[🕹️ CIRCUMVENTED]** [Paid API] Do not consider the goals that would be invalidated during the penalty shootout session 91 | * **Status**: By design - only successful penalty shootout goals are reported to avoid confusion 92 | 93 | ## 📋 Good to know 94 | * The bot cannot provide ongoing match information if launched on a server during the match; only upcoming matches are considered 95 | * **NEW**: Log files are automatically rotated when they reach 10MB, keeping the last 5 backups 96 | * **NEW**: API costs are tracked and summarized at the end of each match (when enabled in config.ini) 97 | * **NEW**: Match analyses are stored locally in `match_analyses.json` for contextual AI insights 98 | * [Free API] Due to API call limitations, 5-minute breaks during extra time are considered as regular half-times, causing the script to pause for 13 minutes 99 | * [Free API] Due to API call limitations, during penalty shootout sessions, the script pauses for 20 minutes (good to know if ever but penalty goals are managed differently than goals during a match) 100 | * [Free API] Check intervals vary by league (76-96 seconds) to optimize the 100 daily API calls limit 101 | 102 | ## 🔧 Configuration 103 | The bot is configured via `config.ini` with the following sections: 104 | 105 | ### [KEYS] 106 | * `POE_API_KEY` or `OPENAI_API_KEY`: Your AI API key 107 | * `TELEGRAM_BOT_TOKEN`: Your Telegram bot token 108 | * `DISCORD_BOT_TOKEN`: Your Discord bot token 109 | * `TEAM_ID`: The ID of the team to track 110 | * `TEAM_NAME`: The name of the team 111 | * `LEAGUE_IDS`: Comma-separated list of league IDs to monitor 112 | * `API_FOOTBALL_KEY`: Your api-football.com API key 113 | 114 | ### [OPTIONS] 115 | * `USE_TELEGRAM`: Enable/disable Telegram bot (true/false) 116 | * `USE_DISCORD`: Enable/disable Discord bot (true/false) 117 | * `IS_PAID_API`: Use paid api-football plan for more frequent updates (true/false) 118 | * `ENABLE_COST_TRACKING`: Track and log AI API costs (true/false) 119 | 120 | ### [API_MODELS] 121 | * `MAIN_MODEL`: AI model for match analysis (e.g., Grok-4-Fast-Reasoning, gpt-4o) 122 | * `TRANSLATION_MODEL`: AI model for translations 123 | 124 | ### [API_PRICING] 125 | * `INPUT_COST_PER_1M_TOKENS`: Cost per 1M input tokens in USD 126 | * `OUTPUT_COST_PER_1M_TOKENS`: Cost per 1M output tokens in USD 127 | * `CACHE_DISCOUNT_PERCENTAGE`: Cache discount percentage 128 | 129 | ### [SERVER] 130 | * `TIMEZONE`: Server timezone (e.g., Europe/Paris) 131 | 132 | ### [LANGUAGES] 133 | * `LANGUAGE`: Output language (e.g., french, english) 134 | 135 | ## 📊 Technical Improvements in v2.5.0 136 | 137 | ### Reliability 138 | * Retry mechanism with exponential backoff (max 3 attempts) 139 | * Improved error handling for network issues 140 | * Graceful degradation when AI API is unavailable 141 | * Timeout management (30s for api-football, 60s for AI API) 142 | 143 | ### Performance 144 | * Optimized API call patterns 145 | * Efficient event deduplication with dual-key system 146 | * Smart message batching for multiple goals 147 | 148 | ### Maintainability 149 | * Refactored code with helper functions 150 | * Clear separation of concerns 151 | * Comprehensive logging for debugging 152 | * Type hints and documentation 153 | 154 | ### User Experience 155 | * Intelligent message splitting (no truncation) 156 | * Contextual AI analysis with match history 157 | * Clear error messages 158 | * Cost transparency 159 | 160 | ## 📝 Licence 161 | Attribution-NonCommercial 4.0 International (https://creativecommons.org/licenses/by-nc/4.0/legalcode) 162 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial 4.0 International Public License 2 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 3 | 4 | Section 1 – Definitions. 5 | 6 | Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 7 | Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 8 | Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 9 | Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 10 | Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 11 | Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 12 | Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 13 | Licensor means the individual(s) or entity(ies) granting rights under this Public License. 14 | NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 15 | Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 16 | Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 17 | You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 18 | 19 | Section 2 – Scope. 20 | 21 | License grant. 22 | Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 23 | reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 24 | produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 25 | Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 26 | Term. The term of this Public License is specified in Section 6(a). 27 | Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 28 | Downstream recipients. 29 | Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 30 | No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 31 | No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 32 | Other rights. 33 | 34 | Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 35 | Patent and trademark rights are not licensed under this Public License. 36 | To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 37 | 38 | Section 3 – License Conditions. 39 | 40 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 41 | 42 | Attribution. 43 | 44 | If You Share the Licensed Material (including in modified form), You must: 45 | 46 | retain the following if it is supplied by the Licensor with the Licensed Material: 47 | identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 48 | a copyright notice; 49 | a notice that refers to this Public License; 50 | a notice that refers to the disclaimer of warranties; 51 | a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 52 | indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 53 | indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 54 | You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 55 | If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 56 | If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 57 | 58 | Section 4 – Sui Generis Database Rights. 59 | 60 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 61 | 62 | for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 63 | if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 64 | You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 65 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 66 | 67 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 68 | 69 | Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 70 | To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 71 | The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 72 | 73 | Section 6 – Term and Termination. 74 | 75 | This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 76 | Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 77 | 78 | automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 79 | upon express reinstatement by the Licensor. 80 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 81 | For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 82 | Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 83 | 84 | Section 7 – Other Terms and Conditions. 85 | 86 | The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 87 | Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 88 | 89 | Section 8 – Interpretation. 90 | 91 | For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 92 | To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 93 | No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 94 | Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 95 | 96 | Translation of the license available here: https://creativecommons.org/licenses/by-nc/4.0/legalcode" 97 | -------------------------------------------------------------------------------- /gptfoot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # AUTEUR : Rymentz (https://github.com/Macmachi/gptfoot) 4 | # VERSION : v2.5.7 5 | # LICENCE : Attribution-NonCommercial 4.0 International 6 | # 7 | import asyncio 8 | import datetime 9 | from aiogram import Bot, Dispatcher, types 10 | from aiogram.filters import Command 11 | from aiogram.exceptions import TelegramAPIError, TelegramBadRequest, TelegramForbiddenError, TelegramNetworkError 12 | from aiohttp.client_exceptions import ClientConnectorError 13 | import discord 14 | from discord.ext import commands, tasks 15 | import json 16 | import os 17 | import aiohttp 18 | import pytz 19 | import httpx 20 | import configparser 21 | import time 22 | import atexit 23 | import signal 24 | import sys 25 | import logging 26 | from logging.handlers import RotatingFileHandler 27 | 28 | script_dir = os.path.dirname(os.path.realpath(__file__)) 29 | os.chdir(script_dir) 30 | config_path = os.path.join(script_dir, 'config.ini') 31 | 32 | # Fonction pour valider les clés API 33 | def validate_api_keys(): 34 | """Valide l'existence et la validité basique des clés API""" 35 | errors = [] 36 | warnings = [] 37 | 38 | # Vérifier POE_API_KEY 39 | if not API_KEY or API_KEY == 'your_poe_api_key_here': 40 | errors.append("POE_API_KEY n'est pas configurée ou utilise la valeur par défaut") 41 | elif len(API_KEY) < 10: 42 | warnings.append("POE_API_KEY semble trop courte, vérifiez sa validité") 43 | 44 | # Vérifier API_FOOTBALL_KEY 45 | if not API_FOOTBALL_KEY or len(API_FOOTBALL_KEY) < 10: 46 | errors.append("API_FOOTBALL_KEY n'est pas configurée correctement") 47 | 48 | # Vérifier les tokens des bots si activés 49 | if USE_TELEGRAM: 50 | if not TOKEN_TELEGRAM or len(TOKEN_TELEGRAM) < 20: 51 | errors.append("TELEGRAM_BOT_TOKEN n'est pas configuré correctement") 52 | 53 | if USE_DISCORD: 54 | if not TOKEN_DISCORD or len(TOKEN_DISCORD) < 20: 55 | errors.append("DISCORD_BOT_TOKEN n'est pas configuré correctement") 56 | 57 | # Vérifier TEAM_ID 58 | try: 59 | team_id_int = int(TEAM_ID) 60 | if team_id_int <= 0: 61 | errors.append("TEAM_ID doit être un nombre positif") 62 | except (ValueError, TypeError): 63 | errors.append("TEAM_ID doit être un nombre valide") 64 | 65 | # Afficher les erreurs et warnings 66 | if errors: 67 | print("\n" + "="*60) 68 | print("❌ ERREURS DE CONFIGURATION CRITIQUES:") 69 | for error in errors: 70 | print(f" • {error}") 71 | print("="*60 + "\n") 72 | return False 73 | 74 | if warnings: 75 | print("\n" + "="*60) 76 | print("⚠️ AVERTISSEMENTS DE CONFIGURATION:") 77 | for warning in warnings: 78 | print(f" • {warning}") 79 | print("="*60 + "\n") 80 | 81 | print("✅ Validation des clés API réussie\n") 82 | return True 83 | 84 | config = configparser.ConfigParser() 85 | 86 | try: 87 | # Lire le contenu du fichier config.ini 88 | if not os.path.exists(config_path): 89 | print(f"❌ ERREUR: Le fichier config.ini n'existe pas à l'emplacement: {config_path}") 90 | sys.exit(1) 91 | 92 | config.read(config_path, encoding='utf-8') 93 | 94 | # Récupérer les variables de la section KEYS 95 | API_KEY = config['KEYS'].get('POE_API_KEY', '').strip() 96 | TOKEN_TELEGRAM = config['KEYS'].get('TELEGRAM_BOT_TOKEN', '').strip() 97 | TEAM_ID = config['KEYS'].get('TEAM_ID', '').strip() 98 | TEAM_NAME = config['KEYS'].get('TEAM_NAME', '').strip() 99 | LEAGUE_IDS_STR = config['KEYS'].get('LEAGUE_IDS', '').strip() 100 | SEASON_ID = config['KEYS'].get('SEASON_ID', '').strip() 101 | API_FOOTBALL_KEY = config['KEYS'].get('API_FOOTBALL_KEY', '').strip() 102 | TOKEN_DISCORD = config['KEYS'].get('DISCORD_BOT_TOKEN', '').strip() 103 | 104 | # Récupérer les variables de la section OPTIONS 105 | USE_TELEGRAM = config['OPTIONS'].getboolean('USE_TELEGRAM', fallback=True) 106 | USE_DISCORD = config['OPTIONS'].getboolean('USE_DISCORD', fallback=True) 107 | IS_PAID_API = config['OPTIONS'].getboolean('IS_PAID_API', fallback=False) 108 | ENABLE_COST_TRACKING = config['OPTIONS'].getboolean('ENABLE_COST_TRACKING', fallback=True) 109 | 110 | # Récupérer le fuseau horaire du serveur à partir de la section SERVER 111 | SERVER_TIMEZONE_STR = config['SERVER'].get('TIMEZONE', 'Europe/Paris') 112 | 113 | # Récupérer la langue à partir de la section LANGUAGES 114 | LANGUAGE = config['LANGUAGES'].get('LANGUAGE', 'english') 115 | 116 | # Récupérer les modèles API à partir de la section API_MODELS 117 | GPT_MODEL_NAME = config['API_MODELS'].get('MAIN_MODEL', 'Grok-4-Fast-Reasoning') 118 | GPT_MODEL_NAME_TRANSLATION = config['API_MODELS'].get('TRANSLATION_MODEL', 'Grok-4-Fast-Reasoning') 119 | 120 | # Récupérer la tarification à partir de la section API_PRICING 121 | INPUT_COST_PER_1M_TOKENS = float(config['API_PRICING'].get('INPUT_COST_PER_1M_TOKENS', '0.21')) 122 | OUTPUT_COST_PER_1M_TOKENS = float(config['API_PRICING'].get('OUTPUT_COST_PER_1M_TOKENS', '0.51')) 123 | CACHE_DISCOUNT_PERCENTAGE = float(config['API_PRICING'].get('CACHE_DISCOUNT_PERCENTAGE', '75')) 124 | 125 | except KeyError as e: 126 | print(f"❌ ERREUR: Section ou clé manquante dans config.ini: {e}") 127 | print("Vérifiez que toutes les sections [KEYS], [OPTIONS], [SERVER], [LANGUAGES], [API_MODELS], [API_PRICING] existent") 128 | sys.exit(1) 129 | except ValueError as e: 130 | print(f"❌ ERREUR: Valeur invalide dans config.ini: {e}") 131 | sys.exit(1) 132 | except Exception as e: 133 | print(f"❌ ERREUR lors de la lecture de config.ini: {e}") 134 | sys.exit(1) 135 | 136 | # Valider les clés API au démarrage 137 | if not validate_api_keys(): 138 | print("\n⚠️ Le script va continuer mais des erreurs peuvent survenir avec des clés API invalides") 139 | print("Appuyez sur Ctrl+C pour arrêter et corriger la configuration\n") 140 | time.sleep(5) 141 | 142 | # Variable pour suivre si le message a été envoyé pendant les tirs au but 143 | penalty_message_sent = False 144 | interruption_message_sent = False 145 | # Convertir la chaîne du fuseau horaire en objet pytz 146 | server_timezone = pytz.timezone(SERVER_TIMEZONE_STR) 147 | # Convertir la chaîne de LEAGUE_IDS en une liste d'entiers 148 | LEAGUE_IDS = [int(id) for id in LEAGUE_IDS_STR.split(',')] 149 | # Empêche notre variable 'main' de se terminer après avoir créé la tâche pour lancer notre bot Discord. 150 | is_running = True 151 | # Défini notre variable pour stocker les événements envoyé pendant le match 152 | sent_events = set() 153 | # Stockage détaillé des événements pour détecter les corrections de timing 154 | sent_events_details = {} 155 | # Variables pour le suivi des coûts API 156 | api_call_count = 0 157 | total_input_tokens = 0 158 | total_output_tokens = 0 159 | total_cost_usd = 0.0 160 | match_tracking_start_time = None 161 | # Chemin du fichier de stockage des analyses de matchs 162 | match_analyses_path = os.path.join(script_dir, 'match_analyses.json') 163 | 164 | # Permet de générer une exception si on dépasse le nombre de call api défini dans une de ces fonctions 165 | class RateLimitExceededError(Exception): 166 | pass 167 | 168 | # Configuration du système de logging professionnel 169 | def setup_logging(): 170 | """Configure le système de logging avec rotation des fichiers""" 171 | logger = logging.getLogger('gptfoot') 172 | logger.setLevel(logging.INFO) 173 | 174 | # Éviter les doublons de handlers 175 | if logger.handlers: 176 | return logger 177 | 178 | # Handler pour fichier avec rotation (max 10MB, garde 5 fichiers) 179 | file_handler = RotatingFileHandler( 180 | 'gptfoot.log', 181 | maxBytes=10*1024*1024, # 10MB 182 | backupCount=5, 183 | encoding='utf-8' 184 | ) 185 | file_handler.setLevel(logging.INFO) 186 | 187 | # Handler pour console 188 | console_handler = logging.StreamHandler() 189 | console_handler.setLevel(logging.WARNING) 190 | 191 | # Format des logs 192 | formatter = logging.Formatter( 193 | '%(asctime)s - %(levelname)s - %(message)s', 194 | datefmt='%Y-%m-%d %H:%M:%S' 195 | ) 196 | file_handler.setFormatter(formatter) 197 | console_handler.setFormatter(formatter) 198 | 199 | logger.addHandler(file_handler) 200 | logger.addHandler(console_handler) 201 | 202 | return logger 203 | 204 | # Initialiser le logger 205 | logger = setup_logging() 206 | 207 | # Fonction pour afficher un message de journalisation avec un horodatage. 208 | def log_message(message: str, level: str = "INFO"): 209 | """ 210 | Fonction de logging améliorée compatible avec l'ancien code 211 | 212 | Args: 213 | message: Le message à logger 214 | level: Niveau de log (INFO, WARNING, ERROR, DEBUG) 215 | """ 216 | level = level.upper() 217 | if level == "DEBUG": 218 | logger.debug(message) 219 | elif level == "WARNING": 220 | logger.warning(message) 221 | elif level == "ERROR": 222 | logger.error(message) 223 | else: 224 | logger.info(message) 225 | 226 | # Fonction pour tracker les coûts API 227 | def track_api_cost(input_tokens: int, output_tokens: int, function_name: str = ""): 228 | """Track API costs based on token usage""" 229 | global api_call_count, total_input_tokens, total_output_tokens, total_cost_usd 230 | 231 | if not ENABLE_COST_TRACKING: 232 | return 233 | 234 | api_call_count += 1 235 | total_input_tokens += input_tokens 236 | total_output_tokens += output_tokens 237 | 238 | # Calculer le coût en USD 239 | input_cost = (input_tokens / 1_000_000) * INPUT_COST_PER_1M_TOKENS 240 | output_cost = (output_tokens / 1_000_000) * OUTPUT_COST_PER_1M_TOKENS 241 | call_cost = input_cost + output_cost 242 | total_cost_usd += call_cost 243 | 244 | log_message(f"[API_COST] {function_name} - Input: {input_tokens} tokens (${input_cost:.6f}), Output: {output_tokens} tokens (${output_cost:.6f}), Total call: ${call_cost:.6f}, Cumulative: ${total_cost_usd:.6f}") 245 | 246 | # Fonction pour afficher le résumé des coûts 247 | def log_cost_summary(): 248 | """Log a summary of API costs at the end of the match""" 249 | if not ENABLE_COST_TRACKING: 250 | return 251 | 252 | log_message("=" * 80) 253 | log_message("[COST_SUMMARY] ===== RÉSUMÉ DES COÛTS API =====") 254 | log_message(f"[COST_SUMMARY] Nombre d'appels API : {api_call_count}") 255 | log_message(f"[COST_SUMMARY] Total tokens entrée : {total_input_tokens}") 256 | log_message(f"[COST_SUMMARY] Total tokens sortie : {total_output_tokens}") 257 | log_message(f"[COST_SUMMARY] Total tokens : {total_input_tokens + total_output_tokens}") 258 | log_message(f"[COST_SUMMARY] Coût total USD : ${total_cost_usd:.6f}") 259 | log_message(f"[COST_SUMMARY] Coût moyen par appel : ${total_cost_usd / api_call_count:.6f}" if api_call_count > 0 else "[COST_SUMMARY] Aucun appel API") 260 | log_message("=" * 80) 261 | 262 | # Fonction qui nous permet de vider le fichier log lorsqu'un nouveau match est détecté optimise la place sur le serveur et laisse quelques jours pour vérifier les logs du match précédent entre chaque match 263 | def clear_log(): 264 | """Vide le fichier de log principal (garde les backups)""" 265 | try: 266 | # Fermer et rouvrir le handler pour vider le fichier 267 | for handler in logger.handlers: 268 | if isinstance(handler, RotatingFileHandler): 269 | handler.close() 270 | 271 | with open("gptfoot.log", "w", encoding='utf-8'): 272 | pass 273 | 274 | # Réinitialiser le logger (pas besoin de global car logger est déjà module-level) 275 | setup_logging() 276 | log_message("Fichier de log vidé pour nouveau match") 277 | except Exception as e: 278 | log_message(f"Erreur lors du vidage du log: {e}", "ERROR") 279 | 280 | ### DEBUT DE GESTION DU STOCKAGE DES ANALYSES DE MATCHS 281 | 282 | # Fonction pour charger l'historique des matchs depuis le fichier JSON 283 | def load_match_history(): 284 | """Charge l'historique des matchs depuis match_analyses.json""" 285 | try: 286 | if os.path.exists(match_analyses_path): 287 | with open(match_analyses_path, "r", encoding="utf-8") as file: 288 | data = json.load(file) 289 | log_message(f"Historique des matchs chargé : {len(data.get('matches', []))} matchs trouvés") 290 | return data 291 | else: 292 | log_message("Fichier match_analyses.json n'existe pas, création d'une nouvelle structure") 293 | return {"matches": []} 294 | except json.JSONDecodeError: 295 | log_message("Erreur de décodage JSON dans match_analyses.json, création d'une nouvelle structure") 296 | return {"matches": []} 297 | except Exception as e: 298 | log_message(f"Erreur lors du chargement de l'historique des matchs : {e}") 299 | return {"matches": []} 300 | 301 | # Fonction pour sauvegarder l'historique des matchs dans le fichier JSON 302 | def save_match_history(data): 303 | """Sauvegarde l'historique des matchs dans match_analyses.json""" 304 | try: 305 | with open(match_analyses_path, "w", encoding="utf-8") as file: 306 | json.dump(data, file, ensure_ascii=False, indent=2) 307 | log_message(f"Historique des matchs sauvegardé : {len(data.get('matches', []))} matchs") 308 | except Exception as e: 309 | log_message(f"Erreur lors de la sauvegarde de l'historique des matchs : {e}") 310 | 311 | # Fonction pour sauvegarder une analyse de match 312 | def save_match_analysis(fixture_id, match_info, pre_match_analysis, post_match_analysis=None): 313 | """Sauvegarde une analyse de match dans l'historique""" 314 | try: 315 | data = load_match_history() 316 | 317 | # Créer l'entrée du match 318 | match_entry = { 319 | "fixture_id": fixture_id, 320 | "date": match_info.get("date", datetime.datetime.now().isoformat()), 321 | "league": match_info.get("league", "Unknown"), 322 | "round": match_info.get("round", "Unknown"), 323 | "teams": match_info.get("teams", {}), 324 | "score": match_info.get("score", {}), 325 | "venue": match_info.get("venue", "Unknown"), 326 | "city": match_info.get("city", "Unknown"), 327 | "pre_match_analysis": pre_match_analysis, 328 | "post_match_analysis": post_match_analysis 329 | } 330 | 331 | # Vérifier si le match existe déjà (par fixture_id) 332 | existing_index = None 333 | for i, match in enumerate(data["matches"]): 334 | if match.get("fixture_id") == fixture_id: 335 | existing_index = i 336 | break 337 | 338 | if existing_index is not None: 339 | # Mettre à jour le match existant 340 | data["matches"][existing_index] = match_entry 341 | log_message(f"Match {fixture_id} mis à jour dans l'historique") 342 | else: 343 | # Ajouter le nouveau match 344 | data["matches"].append(match_entry) 345 | log_message(f"Match {fixture_id} ajouté à l'historique") 346 | 347 | # Garder seulement les 5 derniers matchs (utilisés pour le contexte) 348 | if len(data["matches"]) > 5: 349 | data["matches"] = data["matches"][-5:] 350 | log_message(f"Historique limité à 5 matchs") 351 | 352 | save_match_history(data) 353 | except Exception as e: 354 | log_message(f"Erreur lors de la sauvegarde de l'analyse du match : {e}") 355 | 356 | # Fonction pour récupérer les N derniers matchs 357 | def get_last_n_matches(n=5): 358 | """Récupère les N derniers matchs de l'historique""" 359 | try: 360 | data = load_match_history() 361 | matches = data.get("matches", []) 362 | return matches[-n:] if len(matches) >= n else matches 363 | except Exception as e: 364 | log_message(f"Erreur lors de la récupération des derniers matchs : {e}") 365 | return [] 366 | 367 | # Fonction pour formater l'historique des matchs pour le contexte IA 368 | def format_match_history_for_context(matches): 369 | """Formate l'historique des matchs pour l'inclusion dans le contexte IA avec analyses complètes""" 370 | if not matches: 371 | return "Aucun match précédent disponible." 372 | 373 | formatted = "📊 HISTORIQUE DES 5 DERNIERS MATCHS (ANALYSES COMPLÈTES):\n" 374 | for i, match in enumerate(matches, 1): 375 | date = match.get("date", "Unknown") 376 | league = match.get("league", "Unknown") 377 | home = match.get("teams", {}).get("home", "Unknown") 378 | away = match.get("teams", {}).get("away", "Unknown") 379 | score = match.get("score", {}) 380 | home_score = score.get("home", "?") 381 | away_score = score.get("away", "?") 382 | analysis = match.get("post_match_analysis", "Pas d'analyse disponible") 383 | 384 | # Inclure l'analyse COMPLÈTE sans troncature pour ne pas perdre d'informations importantes 385 | formatted += f"\n{i}. {date} - {league}\n" 386 | formatted += f" {home} {home_score} - {away_score} {away}\n" 387 | formatted += f" Analyse complète:\n{analysis}\n" 388 | 389 | return formatted 390 | 391 | ### FIN DE GESTION DU STOCKAGE DES ANALYSES DE MATCHS 392 | 393 | ### DEBUT DE GESTION DE LA FERMETURE DU SCRIPT 394 | 395 | # Voir si notre script se termine 396 | def log_exit(normal_exit=True, *args, **kwargs): 397 | if normal_exit: 398 | log_message("Script terminé normalement.") 399 | else: 400 | log_message("Script fermé inopinément (signal reçu).") 401 | 402 | # Enregistrez la fonction pour qu'elle s'exécute lorsque le script se termine normalement. 403 | atexit.register(log_exit) 404 | 405 | # Liste de signaux à gérer. Les signaux non disponibles sur la plateforme ne seront pas inclus. 406 | signals_to_handle = [getattr(signal, signame, None) for signame in ["SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT"]] 407 | signals_to_handle = [sig for sig in signals_to_handle if sig is not None] 408 | 409 | # Gestion des signaux pour détecter la fermeture inattendue. 410 | for sig in signals_to_handle: 411 | # Evite de marquer que le script se ferme alors que ce n'est pas le cas avec nohup et la fermeture du terminal! 412 | sighup = getattr(signal, 'SIGHUP', None) 413 | if sig == signal.SIGINT or (sighup is not None and sig == sighup): 414 | signal.signal(sig, signal.SIG_IGN) 415 | else: 416 | signal.signal(sig, lambda signal, frame: log_exit(False)) 417 | 418 | ### FIN DE GESTION DE LA FERMETURE DU SCRIPT 419 | ### DEBUT DE GESTION DU BOT DISCORD 420 | 421 | # Utilisez script_dir pour définir le chemin du fichier discord_channels.json 422 | discord_channels_path = os.path.join(script_dir, 'discord_channels.json') 423 | 424 | intents = discord.Intents.default() 425 | intents.messages = True 426 | intents.message_content = True # Activez l'intent message_content 427 | bot_discord = commands.Bot(command_prefix="!", intents=intents) 428 | 429 | @bot_discord.command() 430 | async def register(ctx): 431 | try: 432 | log_message("Commande !register reçue") 433 | if not os.path.exists(discord_channels_path): 434 | with open(discord_channels_path, "w") as file: 435 | json.dump([], file) 436 | 437 | with open(discord_channels_path, "r") as file: 438 | channels = json.load(file) 439 | 440 | if ctx.channel.id not in channels: 441 | channels.append(ctx.channel.id) 442 | with open(discord_channels_path, "w") as file: 443 | json.dump(channels, file) 444 | await ctx.send("Ce channel a été enregistré.") 445 | log_message(f"Channel {ctx.channel.id} a été enregistré.") 446 | else: 447 | await ctx.send("Ce channel est déjà enregistré.") 448 | log_message(f"Channel {ctx.channel.id} est déjà enregistré.") 449 | except FileNotFoundError: 450 | log_message("Erreur: Le fichier discord_channels.json n'a pas été trouvé ou n'a pas pu être créé.") 451 | except Exception as e: 452 | log_message(f"Erreur lors de l'exécution de !register: {e}") 453 | 454 | @bot_discord.event 455 | async def on_ready(): 456 | if bot_discord.user: 457 | log_message(f'Bot Discord is now online as {bot_discord.user.name}') 458 | else: 459 | log_message('Bot Discord is now online but user is None') 460 | 461 | @bot_discord.event 462 | async def on_error(event, *args, **kwargs): 463 | log_message(f"Erreur dans l'événement {event} : {sys.exc_info()[1]}") 464 | 465 | @bot_discord.event 466 | async def on_command_error(ctx, error): 467 | log_message(f"Erreur avec la commande {ctx.command}: {error}") 468 | 469 | @tasks.loop(count=1) 470 | async def run_discord_bot(token): 471 | try: 472 | await bot_discord.start(token) 473 | except Exception as e: 474 | log_message(f"Erreur lors du démarrage du bot Discord: {e}") 475 | 476 | ### FIN DE GESTION DU BOT DISCORD 477 | ### DEBUT DE GESTION DU BOT TELEGRAM 478 | 479 | # Fonction pour initialiser le fichier des IDs de chat autorisés s'il n'existe pas déjà. 480 | def initialize_chat_ids_file(): 481 | """ 482 | Crée un fichier JSON vide pour stocker les ID de chat si le fichier n'existe pas déjà. 483 | """ 484 | if not os.path.exists("telegram_chat_ids.json"): 485 | try: 486 | with open("telegram_chat_ids.json", "w") as file: 487 | json.dump([], file) 488 | except IOError as e: 489 | log_message(f"Erreur lors de la création du fichier telegram_chat_ids.json : {e}") 490 | 491 | # Fonction déclenchée lorsqu'un utilisateur envoie la commande /start au bot. 492 | async def on_start(message: types.Message): 493 | log_message("on_start(message: types.Message) appelée.") 494 | chat_id = message.chat.id 495 | log_message(f"Fonction on_start() appelée pour le chat {chat_id}") 496 | 497 | # Récupérez les ID de chat existants à partir du fichier JSON 498 | with open("telegram_chat_ids.json", "r") as file: 499 | chat_ids = json.load(file) 500 | 501 | # Ajoutez l'ID de chat au fichier JSON s'il n'est pas déjà présent 502 | if chat_id not in chat_ids: 503 | chat_ids.append(chat_id) 504 | 505 | with open("telegram_chat_ids.json", "w") as file: 506 | json.dump(chat_ids, file) 507 | 508 | await message.reply("Le bot a été démarré et l'ID du chat a été enregistré.") 509 | log_message(f"Le bot a été démarré et l'ID du chat {chat_id} a été enregistré.") 510 | else: 511 | await message.reply("Le bot a déjà été démarré dans ce chat.") 512 | log_message(f"Le bot a déjà été démarré dans ce chat {chat_id}.") 513 | 514 | ### FIN DE GESTION DU BOT TELEGRAM 515 | 516 | # Vérifie périodiquement si un match est prévu et, si c'est le cas, récupère les informations pertinentes et effectue des actions appropriées. 517 | async def check_match_periodically(): 518 | 519 | while True: 520 | now = datetime.datetime.now() 521 | target_time = datetime.datetime(now.year, now.month, now.day, 9, 0, 0) 522 | 523 | if now > target_time: 524 | target_time += datetime.timedelta(days=1) 525 | 526 | seconds_until_target_time = (target_time - now).total_seconds() 527 | log_message(f"Attente de {seconds_until_target_time} secondes jusqu'à la prochaine vérification des matchs (09:00).") 528 | await asyncio.sleep(seconds_until_target_time) 529 | 530 | # Vérifiez les matchs 531 | log_message("Vérification du match en cours...") 532 | await check_matches() 533 | 534 | # Vérifie si un match est prévu aujourd'hui et effectue les actions appropriées, comme envoyer des messages de début et de fin de match, et vérifier les événements pendant le match. 535 | async def check_matches(): 536 | global sent_events 537 | log_message("get_team_match_info() appelée.") 538 | #On ignore la dernière value (current_league_id) qui n'est pas importante ici et déjà déclaré comme une variable globale ! 539 | match_today, match_start_time, fixture_id, current_league_id, teams, league, round_info, venue, city = await is_match_today() 540 | 541 | log_message( 542 | f"Résultat de is_match_today() dans la fonction check_match_periodically : " 543 | f"match_today = {match_today}, " 544 | f"match_start_time = {match_start_time}, " 545 | f"fixture_id = {fixture_id}, " 546 | f"current_league_id = {current_league_id}, " 547 | f"teams = {teams}, " 548 | f"league = {league}, " 549 | f"round_info = {round_info}, " 550 | f"venue = {venue}, " 551 | f"city = {city}" 552 | ) 553 | 554 | if match_today: 555 | # Réinitialiser les variables de suivi des coûts pour ce match 556 | global api_call_count, total_input_tokens, total_output_tokens, total_cost_usd, penalty_message_sent, interruption_message_sent 557 | api_call_count = 0 558 | total_input_tokens = 0 559 | total_output_tokens = 0 560 | total_cost_usd = 0.0 561 | # Réinitialiser les flags de message pour le nouveau match 562 | penalty_message_sent = False 563 | interruption_message_sent = False 564 | 565 | # Vider le fichier de logs si un match est trouvé 566 | clear_log() 567 | log_message(f"un match a été trouvé") 568 | log_message(f"[COST_TRACKING] Début du suivi des coûts pour le match") 569 | # Vérifie que match_start_time n'est pas None et qu'il a des attributs hour et minute. 570 | if match_start_time and hasattr(match_start_time, 'hour') and hasattr(match_start_time, 'minute'): 571 | now = datetime.datetime.now() 572 | match_start_datetime = now.replace(hour=match_start_time.hour, minute=match_start_time.minute, second=0, microsecond=0) 573 | seconds_until_match_start = (match_start_datetime - now).total_seconds() 574 | log_message(f"Il reste {seconds_until_match_start} secondes avant le début du match.") 575 | # Calculer le temps pour envoyer le message 10 minutes avant le début du match 576 | seconds_until_message = max(0, seconds_until_match_start - 900) # 900 secondes = 15 minutes 577 | # On envoie le message pour annoncer qu'il y a un match aujourd'hui 578 | await send_match_today_message(match_start_time, fixture_id, current_league_id, teams, league, round_info, venue, city) 579 | 580 | # Attendre jusqu'à l'heure d'envoi du message 10 minutes avant le début du match 581 | await asyncio.sleep(seconds_until_message) 582 | # Envoyer le message si IS_PAID_API est vrai 583 | if IS_PAID_API: 584 | # Attendez que le match débute réellement qui est vérifié dans wait_for_match_start 585 | match_data = (await wait_for_match_start(fixture_id, teams, league, round_info, venue, city))[3] 586 | log_message(f"match_data reçu de wait_for_match_start dans check_matches {match_data}\n") 587 | 588 | else: 589 | # Attendre jusqu'au début du match pour envoyer la compo et voir le match a réellement commencé si on utilise l'api free pour limiter les calls à l'api 590 | # On vérifie pas ici si le match a déjà commencé car la structure du code fait en sorte qu'on puisse pas lancer le script pendant un match qui a commencé pour récuprer ses infos il faut attendre les matchs suivants. 591 | remaining_seconds = seconds_until_match_start - seconds_until_message 592 | await asyncio.sleep(remaining_seconds) 593 | log_message(f"Fin de l'attente jusqu'à l'heure prévu de début de match") 594 | # Attendez que le match débute réellement 595 | match_data = (await wait_for_match_start(fixture_id, teams, league, round_info, venue, city))[3] 596 | log_message(f"match_data reçu de wait_for_match_start dans check_matches {match_data}\n") 597 | 598 | # Envoyez le message de début de match et commencez à vérifier les événements 599 | if match_data is not None: 600 | log_message(f"Envoie du message de début de match avec send_start_message (uniquement utile pour l'api payante avec interval court)") 601 | await send_start_message() 602 | log_message(f"Check des événements du match avec check_events") 603 | # Réinitialiser les événements envoyés au début de chaque match 604 | sent_events.clear() 605 | sent_events_details.clear() 606 | log_message(f"sent_events et sent_events_details vidés pour le nouveau match, taille: {len(sent_events)}") 607 | await check_events(fixture_id) 608 | else: 609 | log_message(f"Pas de match_data pour l'instant (fonction check_matches), résultat de match_data : {match_data}") 610 | else: 611 | log_message(f"Pas d'heure de début de match") 612 | else: 613 | log_message(f"Aucun match prévu aujourd'hui") 614 | 615 | # Fonction pour récupérer les prédictions 616 | async def get_match_predictions(fixture_id): 617 | log_message("get_match_predictions() appelée.") 618 | url = f"https://v3.football.api-sports.io/predictions?fixture={fixture_id}" 619 | headers = { 620 | "x-apisports-key": API_FOOTBALL_KEY 621 | } 622 | 623 | try: 624 | timeout = aiohttp.ClientTimeout(total=30) 625 | async with aiohttp.ClientSession(timeout=timeout) as session: 626 | async with session.get(url, headers=headers) as resp: 627 | remaining_calls_per_day = int(resp.headers.get('x-ratelimit-requests-remaining', 0)) 628 | log_message(f"Nombre d'appels à l'api restants : {remaining_calls_per_day}") 629 | 630 | if remaining_calls_per_day < 3: 631 | log_message(f"#####\nLe nombre d'appels à l'API est dépassé. Le suivi du match est stoppé.\n#####", "WARNING") 632 | await notify_users_max_api_requests_reached() 633 | raise RateLimitExceededError("Le nombre d'appels maximum à l'API est dépassé.") 634 | 635 | resp.raise_for_status() 636 | data = await resp.json() 637 | 638 | if not data.get('response'): 639 | log_message(f"Pas de données récupérées depuis get_match_predictions", "WARNING") 640 | return None 641 | 642 | return data['response'][0]['predictions'] 643 | 644 | except asyncio.TimeoutError: 645 | log_message(f"Timeout lors de la récupération des prédictions pour fixture {fixture_id}", "ERROR") 646 | return None 647 | except aiohttp.ClientError as e: 648 | log_message(f"Erreur réseau dans get_match_predictions: {e}", "ERROR") 649 | return None 650 | except RateLimitExceededError: 651 | raise 652 | except KeyError as e: 653 | log_message(f"Données manquantes dans la réponse API (get_match_predictions): {e}", "ERROR") 654 | return None 655 | except Exception as e: 656 | log_message(f"Erreur inattendue dans get_match_predictions: {e}", "ERROR") 657 | return None 658 | 659 | #Fonction qui permet de vérifier quand le match démarre réellement par rapport à l'heure prévu en vérifiant si le match a toujours lieu! 660 | async def wait_for_match_start(fixture_id, teams=None, league=None, round_info=None, venue=None, city=None): 661 | log_message(f"fonction wait_for_match_start appelée") 662 | 663 | # Récupérer les prédictions avant d'envoyer le message de compo (uniquement avec l'api payante car call limité avec gratuit) 664 | predictions = None 665 | if IS_PAID_API: 666 | predictions = await get_match_predictions(fixture_id) 667 | if predictions: 668 | log_message(f"Prédictions obtenues : {predictions}") 669 | 670 | # Permet d'envoyer la compo à l'heure du début du match prévue avant que le match commence réellement ! Attention coûte un appel API en plus ! 671 | match_status, match_date, elapsed_time, match_data = await get_check_match_status(fixture_id) 672 | # Note : On peut potentiellement vérifier ici (actuellement pas le cas) si la compo renvoyée par get_check_match_status et pas none et on pourrait retenter 5 minutes plus tard en mettant le script sur pause uniquement si paid api ?! 673 | log_message(f"match_status: {match_status}, match_date: {match_date}, elapsed_time: {elapsed_time} et \n [DEBUG] match data :\n {match_data}\n\n") 674 | log_message(f"Envoie du message de compo de match avec send_compo_message") 675 | await send_compo_message(match_data, predictions, fixture_id, teams, league, round_info, venue, city) 676 | 677 | while True: 678 | match_status, match_date, elapsed_time, match_data = await get_check_match_status(fixture_id) 679 | 680 | if match_status and elapsed_time is not None: 681 | # Sortir de la boucle si le match commence, ou est annulé pour X raisons 682 | if elapsed_time > 0 or match_status in ('PST', 'CANC', 'ABD', 'AWD', 'WO'): 683 | log_message(f"le match a commencé sorti de la boucle wait_for_match_start") 684 | break 685 | log_message("Le match n'a pas encore commencé, en attente...") 686 | 687 | # Si api gratuite utilisée Attendre 120 secondes avant de vérifier à nouveau (comme on envoie plus le message de début on peut limiter le nombre d'appels à l'api!) 688 | sleep_time = 30 if IS_PAID_API else 120 # On met 30 secondes et pas 15 car cela évite trop d'appeler la fonction trop fréquemment sachant que la composition est envoyé 15 minutes avant 689 | await asyncio.sleep(sleep_time) 690 | 691 | # Retourner None si le match est reporté ou annulé 692 | if match_status in ('PST', 'CANC', 'ABD'): 693 | log_message(f"Le statut du match indique qu'il a été annulé ou reporté") 694 | message = f"🤖 : Le statut du match indique qu'il a été annulé ou reporté" 695 | await send_message_to_all_chats(message) 696 | return None, None, None, None 697 | elif match_status in ('AWD', 'WO'): 698 | log_message(f"Défaite technique ou victoire par forfait ou absence de compétiteur") 699 | message = f"🤖 : Défaite technique ou victoire par forfait ou absence de compétiteur" 700 | await send_message_to_all_chats(message) 701 | return None, None, None, None 702 | else: 703 | log_message(f"match_status: {match_status}, match_date: {match_date}, elapsed_time: {elapsed_time}, match_data (pas log)\n") 704 | return match_status, match_date, elapsed_time, match_data 705 | 706 | # Récupère le statut et la date du match de la team dans la ligue spécifiée avec retry. 707 | async def get_check_match_status(fixture_id, max_retries=3): 708 | log_message("get_check_match_status() appelée.") 709 | url = f"https://v3.football.api-sports.io/fixtures?id={fixture_id}" 710 | headers = { 711 | "x-apisports-key": API_FOOTBALL_KEY 712 | } 713 | 714 | for attempt in range(max_retries): 715 | try: 716 | async with aiohttp.ClientSession() as session: 717 | async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp: 718 | # Vérifiez le nombre d'appels restants par jour 719 | remaining_calls_per_day = int(resp.headers.get('x-ratelimit-requests-remaining', 0)) 720 | log_message(f"Nombre d'appels à l'api restants : {remaining_calls_per_day}") 721 | 722 | #Permet de sortir si on reste bloqué dans cette fonction pour x raisons 723 | #3 car on check 3 league à la sortie 724 | if remaining_calls_per_day < 3: 725 | log_message(f"#####\nLe nombre d'appels à l'API est dépassé. Le suivi du match est stoppé.\n#####") 726 | await notify_users_max_api_requests_reached() 727 | raise RateLimitExceededError("Le nombre d'appels maximum à l'API est dépassé.") 728 | 729 | # Vérifier le code de statut HTTP 730 | if resp.status != 200: 731 | log_message(f"Erreur HTTP {resp.status} de l'API football (tentative {attempt + 1}/{max_retries})") 732 | if resp.status >= 500 and attempt < max_retries - 1: 733 | await asyncio.sleep(2 ** attempt) 734 | continue 735 | elif resp.status == 429 and attempt < max_retries - 1: 736 | await asyncio.sleep(5 * (2 ** attempt)) 737 | continue 738 | return None, None, None, None 739 | 740 | data = await resp.json() 741 | if not data.get('response'): 742 | log_message(f"Pas de données récupérées depuis get_check_match_status") 743 | return None, None, None, None 744 | 745 | fixture = data['response'][0] 746 | match_status = fixture['fixture']['status']['short'] 747 | match_date = datetime.datetime.strptime(fixture['fixture']['date'], '%Y-%m-%dT%H:%M:%S%z').astimezone(server_timezone) 748 | elapsed_time = fixture['fixture']['status']['elapsed'] 749 | # Récupérez match_data à partir de la variable fixture 750 | match_data = { 751 | "teams": { 752 | "home": { 753 | "name": fixture['teams']['home']['name'] 754 | }, 755 | "away": { 756 | "name": fixture['teams']['away']['name'] 757 | } 758 | }, 759 | "lineups": {} 760 | } 761 | 762 | if 'lineups' in fixture and len(fixture['lineups']) >= 2: 763 | home_lineup = fixture['lineups'][0] 764 | away_lineup = fixture['lineups'][1] 765 | 766 | home_startXI = home_lineup.get('startXI', []) 767 | away_startXI = away_lineup.get('startXI', []) 768 | 769 | match_data["lineups"] = { 770 | home_lineup['team']['name']: { 771 | "formation": home_lineup.get('formation', ''), 772 | "startXI": home_startXI 773 | }, 774 | away_lineup['team']['name']: { 775 | "formation": away_lineup.get('formation', ''), 776 | "startXI": away_startXI 777 | } 778 | } 779 | else: 780 | match_data["lineups"] = { 781 | match_data["teams"]["home"]["name"]: { 782 | "formation": "", 783 | "startXI": [] 784 | }, 785 | match_data["teams"]["away"]["name"]: { 786 | "formation": "", 787 | "startXI": [] 788 | } 789 | } 790 | 791 | log_message(f"Statut et données de match récupérés depuis get_check_match_status : {match_status}, \n Date du match : {match_date}, \n Temps écoulé : {elapsed_time}, \n match data : (no log) \n") 792 | return match_status, match_date, elapsed_time, match_data 793 | 794 | except asyncio.TimeoutError: 795 | log_message(f"Timeout lors de l'appel à l'API football (tentative {attempt + 1}/{max_retries})") 796 | if attempt < max_retries - 1: 797 | await asyncio.sleep(2 ** attempt) 798 | continue 799 | return None, None, None, None 800 | except aiohttp.ClientError as e: 801 | log_message(f"Erreur réseau lors de la requête à l'API football (tentative {attempt + 1}/{max_retries}): {e}") 802 | if attempt < max_retries - 1: 803 | await asyncio.sleep(2 ** attempt) 804 | continue 805 | return None, None, None, None 806 | except Exception as e: 807 | log_message(f"Erreur inattendue lors de la requête à l'API football (tentative {attempt + 1}/{max_retries}): {e}") 808 | if attempt < max_retries - 1: 809 | await asyncio.sleep(2 ** attempt) 810 | continue 811 | return None, None, None, None 812 | 813 | log_message(f"Tous les {max_retries} appels à l'API football ont échoué") 814 | return None, None, None, None 815 | 816 | # Fonction asynchrone pour vérifier s'il y a un match aujourd'hui et retourner les informations correspondantes avec retry. 817 | async def is_match_today(max_retries=3): 818 | log_message("is_match_today() appelée.") 819 | responses = [] 820 | # déclaration de la variable comme globale 821 | global current_league_id 822 | # Variable pour stocker l'ID de la ligue en cours de traitement 823 | current_league_id = None 824 | 825 | for attempt in range(max_retries): 826 | try: 827 | async with aiohttp.ClientSession() as session: 828 | for LEAGUE_ID in LEAGUE_IDS: 829 | url = f"https://v3.football.api-sports.io/fixtures?team={TEAM_ID}&league={LEAGUE_ID}&next=1" 830 | headers = { 831 | "x-apisports-key": API_FOOTBALL_KEY 832 | } 833 | try: 834 | async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp: 835 | if resp.status == 200: 836 | data = await resp.json() 837 | if data.get('response'): 838 | responses.append(data) 839 | current_league_id = LEAGUE_ID 840 | elif resp.status >= 500 and attempt < max_retries - 1: 841 | log_message(f"Erreur serveur {resp.status} pour la ligue {LEAGUE_ID}, retry...") 842 | await asyncio.sleep(2 ** attempt) 843 | continue 844 | elif resp.status == 429 and attempt < max_retries - 1: 845 | log_message(f"Rate limit pour la ligue {LEAGUE_ID}, attente...") 846 | await asyncio.sleep(5 * (2 ** attempt)) 847 | continue 848 | except asyncio.TimeoutError: 849 | log_message(f"Timeout pour la ligue {LEAGUE_ID} (tentative {attempt + 1}/{max_retries})") 850 | if attempt < max_retries - 1: 851 | await asyncio.sleep(2 ** attempt) 852 | continue 853 | except aiohttp.ClientError as e: 854 | log_message(f"Erreur réseau pour la ligue {LEAGUE_ID} (tentative {attempt + 1}/{max_retries}): {e}") 855 | if attempt < max_retries - 1: 856 | await asyncio.sleep(2 ** attempt) 857 | continue 858 | 859 | # Si on a au moins une réponse, on sort de la boucle de retry 860 | if responses: 861 | break 862 | except Exception as e: 863 | log_message(f"Erreur inattendue dans is_match_today (tentative {attempt + 1}/{max_retries}): {e}") 864 | if attempt < max_retries - 1: 865 | await asyncio.sleep(2 ** attempt) 866 | continue 867 | 868 | if not responses: 869 | log_message(f"Impossible de récupérer les matchs après {max_retries} tentatives") 870 | await send_message_to_all_chats("🤖 : Impossible de vérifier les matchs. L'API football est indisponible. Veuillez réessayer plus tard.") 871 | return False, None, None, None, None, None, None, None, None 872 | 873 | match_today = False 874 | match_start_time = None 875 | fixture_id = None 876 | 877 | # Ajout de nouvelles variables pour les informations supplémentaires 878 | teams = None 879 | league = None 880 | round_info = None 881 | venue = None 882 | city = None 883 | 884 | for response in responses: 885 | if response['results'] > 0: 886 | fixture_data = response['response'][0]['fixture'] 887 | league_data = response['response'][0]['league'] 888 | teams_data = response['response'][0]['teams'] 889 | venue_data = fixture_data['venue'] 890 | 891 | match_date = datetime.datetime.strptime(fixture_data['date'], '%Y-%m-%dT%H:%M:%S%z') 892 | match_date = match_date.astimezone(server_timezone) 893 | match_date = match_date.date() 894 | today = datetime.date.today() 895 | 896 | if match_date == today: 897 | match_today = True 898 | match_start_datetime = datetime.datetime.strptime(fixture_data['date'], '%Y-%m-%dT%H:%M:%S%z') 899 | match_start_datetime = match_start_datetime.astimezone(server_timezone) 900 | match_start_time = match_start_datetime.time() 901 | fixture_id = fixture_data['id'] 902 | 903 | # Extraction des informations supplémentaires 904 | teams = { 905 | "home": teams_data['home']['name'], 906 | "away": teams_data['away']['name'] 907 | } 908 | league = league_data['name'] 909 | round_info = league_data['round'] 910 | venue = venue_data['name'] 911 | city = venue_data['city'] 912 | break 913 | 914 | # Inclure les nouvelles informations dans la valeur de retour 915 | return match_today, match_start_time, fixture_id, current_league_id, teams, league, round_info, venue, city 916 | 917 | # Récupère les événements en direct (buts, cartons, etc.) et le statut du match pour un match donné. 918 | async def get_team_live_events(fixture_id): 919 | log_message("get_team_live_events() appelée.") 920 | events_url = f"https://v3.football.api-sports.io/fixtures?id={fixture_id}" 921 | headers = { 922 | "x-apisports-key": API_FOOTBALL_KEY 923 | } 924 | 925 | try: 926 | timeout = aiohttp.ClientTimeout(total=30) 927 | async with aiohttp.ClientSession(timeout=timeout) as session: 928 | async with session.get(events_url, headers=headers) as events_response: 929 | # Vérifiez le nombre d'appels restants par jour 930 | remaining_calls_per_day = int(events_response.headers.get('x-ratelimit-requests-remaining', 0)) 931 | log_message(f"Nombre d'appels à l'api restants : {remaining_calls_per_day}") 932 | 933 | # 3 car on check 3 league à la sortie 934 | if remaining_calls_per_day < 3: 935 | await notify_users_max_api_requests_reached() 936 | log_message(f"#####\nLe nombre d'appels à l'API est dépassé. Le suivi du match est stoppé.\n#####", "WARNING") 937 | raise RateLimitExceededError("Le nombre d'appels maximum à l'API est dépassé.") 938 | 939 | events_response.raise_for_status() 940 | events_data = await events_response.json() 941 | 942 | if not events_data.get('response'): 943 | log_message("Pas de réponse de l'API pour get_team_live_events", "WARNING") 944 | return None, None, None, None, None 945 | 946 | match_info = events_data['response'][0] 947 | events = match_info['events'] 948 | match_status = match_info['fixture']['status']['short'] 949 | elapsed_time = match_info['fixture']['status']['elapsed'] 950 | match_data = match_info 951 | log_message(f"Temps écoulé du match : {elapsed_time}\n") 952 | match_statistics = match_info['statistics'] 953 | 954 | return events, match_status, elapsed_time, match_data, match_statistics 955 | 956 | except asyncio.TimeoutError: 957 | log_message(f"Timeout lors de la récupération des événements pour fixture {fixture_id}", "ERROR") 958 | return None, None, None, None, None 959 | except aiohttp.ClientError as e: 960 | log_message(f"Erreur réseau lors de la requête à l'API (via get_team_live_events): {e}", "ERROR") 961 | return None, None, None, None, None 962 | except RateLimitExceededError: 963 | raise 964 | except KeyError as e: 965 | log_message(f"Données manquantes dans la réponse API (get_team_live_events): {e}", "ERROR") 966 | return None, None, None, None, None 967 | except Exception as e: 968 | log_message(f"Erreur inattendue lors de la requête à l'API (via get_team_live_events): {e}", "ERROR") 969 | return None, None, None, None, None 970 | 971 | # Fonction helper pour gérer la mi-temps 972 | async def handle_halftime(fixture_id, match_status, IS_PAID_API): 973 | """Gère la logique de mi-temps pour API payante et gratuite""" 974 | log_message(f"mi-temps détectée") 975 | 976 | if IS_PAID_API: 977 | log_message(f"API payante : vérification toutes les 15 secondes") 978 | 979 | # Boucle pour vérifier le statut du match après la pause 980 | while True: 981 | log_message(f"On vérifie si le match a repris (statut actuel : {match_status})") 982 | events, match_status, elapsed_time, match_data, match_statistics = await get_team_live_events(fixture_id) 983 | log_message(f"Données récupérées de get_team_live_events dans handle_halftime;\n Statistiques de match : (pas log),\n Status de match : {match_status},\n Events {events},\n match_data : (pas log)\n") 984 | 985 | if match_status != 'HT': 986 | log_message(f"Le match a repris (statut actuel : {match_status}), continuation de l'execution du code") 987 | return None, match_status, elapsed_time, match_data, match_statistics 988 | 989 | await asyncio.sleep(15) 990 | else: 991 | # API gratuite 992 | log_message(f"mi-temps détectée - mise en pause de l'execution du code pour 780 secondes") 993 | await asyncio.sleep(780) # 13 minutes 994 | 995 | # Boucle pour vérifier le statut du match après la pause 996 | while True: 997 | log_message(f"On vérifie si le match a repris (statut actuel : {match_status})") 998 | events, match_status, elapsed_time, match_data, match_statistics = await get_team_live_events(fixture_id) 999 | log_message(f"Données récupérées de get_team_live_events dans handle_halftime;\n Statistiques de match : (pas log),\n Status de match : {match_status},\n Events {events},\n match_data : (pas log)\n") 1000 | 1001 | if match_status != 'HT': 1002 | log_message(f"Le match a repris (statut actuel : {match_status}), continuation de l'execution du code") 1003 | return None, match_status, elapsed_time, match_data, match_statistics 1004 | 1005 | await asyncio.sleep(120) 1006 | 1007 | # Fonction helper pour gérer les tirs au but 1008 | async def handle_penalty_shootout(fixture_id, penalty_message_sent, IS_PAID_API): 1009 | """Gère la logique des tirs au but""" 1010 | if not penalty_message_sent: 1011 | log_message("Séance de tir au but : attente de 20 minutes la fin des pénos pour envoyer les informations du match restants + fin de match pour limiter le nombre d'appels à l'api !") 1012 | await pause_for_penalty_shootout() 1013 | penalty_message_sent = True 1014 | 1015 | if IS_PAID_API: 1016 | wait_time = 30 1017 | else: 1018 | await asyncio.sleep(1200) # 20 minutes 1019 | wait_time = 300 1020 | 1021 | # Boucle pour vérifier le statut du match après les pénos 1022 | while True: 1023 | log_message(f"On vérifie si les pénos (PEN) sont terminés") 1024 | events, match_status, elapsed_time, match_data, match_statistics = await get_team_live_events(fixture_id) 1025 | log_message("Données récupérées de get_team_live_events; Statistiques de match : (pas log), Status de match : {}, Events {}, match_data : (pas log)".format(match_status, events)) 1026 | 1027 | if match_status != 'PEN': 1028 | log_message(f"Le match a repris (statut actuel : {match_status}), continuation de l'exécution du code") 1029 | return penalty_message_sent, events, match_status, elapsed_time, match_data, match_statistics 1030 | 1031 | await asyncio.sleep(wait_time) 1032 | 1033 | # Fonction helper pour gérer les interruptions 1034 | async def handle_interruption(fixture_id, interruption_message_sent, IS_PAID_API): 1035 | """Gère la logique des interruptions de match""" 1036 | log_message(f"Match interrompu (INT)") 1037 | 1038 | if not interruption_message_sent: 1039 | await notify_match_interruption() 1040 | interruption_message_sent = True 1041 | 1042 | if IS_PAID_API: 1043 | wait_time = 120 1044 | else: 1045 | await asyncio.sleep(600) # 10 minutes 1046 | wait_time = 600 1047 | 1048 | # Boucle pour vérifier le statut du match après l'interruption 1049 | while True: 1050 | log_message(f"On vérifie si l'interruption est terminée (statut actuel : INT)") 1051 | events, match_status, elapsed_time, match_data, match_statistics = await get_team_live_events(fixture_id) 1052 | log_message(f"Données récupérées de get_team_live_events;\n Statistiques de match : (pas log),\n Status de match : {match_status},\n Events {events},\n match_data : (pas log)\n") 1053 | 1054 | if match_status != 'INT': 1055 | log_message(f"Le match a repris (statut actuel : {match_status}), continuation de l'execution du code") 1056 | return interruption_message_sent, events, match_status, elapsed_time, match_data, match_statistics 1057 | 1058 | await asyncio.sleep(wait_time) 1059 | 1060 | # Fonction helper pour traiter un événement de but 1061 | async def process_goal_event(event, match_data, elapsed_time, current_score, previous_score, is_first_event, IS_PAID_API, match_status): 1062 | """Traite un événement de but et retourne les informations nécessaires""" 1063 | if event['detail'] == 'Missed Penalty': 1064 | last_missed_penalty_time = event['time']['elapsed'] 1065 | log_message(f"Penalty manqué détecté à {last_missed_penalty_time} minutes.") 1066 | 1067 | # Notifier uniquement si ce n'est PAS pendant les tirs au but 1068 | if match_status not in ('P', 'PEN'): 1069 | player = event['player'] 1070 | team = event['team'] 1071 | if player is not None and team is not None: 1072 | await send_missed_penalty_message(player, team, last_missed_penalty_time) 1073 | 1074 | return None, False, is_first_event 1075 | 1076 | log_message(f"Not Missed Penalty") 1077 | player = event['player'] 1078 | team = event['team'] 1079 | current_elapsed_time = elapsed_time 1080 | goal_elapsed_time = event['time']['elapsed'] 1081 | allowed_difference = -10 1082 | 1083 | # Créer un dictionnaire avec toutes les informations du but 1084 | goal_info = { 1085 | 'player': player, 1086 | 'team': team, 1087 | 'event': event, 1088 | 'elapsed_time': goal_elapsed_time, 1089 | 'event_key': f"{event['type']}_{event['time']['elapsed']}_{player['id'] if player and player.get('id') else None}", 1090 | 'player_statistics': None, 1091 | 'significant_increase': False 1092 | } 1093 | 1094 | # Récupérer les statistiques du joueur si disponibles 1095 | if player is not None and player.get('id'): 1096 | player_id = player['id'] 1097 | if 'players' in match_data: 1098 | for team_data in match_data['players']: 1099 | for player_stats in team_data['players']: 1100 | if 'player' in player_stats and player_stats['player']['id'] == player_id: 1101 | goal_info['player_statistics'] = player_stats['statistics'] 1102 | break 1103 | if goal_info['player_statistics']: 1104 | break 1105 | 1106 | # Vérifier si on a un temps de match valide avant de faire des comparaisons 1107 | if current_elapsed_time is None: 1108 | log_message(f"[AVERTISSEMENT] Impossible de vérifier l'horodatage du but car le temps de match écoulé est None.") 1109 | return None, False, is_first_event 1110 | 1111 | # Vérifier si le but est dans l'intervalle de temps acceptable 1112 | if not (player is not None and player.get('id') and player.get('name') and 1113 | goal_elapsed_time is not None and 1114 | goal_elapsed_time >= current_elapsed_time + allowed_difference): 1115 | if goal_elapsed_time is not None and goal_elapsed_time < current_elapsed_time + allowed_difference: 1116 | log_message(f"[ATTENTION] L'event goal (temps: {goal_elapsed_time}) est trop ancien par rapport au temps actuel ({current_elapsed_time}).") 1117 | return None, False, is_first_event 1118 | 1119 | log_message(f"L'événement de goal a été détecté dans un interval de 10 minutes") 1120 | 1121 | # Calculer le nouveau score depuis match_data 1122 | new_score = { 1123 | 'home': match_data['goals']['home'], 1124 | 'away': match_data['goals']['away'] 1125 | } 1126 | 1127 | if is_first_event or new_score != previous_score: 1128 | # Vérifier l'augmentation significative du score (plus de 1 but marqué par une équipe) 1129 | significant_increase_in_score = False 1130 | if team['id'] == match_data['teams']['home']['id'] and new_score['home'] - current_score['home'] > 1: 1131 | significant_increase_in_score = True 1132 | elif team['id'] == match_data['teams']['away']['id'] and new_score['away'] - current_score['away'] > 1: 1133 | significant_increase_in_score = True 1134 | 1135 | goal_info['significant_increase'] = significant_increase_in_score 1136 | return goal_info, True, False 1137 | 1138 | elif IS_PAID_API and match_status == 'P': 1139 | await send_shootout_goal_message(player, team, 1140 | goal_info['player_statistics'] if goal_info['player_statistics'] else [], 1141 | event) 1142 | return None, False, is_first_event 1143 | else: 1144 | log_message(f"Le score n'a pas été modifié car l'API ne l'a pas mis à jour") 1145 | return None, False, is_first_event 1146 | 1147 | # Fonction asynchrone pour vérifier les événements en cours pendant un match, tels que les buts et les cartons rouges. 1148 | async def check_events(fixture_id): 1149 | log_message("check_events(fixture_id) appelée.") 1150 | global sent_events 1151 | global sent_events_details 1152 | global IS_PAID_API 1153 | global penalty_message_sent 1154 | global interruption_message_sent 1155 | current_score = {'home': 0, 'away': 0} 1156 | previous_score = {'home': 0, 'away': 0} 1157 | score_updated = False 1158 | is_first_event = True 1159 | # Nouvelle liste pour stocker temporairement les événements de but 1160 | goal_events = [] 1161 | 1162 | while True: 1163 | try: 1164 | events, match_status, elapsed_time, match_data, match_statistics = await get_team_live_events(fixture_id) 1165 | # S'assurer que match_data n'est pas None avant d'en extraire 'goals'. 1166 | if match_data and match_data.get('goals'): 1167 | new_score = { 1168 | 'home': match_data['goals']['home'], 1169 | 'away': match_data['goals']['away'] 1170 | } 1171 | 1172 | if new_score != current_score: 1173 | log_message(f"Mise à jour du score après les événements VAR : {current_score} -> {new_score}") 1174 | previous_score = current_score.copy() 1175 | current_score = new_score.copy() 1176 | else: 1177 | log_message(f"Pas de match_data ou de données de buts disponibles (none)\n") 1178 | new_score = current_score # Garder le score actuel si pas de nouvelles données 1179 | 1180 | # Calcul de l'intervalle optimisé selon api payante ou non 1181 | if IS_PAID_API: 1182 | interval = 15 1183 | else: 1184 | # Pour API gratuite - Utilisez current_league_id pour définir un intervalle différent selon l'id de la ligue 1185 | if current_league_id == 2: 1186 | total_duree_championnat = 5 + 45 + 10 + 45 + 10 + 30 1187 | interval = (total_duree_championnat * 60) / 90 1188 | elif current_league_id == 207: 1189 | total_duree_championnat = 5 + 45 + 10 + 45 + 10 1190 | interval = (total_duree_championnat * 60) / 90 1191 | elif current_league_id == 209: 1192 | total_duree_championnat = 5 + 45 + 10 + 45 + 10 + 30 1193 | interval = (total_duree_championnat * 60) / 90 1194 | else: 1195 | total_duree_championnat = 5 + 45 + 10 + 45 + 10 + 30 1196 | interval = (total_duree_championnat * 60) / 90 1197 | 1198 | # Gestion de la mi-temps 1199 | if match_status == 'HT': 1200 | result = await handle_halftime(fixture_id, match_status, IS_PAID_API) 1201 | if result: 1202 | events, match_status, elapsed_time, match_data, match_statistics = result 1203 | events = None # Réinitialiser pour éviter de renvoyer les événements de la première mi-temps 1204 | 1205 | # Gestion des tirs au but 1206 | if match_status == 'P' or match_status == 'PEN': 1207 | penalty_message_sent, events, match_status, elapsed_time, match_data, match_statistics = await handle_penalty_shootout(fixture_id, penalty_message_sent, IS_PAID_API) 1208 | 1209 | # Gestion des interruptions 1210 | if match_status == 'INT': 1211 | interruption_message_sent, events, match_status, elapsed_time, match_data, match_statistics = await handle_interruption(fixture_id, interruption_message_sent, IS_PAID_API) 1212 | 1213 | # Vérifiez que events n'est pas None avant de l'itérer 1214 | if events is None: 1215 | await asyncio.sleep(interval) 1216 | continue 1217 | 1218 | # Boucle pour vérifier les événements 1219 | for event in events: 1220 | # Vérifier si match_data n'est pas None 1221 | if match_data is None: 1222 | log_message("match_data est None, impossible de continuer le traitement des événements") 1223 | break 1224 | 1225 | # Vérifiez si l'attribut 'player' existe sinon on lui attribue une valeur nulle 1226 | player_id = event['player']['id'] if 'player' in event and event['player'] is not None else None 1227 | # On créé une clé uniquement pour identifier l'événement en question 1228 | event_key = f"{event['type']}_{event['time']['elapsed']}_{player_id}" 1229 | # Clé pour détecter les corrections (inclut le timing pour distinguer les doublés) 1230 | event_key_sub = f"{event['type']}_{player_id}_{event['time']['elapsed']}" 1231 | 1232 | # ISSUE 6: Vérifier si l'événement a déjà été envoyé et si le timing a changé 1233 | if event_key_sub in sent_events_details: 1234 | old_data = sent_events_details[event_key_sub] 1235 | old_time = old_data.get('time') 1236 | new_time = event['time']['elapsed'] 1237 | 1238 | # Vérifier si c'est une vraie correction (petite différence de timing) ou un doublon (nouveau but) 1239 | # Si la différence est > 2 minutes, c'est probablement un nouveau but (doublon), pas une correction 1240 | time_difference = abs(new_time - old_time) if old_time is not None else 0 1241 | 1242 | if old_time is not None and old_time != new_time and not old_data.get('correction_sent', False): 1243 | if time_difference <= 2: 1244 | # Petite différence (≤2 min) = correction de timing du même but 1245 | player_name = old_data.get('player_name', 'Joueur inconnu') 1246 | team_name = old_data.get('team_name', 'Équipe inconnue') 1247 | message = f"⚠️ Correction: Le but de {player_name} ({team_name}) était à {new_time}' (et non {old_time}')" 1248 | await send_message_to_all_chats(message) 1249 | log_message(f"Correction de timing envoyée: {old_time}' → {new_time}' pour {player_name}") 1250 | 1251 | # Mettre à jour le timing et marquer correction envoyée 1252 | sent_events_details[event_key_sub]['time'] = new_time 1253 | sent_events_details[event_key_sub]['correction_sent'] = True 1254 | continue 1255 | else: 1256 | # Grande différence = nouveau but du même joueur (doublon/triplé) 1257 | log_message(f"Nouveau but détecté pour le même joueur (différence de {time_difference} min): {old_time}' vs {new_time}'") 1258 | # Ne pas continuer, traiter comme un nouveau but 1259 | else: 1260 | # Même timing ou correction déjà envoyée = ignorer 1261 | continue 1262 | 1263 | if event_key in sent_events: 1264 | continue 1265 | 1266 | if event['type'] == "Goal": 1267 | log_message(f"type == Goal") 1268 | log_message(f"Données de score récupéré dans match_data pour la variable new_score : {new_score}") 1269 | log_message(f"Previous score : {previous_score}") 1270 | log_message(f"Contenu de l'event de type goal :\n {event}\n\n") 1271 | 1272 | # Traiter le but avec la fonction helper 1273 | goal_info, should_update_score, is_first_event = await process_goal_event( 1274 | event, match_data, elapsed_time, current_score, previous_score, 1275 | is_first_event, IS_PAID_API, match_status 1276 | ) 1277 | 1278 | if goal_info: 1279 | goal_events.append(goal_info) 1280 | score_updated = should_update_score 1281 | else: 1282 | # Événement déjà traité ou non valide 1283 | sent_events.add(event_key) 1284 | continue 1285 | 1286 | # Gestion des buts annulés par le VAR (doit être au même niveau que Goal, pas imbriqué) 1287 | elif event['type'] == "Var" and "Goal Disallowed" in event['detail']: 1288 | log_message("But annulé détecté par le VAR") 1289 | team = event['team'] 1290 | new_score_var = { 1291 | 'home': match_data['goals']['home'], 1292 | 'away': match_data['goals']['away'] 1293 | } 1294 | # Vérifier si le score a diminué 1295 | if new_score_var['home'] < current_score['home'] or new_score_var['away'] < current_score['away']: 1296 | await send_goal_cancelled_message(current_score, new_score_var) 1297 | previous_score = current_score.copy() 1298 | current_score = new_score_var.copy() 1299 | else: 1300 | log_message("Le score n'a pas changé après l'annulation du but") 1301 | sent_events.add(event_key) 1302 | continue 1303 | 1304 | elif event['type'] == "Card" and event['detail'] == "Red Card": 1305 | log_message(f"Carton rouge détecté") 1306 | player = event['player'] 1307 | team = event['team'] 1308 | 1309 | log_message(f"Contenu de l'event de type carton rouge :\n {event}\n\n") 1310 | 1311 | # Vérifiez si le joueur n'est pas None et si son nom est présent 1312 | if player is not None and 'name' in player: 1313 | # Vérifiez si l'événement de carton rouge est dans les dernières minutes 1314 | current_elapsed_time = elapsed_time 1315 | red_card_elapsed_time = event['time']['elapsed'] 1316 | allowed_difference = -10 1317 | log_message(f"if {red_card_elapsed_time} >= {current_elapsed_time} + {allowed_difference}") 1318 | 1319 | if red_card_elapsed_time is not None and current_elapsed_time is not None and red_card_elapsed_time >= current_elapsed_time + allowed_difference: 1320 | await send_red_card_message(player, team, event['time']['elapsed'], event) 1321 | log_message(f"event_key enregistrée : {event_key}") 1322 | sent_events.add(event_key) 1323 | else: 1324 | log_message(f"Le carton rouge a été donné il y a plus de 10 minutes. Le message n'a pas été envoyé.") 1325 | else: 1326 | log_message(f"Le nom du joueur est manquant ou 'player' est None, le message de carton rouge n'a pas été envoyé") 1327 | continue 1328 | 1329 | #Fin de la boucle for event in events: 1330 | 1331 | # Traiter tous les buts accumulés 1332 | if goal_events: 1333 | # Vérifier si un des buts avait une augmentation significative AVANT de vider la liste 1334 | has_significant_increase = any(goal['significant_increase'] for goal in goal_events) 1335 | 1336 | for goal_info in goal_events: 1337 | if goal_info['event_key'] not in sent_events: 1338 | if goal_info['significant_increase']: 1339 | await send_goal_message_significant_increase_in_score( 1340 | goal_info['player'], 1341 | goal_info['team'], 1342 | goal_info['player_statistics'] if goal_info['player_statistics'] else [], 1343 | goal_info['elapsed_time'], 1344 | match_data, 1345 | goal_info['event'] 1346 | ) 1347 | else: 1348 | await send_goal_message( 1349 | goal_info['player'], 1350 | goal_info['team'], 1351 | goal_info['player_statistics'] if goal_info['player_statistics'] else [], 1352 | goal_info['elapsed_time'], 1353 | match_data, 1354 | goal_info['event'] 1355 | ) 1356 | sent_events.add(goal_info['event_key']) 1357 | 1358 | # ISSUE 6: Stocker les détails de l'événement pour détecter les corrections futures 1359 | # Utiliser une clé unique incluant le timing approximatif pour distinguer les doublés 1360 | event_key_sub = f"Goal_{goal_info['player']['id'] if goal_info['player'] else None}_{goal_info['elapsed_time']}" 1361 | sent_events_details[event_key_sub] = { 1362 | 'time': goal_info['elapsed_time'], 1363 | 'player_id': goal_info['player']['id'] if goal_info['player'] else None, 1364 | 'player_name': goal_info['player']['name'] if goal_info['player'] else 'Inconnu', 1365 | 'team_id': goal_info['team']['id'] if goal_info['team'] else None, 1366 | 'team_name': goal_info['team']['name'] if goal_info['team'] else 'Inconnue', 1367 | 'correction_sent': False 1368 | } 1369 | 1370 | await asyncio.sleep(1) # Petit délai entre les messages 1371 | 1372 | # Envoyer le score actualisé si plusieurs buts ont été marqués 1373 | if score_updated and has_significant_increase: 1374 | log_message(f"score_updated is true et augmentation significative détectée") 1375 | await updated_score(match_data) 1376 | 1377 | goal_events.clear() # Vider la liste après traitement 1378 | 1379 | if score_updated: 1380 | # Mettre à jour les scores 1381 | previous_score = current_score.copy() 1382 | log_message(f"previous_score mis à jour avec current_score.copy() pas encore mis à jour avec new_score : {previous_score}") 1383 | current_score = new_score.copy() 1384 | log_message(f"current_score mise à jour avec new_score.copy() : {current_score}") 1385 | score_updated = False 1386 | 1387 | # Vérifier si un goal a été annulé 1388 | if current_score['home'] < previous_score['home'] or current_score['away'] < previous_score['away']: 1389 | log_message(f"Données previous_score : {previous_score} et current_score : {current_score} avant la condition if current_score['home'] < previous_score['home'] or...") 1390 | log_message("Un goal a été annulé.") 1391 | await send_goal_cancelled_message(previous_score, current_score) 1392 | previous_score = current_score.copy() 1393 | 1394 | # Si le match est terminé ou s'est terminé en prolongation, envoyez le message de fin et arrêtez de vérifier les événements 1395 | if match_status in ['FT', 'AET', 'PEN']: 1396 | log_message(f"Le match est terminé, status : {match_status}\n") 1397 | 1398 | # Avant le bloc de conditions 1399 | home_team = None 1400 | away_team = None 1401 | home_score = None 1402 | away_score = None 1403 | 1404 | if match_data is None: 1405 | log_message("match_data est None, impossible de continuer le traitement des événements") 1406 | elif 'teams' not in match_data or 'home' not in match_data['teams'] or 'name' not in match_data['teams']['home']: 1407 | log_message("Certaines informations d'équipe manquent dans match_data") 1408 | elif 'score' not in match_data or 'fulltime' not in match_data['score'] or 'home' not in match_data['score']['fulltime']: 1409 | log_message("Certaines informations de score manquent dans match_data") 1410 | else: 1411 | home_team = match_data['teams']['home']['name'] 1412 | away_team = match_data['teams']['away']['name'] 1413 | home_score = match_data['score']['fulltime']['home'] 1414 | away_score = match_data['score']['fulltime']['away'] 1415 | 1416 | log_message(f"Envoi des variables à send_end_message avec chat_ids: home_team: {home_team}, away_team: {away_team}, home_score: {home_score}, away_score: {away_score}, match_statistics: {match_statistics}, events: {events}\n") 1417 | await send_end_message(home_team, away_team, home_score, away_score, match_statistics, events) 1418 | break 1419 | 1420 | # Si le nombre d'appels à l'API restant est dépassé, on lève une exception et on sort de la boucle ! 1421 | except RateLimitExceededError as e: 1422 | log_message(f"Erreur : {e}") 1423 | # Propagez l'exception pour sortir de la boucle 1424 | raise e 1425 | 1426 | # Pause avant de vérifier à nouveau les événements 1427 | await asyncio.sleep(interval) 1428 | 1429 | # Fonction pour découper un message selon les limites de la plateforme 1430 | def split_message_by_platform(message, platform="telegram"): 1431 | """ 1432 | Découpe un message selon les limites de caractères de la plateforme 1433 | - Discord: 2000 caractères max 1434 | - Telegram: 4096 caractères max 1435 | 1436 | Retourne une liste de messages découpés intelligemment 1437 | """ 1438 | if platform.lower() == "discord": 1439 | max_length = 2000 1440 | elif platform.lower() == "telegram": 1441 | max_length = 4096 1442 | else: 1443 | max_length = 4096 # Par défaut Telegram 1444 | 1445 | # Si le message est plus court que la limite, retourner tel quel 1446 | if len(message) <= max_length: 1447 | return [message] 1448 | 1449 | # Découper intelligemment le message 1450 | messages = [] 1451 | current_message = "" 1452 | 1453 | # Diviser par paragraphes (sauts de ligne doubles) 1454 | paragraphs = message.split("\n\n") 1455 | 1456 | for paragraph in paragraphs: 1457 | # Si un paragraphe seul dépasse la limite, le découper par lignes 1458 | if len(paragraph) > max_length: 1459 | lines = paragraph.split("\n") 1460 | for line in lines: 1461 | if len(current_message) + len(line) + 2 <= max_length: 1462 | current_message += line + "\n" 1463 | else: 1464 | if current_message: 1465 | messages.append(current_message.strip()) 1466 | current_message = line + "\n" 1467 | else: 1468 | # Ajouter le paragraphe au message courant 1469 | if len(current_message) + len(paragraph) + 4 <= max_length: 1470 | current_message += paragraph + "\n\n" 1471 | else: 1472 | if current_message: 1473 | messages.append(current_message.strip()) 1474 | current_message = paragraph + "\n\n" 1475 | 1476 | # Ajouter le dernier message 1477 | if current_message: 1478 | messages.append(current_message.strip()) 1479 | 1480 | # Ajouter des indicateurs de partie (1/3, 2/3, etc.) si plusieurs messages 1481 | if len(messages) > 1: 1482 | formatted_messages = [] 1483 | for i, msg in enumerate(messages, 1): 1484 | indicator = f"\n\n[Partie {i}/{len(messages)}]" 1485 | if len(msg) + len(indicator) <= max_length: 1486 | formatted_messages.append(msg + indicator) 1487 | else: 1488 | formatted_messages.append(msg) 1489 | return formatted_messages 1490 | 1491 | return messages 1492 | 1493 | # Cette fonction reçoit un message, puis envoie le message à chaque chat_id 1494 | async def send_message_to_all_chats(message, language=LANGUAGE): 1495 | log_message("send_message_to_all_chats() appelée.") 1496 | 1497 | # Traduction du message si la langue n'est pas le français en faisant appel à la fonction spévifique utilisant gpt3.5 1498 | if language.lower() != "french": 1499 | log_message(f"Traduction du message car la langue détectée est {language}.") 1500 | message = await translate_message(message, language) 1501 | 1502 | log_message(f"Contenu du message envoyé : {message}") 1503 | 1504 | # Pour Telegram: 1505 | if USE_TELEGRAM: 1506 | log_message("Lecture des IDs de chat enregistrés pour Telegram...") 1507 | try: 1508 | with open("telegram_chat_ids.json", "r") as file: 1509 | chat_ids = json.load(file) 1510 | log_message(f"Chat IDs chargés depuis le fichier telegram_chat_ids.json : {chat_ids}") 1511 | 1512 | # Découper le message selon les limites de Telegram (4096 caractères) 1513 | message_parts = split_message_by_platform(message, "telegram") 1514 | log_message(f"Message découpé en {len(message_parts)} partie(s) pour Telegram") 1515 | 1516 | for chat_id in chat_ids: 1517 | try: 1518 | for part in message_parts: 1519 | # Utiliser Markdown legacy pour compatibilité avec le formatage Discord 1520 | await bot.send_message(chat_id=chat_id, text=part, parse_mode="Markdown") 1521 | await asyncio.sleep(0.5) # Délai entre les messages pour éviter le rate limiting 1522 | except TelegramForbiddenError: 1523 | # Évite de log si le bot a été bloqué par des utilisateurs 1524 | continue 1525 | except TelegramBadRequest as e: 1526 | log_message(f"Erreur lors de l'envoi du message à Telegram (BadRequest) : {e}") 1527 | except ClientConnectorError as e: 1528 | log_message(f"Erreur lors de l'envoi du message à Telegram (ClientConnectorError) : {e}") 1529 | except TelegramNetworkError as e: 1530 | log_message(f"Erreur lors de l'envoi du message à Telegram (NetworkError) : {e}") 1531 | except TelegramAPIError as e: 1532 | if "user is deactivated" not in str(e).lower(): 1533 | log_message(f"Erreur lors de l'envoi du message à Telegram : {e}") 1534 | except Exception as e: 1535 | log_message(f"Erreur inattendue lors de l'envoi du message à Telegram : {e}") 1536 | except FileNotFoundError: 1537 | log_message("Fichier telegram_chat_ids.json non trouvé") 1538 | except json.JSONDecodeError: 1539 | log_message("Erreur de décodage JSON dans telegram_chat_ids.json") 1540 | except Exception as e: 1541 | log_message(f"Erreur lors de la lecture des IDs Telegram: {e}") 1542 | 1543 | # Pour Discord: 1544 | if USE_DISCORD: 1545 | log_message("Lecture des IDs de channel pour Discord...") 1546 | 1547 | # Utilisez le chemin correct pour le fichier discord_channels.json 1548 | try: 1549 | if os.path.exists(discord_channels_path): 1550 | with open(discord_channels_path, "r") as file: 1551 | channels = json.load(file) 1552 | 1553 | # Découper le message selon les limites de Discord (2000 caractères) 1554 | message_parts = split_message_by_platform(message, "discord") 1555 | log_message(f"Message découpé en {len(message_parts)} partie(s) pour Discord") 1556 | 1557 | for channel_id in channels: 1558 | channel = bot_discord.get_channel(channel_id) 1559 | if channel and isinstance(channel, (discord.TextChannel, discord.VoiceChannel)): 1560 | try: 1561 | for part in message_parts: 1562 | await channel.send(part) 1563 | await asyncio.sleep(0.5) # Délai entre les messages pour éviter le rate limiting 1564 | except discord.Forbidden as e: 1565 | # Évite de log si le bot a été bloqué par des utilisateurs, concerne aussi d'autres problèmes de permission... 1566 | continue 1567 | except discord.NotFound as e: 1568 | log_message(f"Erreur: Canal Discord {channel_id} non trouvé : {e}") 1569 | except discord.HTTPException as e: 1570 | log_message(f"Erreur HTTP lors de l'envoi du message au canal Discord {channel_id}: {e}") 1571 | except discord.ClientException as e: 1572 | log_message(f"Erreur: Argument invalide pour le canal Discord {channel_id}: {e}") 1573 | except Exception as e: 1574 | log_message(f"Erreur inattendue lors de l'envoi du message à Discord : {e}") 1575 | else: 1576 | log_message("Erreur: Le fichier discord_channels.json n'a pas été trouvé.") 1577 | except json.JSONDecodeError: 1578 | log_message("Erreur de décodage JSON dans discord_channels.json") 1579 | except Exception as e: 1580 | log_message(f"Erreur lors de la lecture des IDs Discord: {e}") 1581 | 1582 | # Envoie un message lorsqu'un match est détecté le jour même 1583 | async def send_match_today_message(match_start_time, fixture_id, current_league_id, teams, league, round_info, venue, city): 1584 | log_message("send_match_today_message() appelée.") 1585 | # Appeler l'API ChatGPT 1586 | chatgpt_analysis = await call_chatgpt_api_matchtoday(match_start_time, teams, league, round_info, venue, city) 1587 | message = f"🤖 : {chatgpt_analysis}" 1588 | 1589 | # Envoyer le message du match à tous les chats. 1590 | await send_message_to_all_chats(message) 1591 | 1592 | # Envoie un message de début de match aux utilisateurs avec des informations sur le match, les compositions des équipes. 1593 | async def send_compo_message(match_data, predictions=None, fixture_id=None, teams=None, league=None, round_info=None, venue=None, city=None): 1594 | log_message("send_compo_message() appelée.") 1595 | log_message(f"Informations reçues par l'API : match_data={match_data}, predictions={predictions}") 1596 | 1597 | if match_data is None: 1598 | log_message("Erreur : match_data est None dans send_compo_message") 1599 | message = "🤖 : Désolé, je n'ai pas pu obtenir les informations sur la composition des équipes pour le moment." 1600 | chatgpt_analysis = None 1601 | else: 1602 | # Appeler l'API ChatGPT 1603 | chatgpt_analysis = await call_chatgpt_api_compomatch(match_data, predictions) 1604 | message = "🤖 : " + chatgpt_analysis 1605 | 1606 | # Sauvegarder l'analyse pré-match (compositions) dans l'historique 1607 | if fixture_id and chatgpt_analysis: 1608 | match_info = { 1609 | "date": datetime.datetime.now().isoformat(), 1610 | "league": league if league else "Unknown", 1611 | "round": round_info if round_info else "Unknown", 1612 | "teams": teams if teams else {}, 1613 | "score": {}, 1614 | "venue": venue if venue else "Unknown", 1615 | "city": city if city else "Unknown" 1616 | } 1617 | save_match_analysis(fixture_id, match_info, chatgpt_analysis) 1618 | 1619 | # Envoyer le message du match à tous les chats. 1620 | await send_message_to_all_chats(message) 1621 | 1622 | # Envoie un message de début de match aux utilisateurs avec des informations sur le match, les compositions des équipes. 1623 | async def send_start_message(): 1624 | log_message("send_start_message() appelée.") 1625 | if IS_PAID_API: 1626 | message = f"🤖 : Le match commence !" 1627 | # Envoyer le message du match à tous les chats. 1628 | await send_message_to_all_chats(message) 1629 | 1630 | # Envoie un message aux utilisateurs pour informer d'un but marqué lors du match en cours, y compris les informations sur le joueur, l'équipe et les statistiques. 1631 | async def send_goal_message(player, team, player_statistics, elapsed_time, match_data, event): 1632 | log_message("send_goal_message() appelée.") 1633 | #log_message(f"Player: {player}") 1634 | #log_message(f"Team: {team}") 1635 | #log_message(f"Player statistics: {player_statistics}") 1636 | log_message(f"Minute du match pour le goal : {elapsed_time}") 1637 | home_score = match_data['goals']['home'] 1638 | away_score = match_data['goals']['away'] 1639 | # Utilisez team['name'] pour obtenir uniquement le nom de l'équipe 1640 | message = f"⚽️ {elapsed_time}' - {team['name']}\n {match_data['teams']['home']['name']} {home_score} - {away_score} {match_data['teams']['away']['name']}\n\n" 1641 | # Pour passer le score à l'api de chatgpt 1642 | score_string = f"{match_data['teams']['home']['name']} {home_score} - {away_score} {match_data['teams']['away']['name']}" 1643 | # Appeler l'API ChatGPT 1644 | chatgpt_analysis = await call_chatgpt_api_goalmatch(player, team, player_statistics, elapsed_time, event, score_string) 1645 | message += "🤖 Infos sur le but :\n" + chatgpt_analysis 1646 | await send_message_to_all_chats(message) 1647 | 1648 | # Envoie un message aux utilisateurs pour informer d'un but marqué lors du match en cours SANS LE SCORE !, y compris les informations sur le joueur, l'équipe et les statistiques. 1649 | async def send_goal_message_significant_increase_in_score(player, team, player_statistics, elapsed_time, match_data, event): 1650 | log_message("send_goal_message_significant_increase_in_score() appelée.") 1651 | #log_message(f"Player: {player}") 1652 | #log_message(f"Team: {team}") 1653 | #log_message(f"Player statistics: {player_statistics}") 1654 | log_message(f"Minute du match pour le goal : {elapsed_time}") 1655 | home_score = match_data['goals']['home'] 1656 | away_score = match_data['goals']['away'] 1657 | # Utilisez team['name'] pour obtenir uniquement le nom de l'équipe 1658 | message = f"⚽️ {elapsed_time}' - {team['name']}\n\n" 1659 | # Pour passer le score à l'api de chatgpt 1660 | score_string = f"{match_data['teams']['home']['name']} {home_score} - {away_score} {match_data['teams']['away']['name']}" 1661 | # Appeler l'API ChatGPT 1662 | chatgpt_analysis = await call_chatgpt_api_goalmatch(player, team, player_statistics, elapsed_time, event, score_string) 1663 | message += "🤖 Infos sur le but :\n" + chatgpt_analysis 1664 | await send_message_to_all_chats(message) 1665 | 1666 | # Envoie un message aux utilisateurs pour informer d'un but marqué lors de la séance au tir aux but 1667 | async def send_shootout_goal_message(player, team, player_statistics, event): 1668 | log_message("send_shootout_goal_message() appelée.") 1669 | #log_message(f"Player: {player}") 1670 | #log_message(f"Team: {team}") 1671 | #log_message(f"Player statistics: {player_statistics}") 1672 | # Utilisez team['name'] pour obtenir uniquement le nom de l'équipe 1673 | message = f"⚽️ Pénalty réussi' - {team['name']}\n\n" 1674 | # Appeler l'API ChatGPT 1675 | chatgpt_analysis = await call_chatgpt_api_shootout_goal_match(player, team, player_statistics, event) 1676 | message += "🤖 Infos sur le pénalty :\n" + chatgpt_analysis 1677 | await send_message_to_all_chats(message) 1678 | 1679 | # Envoie juste le score du match si plusieurs buts marqués dans le même intervalle 1680 | async def updated_score(match_data): 1681 | log_message("updated_score() appelée.") 1682 | home_score = match_data['goals']['home'] 1683 | away_score = match_data['goals']['away'] 1684 | score_string = f"{match_data['teams']['home']['name']} {home_score} - {away_score} {match_data['teams']['away']['name']}" 1685 | message = f"🤖 : Score actualisé après les buts : {score_string}" 1686 | await send_message_to_all_chats(message) 1687 | 1688 | # Envoie un message si un but est annulé 1689 | async def send_goal_cancelled_message(previous_score, current_score): 1690 | log_message("send_goal_cancelled_message() appelée.") 1691 | message = f"❌ But annulé ! Le score revient à {current_score['home']} - {current_score['away']}." 1692 | await send_message_to_all_chats(message) 1693 | 1694 | # Envoie un message aux utilisateurs pour informer d'un carton rouge lors du match en cours, y compris les informations sur le joueur et l'équipe. 1695 | async def send_red_card_message(player, team, elapsed_time, event): 1696 | log_message("send_red_card_message() appelée.") 1697 | message = f"🟥 Carton rouge ! {elapsed_time}'\n ({team['name']})\n\n" 1698 | # Appeler l'API ChatGPT 1699 | chatgpt_analysis = await call_chatgpt_api_redmatch(player, team, elapsed_time, event) 1700 | message += "🤖 Infos sur le carton rouge :\n" + chatgpt_analysis 1701 | await send_message_to_all_chats(message) 1702 | 1703 | # Envoie un message aux utilisateurs pour informer qu'un pénalty a été manqué pendant le match 1704 | async def send_missed_penalty_message(player, team, elapsed_time): 1705 | log_message("send_missed_penalty_message() appelée.") 1706 | message = f"❌ Pénalty manqué ! {elapsed_time}'\n ({team['name']})\n\n" 1707 | message += f"🤖 : {player['name']} a manqué son pénalty à la {elapsed_time}ème minute." 1708 | await send_message_to_all_chats(message) 1709 | 1710 | # Envoie un message aux utilisateurs pour informer que le suivi est mis en pause pour les tirs aux but qu'un résumé du match sera envoyé à la fin du match 1711 | async def pause_for_penalty_shootout(): 1712 | log_message("pause_for_penalty_shootout appelée") 1713 | message = "🤖 : Le suivi est mis en pause pour les tirs aux but mais je vous enverrai un résumé du match à la fin.\n" 1714 | await send_message_to_all_chats(message) 1715 | 1716 | # Envoie un message aux utilisateurs pour informer que le match a été interrompu 1717 | async def notify_match_interruption(): 1718 | log_message("notify_match_interruption appelée") 1719 | message = "🤖 : Le match a été interrompu !\n" 1720 | await send_message_to_all_chats(message) 1721 | 1722 | # Envoie un message aux utilisateurs pour informer qu'on a atteint le maximum de call à l'api et qu'on doit stopper le suivi du match 1723 | async def notify_users_max_api_requests_reached(): 1724 | log_message("notify_users_max_api_requests_reached appelée") 1725 | message = "🤖 : Le nombre maximum de requêtes à l'api de foot a été atteinte. Je dois malheureusement mettre fin au suivi du match.\n" 1726 | await send_message_to_all_chats(message) 1727 | 1728 | # Fonction pour formater les événements bruts en cas d'indisponibilité de l'API Poe 1729 | def format_raw_events(events, home_team, away_team): 1730 | """Formate les événements bruts de l'API football en cas d'indisponibilité de l'API Poe""" 1731 | if not events: 1732 | return "Aucun événement enregistré." 1733 | 1734 | formatted = "📋 ÉVÉNEMENTS DU MATCH:\n" 1735 | for event in events: 1736 | try: 1737 | time_elapsed = event.get('time', {}).get('elapsed', '?') 1738 | team_name = event.get('team', {}).get('name', 'Unknown') 1739 | player_name = event.get('player', {}).get('name', 'Unknown') 1740 | event_type = event.get('type', 'Unknown') 1741 | event_detail = event.get('detail', '') 1742 | 1743 | # Formater l'événement de manière lisible 1744 | if event_type == "Goal": 1745 | formatted += f"⚽️ {time_elapsed}' - {team_name}: {player_name} marque" 1746 | if event_detail and event_detail != "Normal Goal": 1747 | formatted += f" ({event_detail})" 1748 | formatted += "\n" 1749 | elif event_type == "Card": 1750 | if event_detail == "Red Card": 1751 | formatted += f"🟥 {time_elapsed}' - {team_name}: {player_name} carton rouge\n" 1752 | elif event_detail == "Yellow Card": 1753 | formatted += f"🟨 {time_elapsed}' - {team_name}: {player_name} carton jaune\n" 1754 | elif event_type == "Substitution": 1755 | formatted += f"🔄 {time_elapsed}' - {team_name}: {player_name} remplacé\n" 1756 | elif event_type == "Var": 1757 | formatted += f"📺 {time_elapsed}' - VAR: {event_detail}\n" 1758 | except Exception as e: 1759 | log_message(f"Erreur lors du formatage d'un événement : {e}") 1760 | continue 1761 | 1762 | return formatted 1763 | 1764 | # Envoie un message de fin de match aux utilisateurs avec le score final. 1765 | async def send_end_message(home_team, away_team, home_score, away_score, match_statistics, events): 1766 | log_message("send_end_message() appelée.") 1767 | message = f"🏁 Fin du match !\n{home_team} {home_score} - {away_score} {away_team}\n\n" 1768 | 1769 | # Appeler l'API ChatGPT et ajouter la réponse à la suite des statistiques du match 1770 | chatgpt_analysis = await call_chatgpt_api_endmatch(match_statistics, events, home_team, home_score, away_score, away_team) 1771 | 1772 | # Vérifier si l'analyse est un message d'erreur (commence par "🤖 :") 1773 | if chatgpt_analysis.startswith("🤖 :"): 1774 | log_message("API Poe indisponible, envoi des événements bruts à la place") 1775 | message += "⚠️ Analyse IA indisponible, voici les événements du match :\n\n" 1776 | message += format_raw_events(events, home_team, away_team) 1777 | 1778 | # Ajouter les statistiques si disponibles 1779 | if match_statistics and len(match_statistics) >= 2: 1780 | message += "\n📊 STATISTIQUES:\n" 1781 | try: 1782 | for home_stat, away_stat in zip(match_statistics[0].get('statistics', []), match_statistics[1].get('statistics', [])): 1783 | if 'type' in home_stat and 'value' in home_stat: 1784 | message += f"• {home_stat['type']}: {home_stat['value']} - {away_stat.get('value', '?')}\n" 1785 | except Exception as e: 1786 | log_message(f"Erreur lors du formatage des statistiques : {e}") 1787 | else: 1788 | message += "🤖 Mon analyse :\n" + chatgpt_analysis 1789 | 1790 | # Sauvegarder l'analyse post-match dans l'historique 1791 | try: 1792 | data = load_match_history() 1793 | if data.get("matches"): 1794 | # Mettre à jour le dernier match avec l'analyse post-match 1795 | last_match = data["matches"][-1] 1796 | last_match["score"] = { 1797 | "home": home_score, 1798 | "away": away_score 1799 | } 1800 | last_match["post_match_analysis"] = chatgpt_analysis 1801 | save_match_history(data) 1802 | log_message(f"Analyse post-match sauvegardée pour le match {last_match.get('fixture_id')}") 1803 | except Exception as e: 1804 | log_message(f"Erreur lors de la sauvegarde de l'analyse post-match : {e}") 1805 | 1806 | await send_message_to_all_chats(message) 1807 | 1808 | # Afficher le résumé des coûts à la fin du match 1809 | log_cost_summary() 1810 | 1811 | # DEBUT DE CODE POUR CONFIGURATION IA 1812 | 1813 | # Fonction pour traduire les messages dans la langue désirée 1814 | async def translate_message(message, language): 1815 | headers = { 1816 | "Content-Type": "application/json", 1817 | "Authorization": f"Bearer {API_KEY}" 1818 | } 1819 | 1820 | log_message(f"La langue détectée n'est pas le français donc on lance la traduction") 1821 | translation_prompt = f"Translate the following sentence from french to {language}: {message}" 1822 | translation_data = { 1823 | "model": GPT_MODEL_NAME_TRANSLATION, 1824 | "messages": [{"role": "user", "content": translation_prompt}], 1825 | "max_tokens": 2000 1826 | } 1827 | 1828 | async with httpx.AsyncClient() as client: 1829 | try: 1830 | translation_response = await client.post("https://api.poe.com/v1/chat/completions", headers=headers, json=translation_data, timeout=60.0) 1831 | translation_response.raise_for_status() 1832 | response_data = translation_response.json() 1833 | translated_message = response_data["choices"][0]["message"]["content"].strip() 1834 | 1835 | # Tracker les tokens et coûts si disponibles 1836 | if ENABLE_COST_TRACKING and "usage" in response_data: 1837 | input_tokens = response_data["usage"].get("prompt_tokens", 0) 1838 | output_tokens = response_data["usage"].get("completion_tokens", 0) 1839 | track_api_cost(input_tokens, output_tokens, "translate_message") 1840 | 1841 | return translated_message 1842 | except httpx.HTTPError as e: 1843 | log_message(f"Error during message translation with the Poe API: {e}") 1844 | return f"🤖 : Sorry, an error occurred while communicating with the translation API." 1845 | except Exception as e: 1846 | log_message(f"Unexpected error during message translation: {e}") 1847 | return f"🤖 : Sorry, an unexpected error occurred during message translation." 1848 | 1849 | # Fonction générique pour appeler l'API ChatGPT avec retry 1850 | async def call_chatgpt_api(data, max_retries=3): 1851 | headers = { 1852 | "Content-Type": "application/json", 1853 | "Authorization": f"Bearer {API_KEY}" 1854 | } 1855 | 1856 | for attempt in range(max_retries): 1857 | try: 1858 | async with httpx.AsyncClient(timeout=60.0) as client: 1859 | # Appel initial à Grok-4-Fast-Reasoning pour obtenir le message 1860 | response_json = await client.post("https://api.poe.com/v1/chat/completions", headers=headers, json=data) 1861 | response_json.raise_for_status() 1862 | response_data = response_json.json() 1863 | 1864 | # Vérifier que la réponse contient les données attendues 1865 | if "choices" not in response_data or not response_data["choices"]: 1866 | log_message(f"Réponse API invalide (pas de choices) : {response_data}") 1867 | if attempt < max_retries - 1: 1868 | await asyncio.sleep(2 ** attempt) # Backoff exponentiel 1869 | continue 1870 | return f"🤖 : Désolé, l'API a retourné une réponse invalide." 1871 | 1872 | message = response_data["choices"][0]["message"]["content"].strip() 1873 | 1874 | # Tracker les tokens et coûts si disponibles 1875 | if ENABLE_COST_TRACKING and "usage" in response_data: 1876 | input_tokens = response_data["usage"].get("prompt_tokens", 0) 1877 | output_tokens = response_data["usage"].get("completion_tokens", 0) 1878 | track_api_cost(input_tokens, output_tokens, f"call_chatgpt_api({data.get('model', 'unknown')})") 1879 | 1880 | log_message(f"Succès de la récupération de la réponse {data.get('model', 'unknown')}") 1881 | return message 1882 | 1883 | except httpx.TimeoutException as e: 1884 | log_message(f"Timeout lors de l'appel à l'API Poe (tentative {attempt + 1}/{max_retries}) : {e}") 1885 | if attempt < max_retries - 1: 1886 | await asyncio.sleep(2 ** attempt) # Backoff exponentiel 1887 | continue 1888 | return f"🤖 : Désolé, l'API Poe ne répond pas (timeout). Veuillez réessayer plus tard." 1889 | 1890 | except httpx.HTTPStatusError as e: 1891 | status_code = e.response.status_code 1892 | log_message(f"Erreur HTTP {status_code} lors de l'appel à l'API Poe (tentative {attempt + 1}/{max_retries}) : {e}") 1893 | 1894 | # Gestion spécifique des codes d'erreur 1895 | if status_code == 401: 1896 | log_message("Erreur d'authentification : Vérifiez votre clé API Poe") 1897 | return f"🤖 : Erreur d'authentification API. Vérifiez votre clé API." 1898 | elif status_code == 429: 1899 | log_message("Rate limit atteint, attente avant retry...") 1900 | if attempt < max_retries - 1: 1901 | await asyncio.sleep(5 * (2 ** attempt)) # Backoff plus long pour rate limit 1902 | continue 1903 | return f"🤖 : Trop de requêtes. Veuillez réessayer dans quelques instants." 1904 | elif status_code >= 500: 1905 | log_message(f"Erreur serveur {status_code}, retry...") 1906 | if attempt < max_retries - 1: 1907 | await asyncio.sleep(2 ** attempt) 1908 | continue 1909 | return f"🤖 : L'API Poe rencontre des problèmes. Veuillez réessayer plus tard." 1910 | else: 1911 | log_message(f"Erreur HTTP {status_code}") 1912 | if attempt < max_retries - 1: 1913 | await asyncio.sleep(2 ** attempt) 1914 | continue 1915 | return f"🤖 : Erreur lors de la communication avec l'API Poe (code {status_code})." 1916 | 1917 | except httpx.NetworkError as e: 1918 | log_message(f"Erreur réseau lors de l'appel à l'API Poe (tentative {attempt + 1}/{max_retries}) : {e}") 1919 | if attempt < max_retries - 1: 1920 | await asyncio.sleep(2 ** attempt) 1921 | continue 1922 | return f"🤖 : Erreur réseau. Vérifiez votre connexion Internet." 1923 | 1924 | except Exception as e: 1925 | log_message(f"Erreur inattendue lors de l'appel à l'API Poe (tentative {attempt + 1}/{max_retries}) : {e}") 1926 | if attempt < max_retries - 1: 1927 | await asyncio.sleep(2 ** attempt) 1928 | continue 1929 | return f"🤖 : Désolé, une erreur inattendue s'est produite." 1930 | 1931 | # Si tous les retries ont échoué 1932 | log_message(f"Tous les {max_retries} tentatives ont échoué pour l'appel API") 1933 | return f"🤖 : Impossible de contacter l'API après {max_retries} tentatives. Veuillez réessayer plus tard." 1934 | 1935 | # Analyse pour l'heure de début du match 1936 | async def call_chatgpt_api_matchtoday(match_start_time, teams, league, round_info, venue, city): 1937 | log_message(f"Informations reçues par l'API : match_start_time={match_start_time}, teams={teams}, league={league}, round_info={round_info}, venue={venue}, city={city}") 1938 | 1939 | # Construire la saison complète (ex: "2025-2026" si SEASON_ID = "2025") 1940 | season_year = int(SEASON_ID) 1941 | current_season = f"{season_year}-{season_year + 1}" 1942 | 1943 | user_message = (f"SAISON ACTUELLE : {current_season}\n\n" 1944 | f"Les informations du match qui a lieu aujourd'hui sont les suivantes : \n" 1945 | f"Ligue actuelle : {league}\n" 1946 | f"Tour : {round_info}\n" 1947 | f"Équipes du match : {teams['home']} contre {teams['away']}\n" 1948 | f"Stade et ville du stade : {venue}, {city}\n" 1949 | f"Heure de début : {match_start_time}\n" 1950 | f"L'heure actuelle est : {datetime.datetime.now()}\n" 1951 | f"Équipe analysée : {TEAM_NAME}") 1952 | system_prompt = (f"Tu es un journaliste sportif expert spécialisé dans l'analyse de matchs de football. " 1953 | f"IMPORTANT : Nous sommes en saison {current_season}. " 1954 | f"Tu dois te baser UNIQUEMENT sur les informations fournies dans le message utilisateur. " 1955 | f"N'utilise JAMAIS tes connaissances sur les saisons antérieures à {current_season}. " 1956 | f"Fais une présentation simple et factuelle du match qui aura lieu aujourd'hui **en 3-4 phrases maximum** : " 1957 | f"annonce les équipes qui s'affrontent, la compétition, le lieu et l'heure. " 1958 | f"**Traduis les noms de villes dans la langue {LANGUAGE}** (ex: Geneva → Genève si french, Geneva → Genf si german, etc.). " 1959 | f"Reste général sans inventer de détails sur la forme des équipes ou les enjeux. " 1960 | f"Embellis la présentation avec des émojis pertinents. " 1961 | f"Sois concis, engageant et informatif. " 1962 | f"FORMATAGE : Utilise un formatage Markdown simple compatible avec Discord et Telegram (gras avec **texte**, italique avec *texte*, pas de titres avec # ni de formatage complexe).") 1963 | data = { 1964 | "model": GPT_MODEL_NAME, 1965 | "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}], 1966 | "max_tokens": 800 1967 | } 1968 | return await call_chatgpt_api(data) 1969 | 1970 | # Analyse de début de match avec des smileys 1971 | async def call_chatgpt_api_compomatch(match_data, predictions=None): 1972 | log_message(f"Informations reçues par l'API : match_data={match_data}, predictions={predictions}") 1973 | 1974 | # Construire la saison complète 1975 | season_year = int(SEASON_ID) 1976 | current_season = f"{season_year}-{season_year + 1}" 1977 | 1978 | user_message = f"SAISON ACTUELLE : {current_season}\n\n" 1979 | 1980 | if match_data is not None: 1981 | user_message += f"Voici les informations du match qui va commencer d'ici quelques minutes : {match_data}" 1982 | else: 1983 | user_message += "Aucune information sur le match n'est disponible pour le moment." 1984 | 1985 | if predictions: 1986 | user_message += f"\nPrédictions de l'issue du match : {predictions['winner']['name']} (Comment: {predictions['winner']['comment']})" 1987 | 1988 | # Ajouter l'historique des 5 derniers matchs pour enrichir le contexte 1989 | last_matches = get_last_n_matches(5) 1990 | if last_matches and len(last_matches) > 0: 1991 | match_history_context = format_match_history_for_context(last_matches) 1992 | user_message += f"\n\n{match_history_context}" 1993 | 1994 | system_prompt = (f"Tu es un journaliste sportif expert spécialisé dans l'analyse tactique de matchs de football. " 1995 | f"IMPORTANT : Nous sommes en saison {current_season}. " 1996 | f"Tu dois te baser UNIQUEMENT sur les informations fournies (compositions, formations, prédictions si disponibles, historique des matchs). " 1997 | f"N'utilise JAMAIS tes connaissances sur les saisons antérieures à {current_season}. " 1998 | f"\n\n" 1999 | f"**STRUCTURE OBLIGATOIRE DE TA RÉPONSE** (utilise des sauts de ligne entre chaque section) :\n" 2000 | f"1️⃣ **COMPOSITIONS** (2-3 phrases) : Analyse les formations et joueurs clés des deux équipes\n" 2001 | f"2️⃣ **CONTEXTE RÉCENT** (2-3 phrases) : Résume brièvement la forme récente basée sur l'historique fourni\n" 2002 | f"3️⃣ **PRONOSTIC** (1-2 phrases) : Donne ton pronostic basé sur les données (prédictions si disponibles)\n" 2003 | f"\n" 2004 | f"Utilise des émojis pertinents (⚽🛡️🔥📊) pour aérer. " 2005 | f"Sois concis, factuel et engageant. Chaque section doit être séparée par un saut de ligne.\n" 2006 | f"FORMATAGE : Utilise un formatage Markdown simple compatible avec Discord et Telegram (gras avec **texte**, italique avec *texte*, pas de titres avec # ni de formatage complexe).") 2007 | 2008 | data = { 2009 | "model": GPT_MODEL_NAME, 2010 | "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}], 2011 | "max_tokens": 1500 2012 | } 2013 | 2014 | return await call_chatgpt_api(data) 2015 | 2016 | # Commentaire sur le goal récent 2017 | async def call_chatgpt_api_goalmatch(player, team, player_statistics, elapsed_time, event, score_string): 2018 | log_message(f"Informations reçues par l'API : player={player}, team={team}, player_statistics={player_statistics}, elapsed_time={elapsed_time}, event={event}, score_string={score_string}") 2019 | user_message = f"Le joueur qui a marqué : {player} " 2020 | user_message += f"L'équipe pour laquelle le but a été comptabilisé : {team}" 2021 | if player_statistics: 2022 | user_message += f"Les statistiques du joueur pour ce match qui a marqué (IGNORE COMPLÈTEMENT le temps de jeu 'minutes' du joueur) : {player_statistics} " 2023 | user_message += f"La minute du match quand le goal a été marqué : {elapsed_time} " 2024 | user_message += f"Le score actuel après le but qui vient d'être marqué pour contextualisé ta réponse , mais ne met pas le score dans ta réponse : {score_string} " 2025 | user_message += f"Voici les détails de l'événement goal du match en cours {event}, utilise les informations pertinentes liées au goal marqué à la {elapsed_time} minute sans parler d'assist!" 2026 | 2027 | system_prompt = "Tu es un journaliste sportif spécialisé dans l'analyse de matchs de football, commente moi le goal le plus récent du match qui est en cours, tu ne dois pas faire plus de deux phrases courtes en te basant sur les informations que je te donne comme qui est le buteur et ses statistiques (si disponible). **INTERDIT ABSOLU de mentionner le temps de jeu du joueur (minutes jouées) car cette donnée est souvent incorrecte.** Concentre-toi sur le type de but, la position du joueur, et les statistiques de passes/tirs uniquement. FORMATAGE : Utilise un formatage Markdown simple compatible avec Discord et Telegram (gras avec **texte**, italique avec *texte*, pas de titres avec # ni de formatage complexe)." 2028 | 2029 | data = { 2030 | "model": GPT_MODEL_NAME, 2031 | "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}], 2032 | "max_tokens": 500 2033 | } 2034 | return await call_chatgpt_api(data) 2035 | 2036 | # Commentaire sur le goal lors de la séance de tir aux penaltys 2037 | async def call_chatgpt_api_shootout_goal_match(player, team, player_statistics, event): 2038 | log_message(f"Informations reçues par l'API : player={player}, team={team}, player_statistics={player_statistics}, event={event}") 2039 | user_message = f"Le joueur qui a marqué le pénalty lors de la séance aux tirs aux buts : {player} " 2040 | user_message += f"L'équipe pour laquelle le but a été comptabilisé : {team}" 2041 | if player_statistics: 2042 | user_message += f"Les statistiques du joueur pour ce match qui a marqué (n'utilise pas le temps de jeu du joueur): {player_statistics} " 2043 | user_message += f"Voici les détails de l'événement goal du match en cours {event}." 2044 | 2045 | system_prompt = "Tu es un journaliste sportif spécialisé dans l'analyse de matchs de football, commente moi le goal lors de cette séance aux tirs au but, tu ne dois pas faire plus de deux phrases courtes en te basant sur les informations que je te donne. FORMATAGE : Utilise un formatage Markdown simple compatible avec Discord et Telegram (gras avec **texte**, italique avec *texte*, pas de titres avec # ni de formatage complexe)." 2046 | 2047 | data = { 2048 | "model": GPT_MODEL_NAME, 2049 | "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}], 2050 | "max_tokens": 1000 2051 | } 2052 | return await call_chatgpt_api(data) 2053 | 2054 | # Commentaire sur le carton rouge 2055 | async def call_chatgpt_api_redmatch(player, team, elapsed_time, event): 2056 | log_message(f"Informations reçues par l'API : player={player}, team={team}, elapsed_time={elapsed_time}, event={event}") 2057 | user_message = (f"Le joueur qui a pris un carton rouge : {player} " 2058 | f"L'équipe dont il fait parti : {team} " 2059 | f"La minute du match à laquelle il a pris un carton rouge : {elapsed_time} " 2060 | f"Voici les détails de l'événement du carton rouge du match en cours {event}, utilise uniquement les informations pertinentes liées à ce carton rouge de la {elapsed_time} minute.") 2061 | system_prompt = "Tu es un journaliste sportif spécialisé dans l'analyse de matchs de football, commente moi ce carton rouge le plus récent du match qui est en cours, tu ne dois pas faire plus de deux phrases courtes en te basant sur les informations que je te donne. FORMATAGE : Utilise un formatage Markdown simple compatible avec Discord et Telegram (gras avec **texte**, italique avec *texte*, pas de titres avec # ni de formatage complexe)." 2062 | data = { 2063 | "model": GPT_MODEL_NAME, 2064 | "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}], 2065 | "max_tokens": 1000 2066 | } 2067 | return await call_chatgpt_api(data) 2068 | 2069 | # Analyse de fin de match 2070 | async def call_chatgpt_api_endmatch(match_statistics, events, home_team, home_score, away_score, away_team): 2071 | log_message(f"Informations reçues par l'API : match_statistics={match_statistics}, events={events}") 2072 | 2073 | # Construire la saison complète 2074 | season_year = int(SEASON_ID) 2075 | current_season = f"{season_year}-{season_year + 1}" 2076 | 2077 | # Récupérer l'analyse pré-match et l'historique des 5 derniers matchs 2078 | last_matches = get_last_n_matches(5) 2079 | match_history_context = format_match_history_for_context(last_matches) 2080 | 2081 | # Récupérer l'analyse pré-match du match actuel (le dernier match dans l'historique) 2082 | pre_match_analysis = "" 2083 | if last_matches and len(last_matches) > 0: 2084 | pre_match_analysis = last_matches[-1].get("pre_match_analysis", "") 2085 | if not pre_match_analysis: 2086 | pre_match_analysis = "" 2087 | 2088 | # Score final 2089 | user_message = f"SAISON ACTUELLE : {current_season}\n\n" 2090 | user_message += f"📊 Score Final:\n{home_team} {home_score} - {away_score} {away_team}\n\n" 2091 | 2092 | # Ajouter l'analyse pré-match pour contexte 2093 | if pre_match_analysis: 2094 | user_message += f"📋 CONTEXTE PRÉ-MATCH:\n{pre_match_analysis}\n\n" 2095 | 2096 | # Ajouter l'historique des matchs 2097 | user_message += f"{match_history_context}\n\n" 2098 | 2099 | # Formater les événements du match 2100 | formatted_events = ["📢 Événements du Match:"] 2101 | if events: 2102 | for event in events: 2103 | time_elapsed = event['time']['elapsed'] 2104 | time_extra = event['time']['extra'] 2105 | team_name = event['team']['name'] 2106 | player_name = event['player']['name'] 2107 | event_type = event['type'] 2108 | event_detail = event['detail'] 2109 | formatted_event = f"• À {time_elapsed}{'+' + str(time_extra) if time_extra else ''} min, {team_name} - {player_name} {event_detail} ({event_type})" 2110 | formatted_events.append(formatted_event) 2111 | user_message += '\n'.join(formatted_events) 2112 | 2113 | # Traitement des match_statistics 2114 | if len(match_statistics) >= 2 and 'statistics' in match_statistics[0] and 'statistics' in match_statistics[1]: 2115 | user_message += f"\n\n📉 Statistiques du Match:\n" 2116 | for home_stat, away_stat in zip(match_statistics[0]['statistics'], match_statistics[1]['statistics']): 2117 | if 'type' in home_stat and 'value' in home_stat and 'type' in away_stat and 'value' in away_stat: 2118 | user_message += f"• {home_stat['type']}: {home_stat['value']} - {away_stat['value']}\n" 2119 | 2120 | system_prompt = (f"Tu es un journaliste sportif expert spécialisé dans l'analyse de matchs de football. " 2121 | f"IMPORTANT : Nous sommes en saison {current_season}. " 2122 | f"Tu dois te baser UNIQUEMENT sur les informations fournies : contexte pré-match, historique des matchs fourni, " 2123 | f"score final, événements et statistiques du match. " 2124 | f"N'utilise JAMAIS tes connaissances sur les saisons antérieures à {current_season}. " 2125 | f"\n\n" 2126 | f"**STRUCTURE OBLIGATOIRE DE TA RÉPONSE** (utilise des sauts de ligne entre chaque section) :\n" 2127 | f"1️⃣ **RÉSULTAT & CONTEXTE** (2-3 phrases) : Compare le résultat aux attentes pré-match et à la forme récente\n" 2128 | f"2️⃣ **ANALYSE TACTIQUE** (2-3 phrases) : Formation, possession, style de jeu et statistiques clés\n" 2129 | f"3️⃣ **MOMENTS DÉCISIFS** (2-3 phrases) : Joueurs clés, buts, cartons et tournants du match\n" 2130 | f"4️⃣ **BILAN** (1-2 phrases) : Conclusion sur la performance globale du {TEAM_NAME}\n" 2131 | f"\n" 2132 | f"Utilise des émojis pertinents (⚽🛡️🔥📊⭐) pour aérer. " 2133 | f"Sois concis, factuel et engageant. Chaque section doit être séparée par un saut de ligne.\n" 2134 | f"FORMATAGE : Utilise un formatage Markdown simple compatible avec Discord et Telegram (gras avec **texte**, italique avec *texte*, pas de titres avec # ni de formatage complexe).") 2135 | 2136 | data = { 2137 | "model": GPT_MODEL_NAME, 2138 | "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}], 2139 | "max_tokens": 2000 2140 | } 2141 | 2142 | return await call_chatgpt_api(data) 2143 | 2144 | # FIN DU CODE DE CONFIGURATION IA 2145 | 2146 | # Fonction principale pour initialiser le bot, enregistrer les gestionnaires de messages et lancer la vérification périodique des matchs. 2147 | async def main(): 2148 | try: 2149 | log_message("fonction main executée") 2150 | 2151 | if USE_TELEGRAM: 2152 | log_message("Bot telegram lancé") 2153 | global bot 2154 | bot = Bot(token=TOKEN_TELEGRAM) 2155 | dp = Dispatcher() 2156 | initialize_chat_ids_file() 2157 | dp.message.register(on_start, Command("start")) 2158 | 2159 | # Démarrer le bot Telegram en tâche de fond 2160 | asyncio.create_task(dp.start_polling(bot)) 2161 | 2162 | if USE_DISCORD: 2163 | log_message("Bot Discord lancé") 2164 | # Lancez le bot Discord dans une nouvelle tâche 2165 | asyncio.create_task(run_discord_bot(TOKEN_DISCORD)) 2166 | 2167 | # Si au moins un des deux bots est activé, exécutez les tâches de vérification 2168 | if USE_TELEGRAM or USE_DISCORD: 2169 | # Check immediate puis vérification périodique 2170 | asyncio.create_task(check_matches()) 2171 | asyncio.create_task(check_match_periodically()) 2172 | 2173 | except Exception as e: 2174 | log_message(f"Erreur inattendue dans main(): {e}") 2175 | 2176 | # Boucle d'attente pour empêcher main() (donc le script) de se terminer 2177 | while is_running: 2178 | # Attente de 10 secondes avant de vérifier à nouveau 2179 | await asyncio.sleep(10) 2180 | 2181 | if __name__ == "__main__": 2182 | asyncio.run(main()) --------------------------------------------------------------------------------