diff --git a/.gitignore b/.gitignore index b9ff121..5df84e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ **/.venv __pycache__ instance +logs .tio.tokens.json diff --git a/Dockerfile b/Dockerfile index 7465a47..91cedc8 100755 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive ENV LANG=fr_FR.UTF-8 ENV LC_ALL=fr_FR.UTF-8 +ENV PYTHONUNBUFFERED=1 RUN apt-get update && apt-get install -y --no-install-recommends \ apt-utils \ @@ -34,7 +35,7 @@ RUN python3 -m venv /app/venv && \ chmod +x /start.sh && \ mkdir -p /app/logs -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ +HEALTHCHECK --interval=1s --timeout=10s --start-period=5s --retries=3 \ CMD pgrep python > /dev/null && ! (tail -n 1000 $(ls -t /app/logs/*.log 2>/dev/null | head -1) 2>/dev/null | grep -iE "(ERROR|CRITICAL|Exception|sqlite3\.OperationalError)") CMD ["/start.sh"] diff --git a/discordbot/__init__.py b/discordbot/__init__.py index 837161c..7503b42 100644 --- a/discordbot/__init__.py +++ b/discordbot/__init__.py @@ -44,10 +44,13 @@ class DiscordBot(discord.Client): return channels - def begin(self) : + def begin(self) : token = Configuration.query.filter_by(key='discord_token').first() if token : - self.run(token.value) + try: + self.run(token.value) + except Exception as e: + logging.error(f'Erreur fatale lors du démarrage du bot Discord : {e}') else : logging.error('Aucun token Discord configuré. Le bot ne peut pas être démarré') @@ -66,8 +69,10 @@ async def on_message(message: Message): commande = Commande.query.filter_by(discord_enable=True, trigger=command_name).first() if commande: try: - await message.channel.send(commande.response, suppress_embeds=True) + await asyncio.wait_for(message.channel.send(commande.response, suppress_embeds=True), timeout=30.0) return + except asyncio.TimeoutError: + logging.error(f'Timeout lors de l\'envoi de la commande Discord : {command_name}') except Exception as e: logging.error(f'Échec de l\'exécution de la commande Discord : {e}') @@ -89,7 +94,9 @@ async def on_message(message: Message): if (rest > 0): msg += f'- et encore {rest} autres jeux' try : - await message.channel.send(msg, suppress_embeds=True) + await asyncio.wait_for(message.channel.send(msg, suppress_embeds=True), timeout=30.0) + except asyncio.TimeoutError: + logging.error(f'Timeout lors de l\'envoi du message ProtonDB') except Exception as e: logging.error(f'Échec de l\'envoi du message ProtonDB : {e}') diff --git a/discordbot/humblebundle.py b/discordbot/humblebundle.py index 7c5ebe5..f2e7eca 100644 --- a/discordbot/humblebundle.py +++ b/discordbot/humblebundle.py @@ -1,3 +1,4 @@ +import asyncio import datetime import logging import json @@ -14,11 +15,18 @@ def _isEnable(): return helper.getValue('humble_bundle_enable') and helper.getIntValue('humble_bundle_channel') != 0 def _callGithub(): - response = requests.get("https://raw.githubusercontent.com/shionn/HumbleBundleGamePack/refs/heads/master/data/game-bundles.json") - if response.status_code == 200: - return response.json() - logging.error(f"Échec de la connexion à la ressource Humble Bundle. Code de statut HTTP : {response.status_code}") - return None + try: + response = requests.get("https://raw.githubusercontent.com/shionn/HumbleBundleGamePack/refs/heads/master/data/game-bundles.json", timeout=30) + if response.status_code == 200: + return response.json() + logging.error(f"Échec de la connexion à la ressource Humble Bundle. Code de statut HTTP : {response.status_code}") + return None + except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: + logging.error(f"Erreur de connexion à la ressource Humble Bundle : {e}") + return None + except Exception as e: + logging.error(f"Erreur inattendue lors de la récupération des bundles : {e}") + return None def _isNotAlreadyNotified(bundle): return GameBundle.query.filter_by(url=bundle['url']).first() == None @@ -46,9 +54,14 @@ async def checkHumbleBundleAndNotify(bot: Client): bundle = _findFirstNotNotified(bundles) if bundle != None : message = _formatMessage(bundle) - await bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message) - db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle))) - db.session.commit() + try: + await asyncio.wait_for(bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message), timeout=30.0) + db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle))) + db.session.commit() + except asyncio.TimeoutError: + logging.error(f'Timeout lors de l\'envoi du message Humble Bundle') + except Exception as send_error: + logging.error(f'Erreur lors de l\'envoi du message Humble Bundle : {send_error}') except Exception as e: logging.error(f"Échec de la vérification des offres Humble Bundle : {e}") else: diff --git a/protondb/__init__.py b/protondb/__init__.py index e2842bd..f40c153 100644 --- a/protondb/__init__.py +++ b/protondb/__init__.py @@ -8,24 +8,38 @@ from database.helpers import ConfigurationHelper from database.models import GameAlias from sqlalchemy import desc,func -def _call_algoliasearch(search_name:str): - config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'), - ConfigurationHelper().getValue('proton_db_api_key')) - config.set_default_hosts() - client = SearchClientSync(config=config) - return client.search_single_index(index_name="steamdb", - search_params={ - "query":search_name, - "facetFilters":[["appType:Game"]], - "hitsPerPage":50}, - request_options= {'headers':{'Referer':'https://www.protondb.com/'}}) +def _call_algoliasearch(search_name:str): + try: + config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'), + ConfigurationHelper().getValue('proton_db_api_key')) + config.set_default_hosts() + client = SearchClientSync(config=config) + return client.search_single_index(index_name="steamdb", + search_params={ + "query":search_name, + "facetFilters":[["appType:Game"]], + "hitsPerPage":50}, + request_options= { + 'headers':{'Referer':'https://www.protondb.com/'}, + 'timeout': 30 + }) + except Exception as e: + logging.error(f'Erreur lors de la recherche Algolia pour "{search_name}" : {e}') + return None def _call_summary(id): - response = requests.get(f'http://jazzy-starlight-aeea19.netlify.app/api/v1/reports/summaries/{id}.json') - if (response.status_code == 200) : - return response.json() - logging.error(f'Échec de la récupération des données ProtonDB pour le jeu {id}. Code de statut HTTP : {response.status_code}') - return None + try: + response = requests.get(f'http://jazzy-starlight-aeea19.netlify.app/api/v1/reports/summaries/{id}.json', timeout=30) + if (response.status_code == 200) : + return response.json() + logging.error(f'Échec de la récupération des données ProtonDB pour le jeu {id}. Code de statut HTTP : {response.status_code}') + return None + except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: + logging.error(f'Erreur de connexion ProtonDB pour le jeu {id} : {e}') + return None + except Exception as e: + logging.error(f'Erreur inattendue lors de la récupération ProtonDB pour le jeu {id} : {e}') + return None def _is_name_match(name:str, search_name:str) -> bool: normalized_game_name = re.sub("[^a-z0-9]", "", name.lower()) @@ -37,10 +51,12 @@ def _apply_game_aliases(search_name:str) -> str: search_name = re.sub(re.escape(alias.alias), alias.name, search_name, flags=re.IGNORECASE) return search_name -def searhProtonDb(search_name:str): +def searhProtonDb(search_name:str): results = [] search_name = _apply_game_aliases(search_name) responses = _call_algoliasearch(search_name) + if responses is None: + return results for hit in responses.model_dump().get('hits'): id = hit.get('object_id') name:str = hit.get('name') diff --git a/twitchbot/__init__.py b/twitchbot/__init__.py index 529bf0d..b394802 100644 --- a/twitchbot/__init__.py +++ b/twitchbot/__init__.py @@ -37,14 +37,16 @@ class TwitchBot() : if _isConfigured() : try : helper = ConfigurationHelper() - self.twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')) - await self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token')) - self.chat = await Chat(self.twitch) + self.twitch = await asyncio.wait_for(Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')), timeout=30.0) + await asyncio.wait_for(self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token')), timeout=30.0) + self.chat = await asyncio.wait_for(Chat(self.twitch), timeout=30.0) self.chat.register_event(ChatEvent.READY, _onReady) self.chat.register_event(ChatEvent.MESSAGE, _onMessage) # chat.register_event(ChatEvent.SUB, on_sub) self.chat.register_command('hello', _helloCommand) self.chat.start() + except asyncio.TimeoutError: + logging.error('Timeout lors de la connexion à Twitch. Vérifiez votre connexion réseau.') except Exception as e: logging.error(f'Échec de l\'authentification Twitch. Vérifiez vos identifiants et redémarrez après correction : {e}') else: diff --git a/twitchbot/live_alert.py b/twitchbot/live_alert.py index 726a130..f2b0a96 100644 --- a/twitchbot/live_alert.py +++ b/twitchbot/live_alert.py @@ -1,3 +1,4 @@ +import asyncio import logging from twitchAPI.twitch import Twitch @@ -36,14 +37,24 @@ async def _notifyAlert(alert : LiveAlert, stream : Stream): async def _sendMessage(channel : int, message : str) : logger.info(f'Envoi de notification : {message}') - await bot.get_channel(channel).send(message) - logger.info(f'Notification envoyé') + try: + await asyncio.wait_for(bot.get_channel(channel).send(message), timeout=30.0) + logger.info(f'Notification envoyée') + except asyncio.TimeoutError: + logger.error(f'Timeout lors de l\'envoi de notification live alert') + except Exception as e: + logger.error(f'Erreur lors de l\'envoi de notification live alert : {e}') async def _retreiveStreams(twitch: Twitch, alerts : list[LiveAlert]) -> list[Stream] : streams : list[Stream] = [] logger.info(f'Recherche de streams pour : {alerts}') - async for stream in twitch.get_streams(user_login = [alert.login for alert in alerts]): - streams.append(stream) - logger.info(f'Ces streams sont en ligne : {streams}') + try: + async for stream in asyncio.wait_for(twitch.get_streams(user_login = [alert.login for alert in alerts]), timeout=30.0): + streams.append(stream) + logger.info(f'Ces streams sont en ligne : {streams}') + except asyncio.TimeoutError: + logger.error('Timeout lors de la récupération des streams Twitch') + except Exception as e: + logger.error(f'Erreur lors de la récupération des streams Twitch : {e}') return streams diff --git a/webapp/twitch_auth.py b/webapp/twitch_auth.py index 4994f81..5442160 100644 --- a/webapp/twitch_auth.py +++ b/webapp/twitch_auth.py @@ -17,34 +17,53 @@ auth: UserAuthenticator def twitchConfigurationHelp(): return render_template("twitch-aide.html", token_redirect_url = _buildUrl()) -@webapp.route("/configurations/twitch/request-token") -async def twitchRequestToken(): +@webapp.route("/configurations/twitch/request-token") +async def twitchRequestToken(): global auth - helper = ConfigurationHelper() - twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')) - auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl()) - return redirect(auth.return_auth_url()) + try: + helper = ConfigurationHelper() + import asyncio + twitch = await asyncio.wait_for( + Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')), + timeout=30.0 + ) + auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl()) + return redirect(auth.return_auth_url()) + except asyncio.TimeoutError: + logging.error('Timeout lors de la connexion à Twitch API pour la demande de token') + return redirect(url_for('openConfigurations')) + except TwitchAPIException as e: + logging.error(f'Erreur API Twitch lors de la demande de token : {e}') + return redirect(url_for('openConfigurations')) + except Exception as e: + logging.error(f'Erreur inattendue lors de la demande de token Twitch : {e}') + return redirect(url_for('openConfigurations')) -@webapp.route("/configurations/twitch/receive-token") +@webapp.route("/configurations/twitch/receive-token") async def twitchReceiveToken(): global auth state = request.args.get('state') code = request.args.get('code') if state != auth.state : - logging('bad returned state') + logging.error('bad returned state') return redirect(url_for('openConfigurations')) if code == None : - logging('no returned state') + logging.error('no returned code') return redirect(url_for('openConfigurations')) - + try: - token, refresh = await auth.authenticate(user_token=code) + import asyncio + token, refresh = await asyncio.wait_for(auth.authenticate(user_token=code), timeout=30.0) helper = ConfigurationHelper() helper.createOrUpdate('twitch_access_token', token) helper.createOrUpdate('twitch_refresh_token', refresh) db.session.commit() + except asyncio.TimeoutError: + logging.error('Timeout lors de l\'authentification Twitch') except TwitchAPIException as e: - logging(e) + logging.error(f'Erreur API Twitch lors de l\'authentification : {e}') + except Exception as e: + logging.error(f'Erreur inattendue lors de l\'authentification Twitch : {e}') return redirect(url_for('openConfigurations')) # hack pas fou mais on estime qu'on sera toujours en ssl en connecté