From bcd75d100249f7d410acb9df753eb1cc7165aa81 Mon Sep 17 00:00:00 2001 From: Rico Date: Thu, 29 Jul 2021 22:15:07 +0200 Subject: [PATCH 1/5] feat: implement resetting of stats --- blackjackbot/__init__.py | 4 ++- blackjackbot/commands/util/__init__.py | 4 +-- blackjackbot/commands/util/commands.py | 34 ++++++++++++++++++- .../lang/strings/translations_en.json | 7 +++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/blackjackbot/__init__.py b/blackjackbot/__init__.py index be61da0..f82e84d 100644 --- a/blackjackbot/__init__.py +++ b/blackjackbot/__init__.py @@ -14,6 +14,7 @@ stop_command_handler = CommandHandler("stop", game.stop_cmd) language_command_handler = CommandHandler("language", settings.language_cmd) stats_command_handler = CommandHandler("stats", util.stats_cmd) +resetstats_command_handler = CommandHandler("resetstats", util.reset_stats_cmd) comment_command_handler = CommandHandler("comment", util.comment_cmd) comment_text_command_handler = MessageHandler(Filters.text & ~(Filters.forwarded | Filters.command), util.comment_text) @@ -33,12 +34,13 @@ start_callback_handler = CallbackQueryHandler(game.start_callback, pattern=r"^start_[0-9]{7}$") newgame_callback_handler = CallbackQueryHandler(game.newgame_callback, pattern=r"^newgame$") language_callback_handler = CallbackQueryHandler(settings.language_callback, pattern=r"^lang_([a-z]{2}(?:-[a-z]{2})?)$") +reset_stats_callback_handler = CallbackQueryHandler(util.reset_stats_callback, pattern=r"^reset_stats_(confirm|cancel)$") handlers = [banned_user_handler, start_command_handler, stop_command_handler, join_callback_handler, hit_callback_handler, stand_callback_handler, start_callback_handler, language_command_handler, stats_command_handler, newgame_callback_handler, reload_lang_command_handler, language_callback_handler, users_command_handler, comment_command_handler, comment_text_command_handler, answer_command_handler, ban_command_handler, - unban_command_handler, bans_command_handler] + unban_command_handler, bans_command_handler, resetstats_command_handler, reset_stats_callback_handler] __all__ = ['handlers', 'error_handler'] diff --git a/blackjackbot/commands/util/__init__.py b/blackjackbot/commands/util/__init__.py index d5afa54..f90b956 100644 --- a/blackjackbot/commands/util/__init__.py +++ b/blackjackbot/commands/util/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from .functions import remove_inline_keyboard, get_start_keyboard, generate_evaluation_string, html_mention, get_game_keyboard, get_join_keyboard from .decorators import admin_method, needs_active_game -from .commands import stats_cmd, comment_cmd, comment_text +from .commands import stats_cmd, comment_cmd, comment_text, reset_stats_cmd, reset_stats_callback __all__ = ['remove_inline_keyboard', 'get_start_keyboard', 'generate_evaluation_string', 'html_mention', 'get_game_keyboard', 'get_join_keyboard', - 'stats_cmd', 'comment_cmd', 'comment_text', 'admin_method', 'needs_active_game'] + 'stats_cmd', 'comment_cmd', 'comment_text', 'admin_method', 'needs_active_game', 'reset_stats_cmd', 'reset_stats_callback'] diff --git a/blackjackbot/commands/util/commands.py b/blackjackbot/commands/util/commands.py index 8536c90..566e9e2 100644 --- a/blackjackbot/commands/util/commands.py +++ b/blackjackbot/commands/util/commands.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from telegram import ForceReply, ParseMode +from telegram import ForceReply, ParseMode, InlineKeyboardButton, InlineKeyboardMarkup from blackjackbot.commands.admin.functions import notify_admins from blackjackbot.lang import translate @@ -13,6 +13,38 @@ def stats_cmd(update, context): update.message.reply_text(get_user_stats(update.effective_user.id), parse_mode=ParseMode.HTML) +def reset_stats_cmd(update, context): + """Asks the user if they want to reset their statistics""" + user_id = update.effective_user.id + db = Database() + lang_id = db.get_lang_id(user_id) + + keyboard = [[ + InlineKeyboardButton(translate("reset_stats_confirm_button"), callback_data='reset_stats_confirm'), + InlineKeyboardButton(translate("reset_stats_cancel_button"), callback_data='reset_stats_cancel'), + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + + update.message.reply_text(translate("reset_stats_confirm", lang_id), reply_markup=reply_markup) + + +def reset_stats_callback(update, context): + """Handler for confirmation of statistics reset""" + query = update.callback_query + query.answer() + + user_id = update.effective_user.id + db = Database() + lang_id = db.get_lang_id(user_id) + + if query.data == "reset_stats_confirm": + db.reset_stats(user_id=user_id) + query.edit_message_text(translate("reset_stats_executed", lang_id)) + + elif query.data == "reset_stats_cancel": + query.edit_message_text(translate("reset_stats_cancelled", lang_id)) + + def comment_cmd(update, context): """MessageHandler callback for the /comment command""" if context.user_data.get("state", UserState.IDLE) != UserState.IDLE: diff --git a/blackjackbot/lang/strings/translations_en.json b/blackjackbot/lang/strings/translations_en.json index 3b9e2de..599885a 100644 --- a/blackjackbot/lang/strings/translations_en.json +++ b/blackjackbot/lang/strings/translations_en.json @@ -47,5 +47,10 @@ "admin_reply": "⚠ You need to reply to the user's comment!", "reply_from_maintainer": "The maintainer of this bot sent you a reply:\n\n{}", "statistic_template": "Here are your statistics 📊:\n\nPlayed Games: {}\nWon Games: {}\nLast Played: {} UTC\n\n{}\n\nWinning rate: {:.2%}", - "dealer_name": "Dealer" + "dealer_name": "Dealer", + "reset_stats_confirm": "Do you really want to reset your statistics?", + "reset_stats_confirm_button": "Reset", + "reset_stats_cancel_button": "Cancel", + "reset_stats_executed": "Alright, I reset your statistics!", + "reset_stats_cancelled": "Okay, I did not reset your statistics!" } From 5f01cf93bd79d02de91dc5c6bf76e2cc7014a812 Mon Sep 17 00:00:00 2001 From: Rico Date: Fri, 30 Jul 2021 22:21:59 +0200 Subject: [PATCH 2/5] feat: remove keyboard from older reset messages --- blackjackbot/commands/util/commands.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/blackjackbot/commands/util/commands.py b/blackjackbot/commands/util/commands.py index 566e9e2..2eeff39 100644 --- a/blackjackbot/commands/util/commands.py +++ b/blackjackbot/commands/util/commands.py @@ -16,6 +16,10 @@ def stats_cmd(update, context): def reset_stats_cmd(update, context): """Asks the user if they want to reset their statistics""" user_id = update.effective_user.id + chat_id = update.effective_chat.id + + _modify_old_reset_message(context) + db = Database() lang_id = db.get_lang_id(user_id) @@ -25,7 +29,23 @@ def reset_stats_cmd(update, context): ]] reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text(translate("reset_stats_confirm", lang_id), reply_markup=reply_markup) + sent_message = update.message.reply_text(translate("reset_stats_confirm", lang_id), reply_markup=reply_markup) + reset_message = {"message_id": sent_message.message_id, "chat_id": chat_id} + context.user_data["reset_messages"] = reset_message + + +def _modify_old_reset_message(context): + """Removes the last saved reset confirmation messages from the chat history""" + reset_message = context.user_data.get("reset_message", None) + if reset_message is None: + return + + try: + context.bot.edit_message_reply_markup(chat_id=reset_message.get("chat_id"), message_id=reset_message.get("message_id")) + except: + pass + + context.user_data["reset_messages"] = None def reset_stats_callback(update, context): From 7c7683269ff30ba4cd1eaa64c496168c0bfec816 Mon Sep 17 00:00:00 2001 From: Rico Date: Fri, 30 Jul 2021 22:28:21 +0200 Subject: [PATCH 3/5] feat: if user has no statistics tell the user Prior to this change we simply sent a weird ass statistics message. This just looked horrible and was pretty confusing. --- blackjackbot/lang/strings/translations_en.json | 3 ++- database/statistics.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blackjackbot/lang/strings/translations_en.json b/blackjackbot/lang/strings/translations_en.json index 599885a..a85db68 100644 --- a/blackjackbot/lang/strings/translations_en.json +++ b/blackjackbot/lang/strings/translations_en.json @@ -52,5 +52,6 @@ "reset_stats_confirm_button": "Reset", "reset_stats_cancel_button": "Cancel", "reset_stats_executed": "Alright, I reset your statistics!", - "reset_stats_cancelled": "Okay, I did not reset your statistics!" + "reset_stats_cancelled": "Okay, I did not reset your statistics!", + "no_stats": "You haven't played yet, there are no statistics for you." } diff --git a/database/statistics.py b/database/statistics.py index 656abf6..afe8040 100644 --- a/database/statistics.py +++ b/database/statistics.py @@ -63,7 +63,7 @@ def get_user_stats(user_id): if played_games == 0: # prevent division by zero errors - played_games = 1 + return translate("no_stats") last_played_formatted = datetime.utcfromtimestamp(last_played).strftime('%d.%m.%y %H:%M') win_percentage = round(float(won_games) / float(played_games), 4) From 52ee6e708aaa8416d854fd8de41a784da461f073 Mon Sep 17 00:00:00 2001 From: Rico Date: Sat, 31 Jul 2021 21:45:15 +0200 Subject: [PATCH 4/5] fix: game stuck at blackjack A few players reported that they were stuck at a blackjack. I assume this was because of the weird if/elif construct? But I don't know for sure. Either way this is at worst just a necessary refactor. --- blackjackbot/commands/game/commands.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/blackjackbot/commands/game/commands.py b/blackjackbot/commands/game/commands.py index a67a8ad..8f33459 100644 --- a/blackjackbot/commands/game/commands.py +++ b/blackjackbot/commands/game/commands.py @@ -168,12 +168,11 @@ def hit_callback(update, context): player_cards = get_cards_string(player, lang_id) if player.has_blackjack(): text = (translator("your_cards_are") + "\n\n" + translator("got_blackjack")).format(user_mention, player.cardvalue, player_cards) - update.effective_message.edit_text(text=text, parse_mode=ParseMode.HTML, reply_markup=None) - next_player(update, context) - elif player.cardvalue == 21: + else: text = (translator("your_cards_are") + "\n\n" + translator("got_21")).format(user_mention, player.cardvalue, player_cards) - update.effective_message.edit_text(text=text, parse_mode=ParseMode.HTML, reply_markup=None) - next_player(update, context) + + update.effective_message.edit_text(text=text, parse_mode=ParseMode.HTML, reply_markup=None) + next_player(update, context) @needs_active_game From 1e449a7e61160fb7b7698fb0f50ff9080dbb1f00 Mon Sep 17 00:00:00 2001 From: Rico Date: Sun, 1 Aug 2021 16:17:46 +0200 Subject: [PATCH 5/5] feat: clean up stale games --- blackjack/game/blackjackgame.py | 4 +++- blackjackbot/gamestore.py | 18 ++++++++++++++++++ bot.py | 13 ++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/blackjack/game/blackjackgame.py b/blackjack/game/blackjackgame.py index 55bd28f..2e450f4 100644 --- a/blackjack/game/blackjackgame.py +++ b/blackjack/game/blackjackgame.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import logging - +from datetime import datetime from enum import Enum + import blackjack.errors as errors from blackjack.game import Player, Dealer, Deck @@ -17,6 +18,7 @@ def __init__(self, gametype=None, game_id=None, lang_id="en"): self.list_won = [] self.list_tie = [] self.list_lost = [] + self.datetime_started = datetime.now() self.bets_active = True self._current_player = 0 self.players = [] diff --git a/blackjackbot/gamestore.py b/blackjackbot/gamestore.py index ab59c8a..53960cb 100644 --- a/blackjackbot/gamestore.py +++ b/blackjackbot/gamestore.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import logging +from datetime import datetime, timedelta from random import randint from .errors.noactivegameexception import NoActiveGameException @@ -82,3 +83,20 @@ def _game_stopped_callback(self, game): self.remove_game(self._game_dict[game.id]) self.logger.debug("Current games: {}".format(len(self._chat_dict))) + + def cleanup_stale_games(self): + stale_timeout_min = 10 + now = datetime.now() + remove_chat_ids = [] + + for game_id, chat_id in self._game_dict.items(): + game = self.get_game(chat_id) + + game_older_than_10_mins = game.datetime_started < (now - timedelta(minutes=stale_timeout_min)) + if game_older_than_10_mins: + logging.info("Killing game with id {} because it's stale for > {} mins".format(game.id, stale_timeout_min)) + # TODO notify chat + remove_chat_ids.append(chat_id) + + for chat_id in remove_chat_ids: + self.remove_game(chat_id) diff --git a/bot.py b/bot.py index bb8bc68..b170bf9 100644 --- a/bot.py +++ b/bot.py @@ -4,10 +4,11 @@ import logging.handlers import pathlib -from telegram.ext import Updater +from telegram.ext import Updater, JobQueue import config from blackjackbot import handlers, error_handler +from blackjackbot.gamestore import GameStore logdir_path = pathlib.Path(__file__).parent.joinpath("logs").absolute() logfile_path = logdir_path.joinpath("bot.log") @@ -32,6 +33,16 @@ updater.dispatcher.add_error_handler(error_handler) + +# Set up jobs +def stale_game_cleaner(context): + gs = GameStore() + gs.cleanup_stale_games() + + +updater.job_queue.run_repeating(callback=stale_game_cleaner, interval=300, first=300) + + if config.USE_WEBHOOK: updater.start_webhook(listen=config.WEBHOOK_IP, port=config.WEBHOOK_PORT, url_path=config.BOT_TOKEN, cert=config.CERTPATH, webhook_url=config.WEBHOOK_URL) updater.bot.set_webhook(config.WEBHOOK_URL)