def git_pull(update: Update, context: CallbackContext, updater: Updater): """Pull the latest changes from git repository""" query = update.callback_query if query: context.bot.answer_callback_query(callback_query_id=query.id, text='Pulling from git repository now.') try: pull_result = subprocess.check_output(["git", "pull"], stderr=subprocess.STDOUT).decode() except subprocess.CalledProcessError as e: text = f"{get_emoji('bug')} *Bug Report*\n\n" \ f"Failed to pull latest changes from github: `{e.output.decode()}`" notify_devs(text=text) return text = f"{get_emoji('info')} *Updating Bot*\n\n" \ f"Pulled the latest changes from git repository:\n" \ f"`{pull_result}`\n" up_to_date = 'Already up to date' if up_to_date not in pull_result: text += "Restarting bot now." notify_devs(text=text) if up_to_date not in pull_result: restart(update, context, updater, notify=False)
def func_wrapper(update: Update, context: CallbackContext, *args, **kwargs): user = update.effective_user if user.id not in bot_devs: warning = f"Unauthorized access to admin function by user {user.first_name} (#{user.id} / @{user.username})" logger.warning(warning) text = f"{get_emoji('warning')} *Unauthorized Access*\n\n" \ f"{warning}" notify_devs(text=text) return return func(update, context, *args, **kwargs)
def error(update: Update, context: CallbackContext): """Handle Errors caused by Updates.""" # get traceback trace = "".join(traceback.format_tb(sys.exc_info()[2])) error_details = "" # inform user if update: if update.effective_message: (chat_id, msg_id, user_id, username) = extract_ids(update) lang = profile.get_language( context.chat_data) if profile.get_language( context.chat_data) else 'en' text = f"{get_emoji('bug')} *{get_text(lang, 'error_occurred_title')}*\n\n" \ f"{get_text(lang, 'error_occurred_message').format(provider=bot_provider)}" keyboard = [[ InlineKeyboardButton( text= f"{get_emoji('overview')} {get_text(lang, 'overview')}", callback_data='overview') ]] message_user(bot=context.bot, chat_id=chat_id, chat_data=context.chat_data, message_type=MessageType.message, payload=text, keyboard=keyboard, category=MessageCategory.main) user_obj = update.effective_user chat_obj = update.effective_chat if user_obj: error_details += f' with the user {mention_markdown(user_obj.id, user_obj.first_name)}' if chat_obj: error_details += f' within the {chat_obj.type} chat _{chat_obj.title}_' if chat_obj.username: error_details += f' (@{chat_obj.username})' # only add the poll id if there is neither a user nor a chat associated with this update if not error_details and update.poll: error_details += f' with the poll id {update.poll.id}.' # construct bug report for devs text = f"{get_emoji('bug')} *Bug Report*\n\n" \ f"The error `{context.error}` happened{error_details}.\n\n" \ f"Traceback:\n" \ f"`{trace}`" notify_devs(text=text) # raise the error again, so the logger module can catch it raise
def restart(update: Update, context: CallbackContext, updater: Updater, notify=True): """Restart the bot upon admin command""" query = update.callback_query if query: context.bot.answer_callback_query(callback_query_id=query.id, text='Restarting bot now.') def stop_and_restart(): """Gracefully stop the Updater and replace the current process with a new one""" updater.stop() os.execl(sys.executable, sys.executable, *sys.argv) if notify: text = f"{get_emoji('info')} *Restarting Bot*\n\n" \ f"Bot is restarting." notify_devs(text=text) Thread(target=stop_and_restart).start()
def load_quests(context: CallbackContext): db = mysql.connector.connect(host=mysql_host, port=mysql_port, user=mysql_user, passwd=mysql_password, database=mysql_db) global latest_quest_scan midnight = datetime.combine(datetime.today(), time.min).timestamp() # make sure only quests from today get loaded if midnight > latest_quest_scan: latest_quest_scan = midnight cursor = db.cursor() cursor.execute( "SELECT " "ps.pokestop_id," "ps.name, " "ps.latitude, " "ps.longitude, " "q.quest_timestamp, " "q.quest_pokemon_id, " "q.quest_item_id, " "q.quest_item_amount, " "q.quest_template " "FROM trs_quest as q " "LEFT JOIN pokestop as ps " "ON q.GUID = ps.pokestop_id " "WHERE q.quest_timestamp >= %s " "AND q.quest_stardust = 0 " "ORDER BY q.quest_timestamp DESC", (latest_quest_scan, )) result = cursor.fetchall() db.close() unknown_tasks = {} for (stop_id, stop_name, latitude, longitude, timestamp, pokemon_id, item_id, item_amount, task_id) in result: # skip quest if older than the existing quest entry if stop_id in quests and quests[stop_id].timestamp > timestamp: continue # remember quest rewards if pokemon_id != 0 and pokemon_id not in quest_pokemon_list: quest_pokemon_list.append(pokemon_id) if item_id != 0 and item_id not in quest_items_list: quest_items_list.append(item_id) # create new quest object quests[stop_id] = Quest(stop_id=stop_id, stop_name=stop_name, latitude=latitude, longitude=longitude, timestamp=timestamp, pokemon_id=pokemon_id, item_id=item_id, item_amount=item_amount, task_id=task_id) if get_task_by_id('en', task_id) == task_id: unknown_tasks[task_id] = quests[stop_id] if timestamp > latest_quest_scan: latest_quest_scan = timestamp + 1 if unknown_tasks: text = f"{get_emoji('bug')} *Bug Report*\n\n" \ f"The following tasks are unknown:\n\n" # gather unknown tasks for quest in unknown_tasks.values(): text += f"`task: {quest.task_id}\n" \ f"pokemon_id: {quest.pokemon_id}\n" \ f"item_id: {quest.item_id}\n" \ f"item_amount: {quest.item_amount}`\n\n" # inform devs notify_devs(text=text) quest_pokemon_list.sort() quest_items_list.sort() logger.info( f"{len(result)} new quests loaded from DB. Total quest count: {len(quests)}" )
def main(): logger.info("Starting Bot.") # request object for bot request = Request(con_pool_size=8) # use message queue bot version if bot_use_message_queue: logger.info("Using MessageQueue to avoid flood limits.") # enable message queue with production limits msg_queue = messagequeue.MessageQueue(all_burst_limit=29, all_time_limit_ms=1017, group_burst_limit=20, group_time_limit_ms=60000) # create a message queue bot bot = MQBot(bot_token, request=request, msg_queue=msg_queue) # use regular bot else: logger.info("Using no MessageQueue. You may run into flood limits.") # use the default telegram bot (without message queue) bot = Bot(bot_token, request=request) set_bot(bot=bot) notify_devs(text=f"{get_emoji('info')} *Starting Bot*\n\nBot is starting.") persistence = PicklePersistence(filename='persistent_data.pickle') # create the EventHandler and pass it the bot's instance updater = Updater(bot=bot, use_context=True, persistence=persistence) # jobs job_queue = updater.job_queue job_queue.run_daily(callback=clear_quests, time=time(hour=0, minute=0, second=0)) job_queue.run_repeating(callback=load_quests, interval=300, first=0) job_queue.run_daily(callback=load_shinies, time=time(hour=0, minute=0, second=0)) job_queue.run_once(callback=load_shinies, when=0) # get the dispatcher to register handlers dp = updater.dispatcher # admin commands dp.add_handler( CallbackQueryHandler(callback=partial(restart, updater=updater), pattern='^restart_bot$')) dp.add_handler( CallbackQueryHandler(callback=partial(git_pull, updater=updater), pattern='^git_pull$')) # overview dp.add_handler(CommandHandler(callback=chat.start, command='start')) dp.add_handler( CallbackQueryHandler(callback=chat.start, pattern='^overview')) # settings dp.add_handler( CallbackQueryHandler(callback=chat.settings, pattern='^settings')) # info section dp.add_handler(CallbackQueryHandler(callback=chat.info, pattern='^info')) # delete data dp.add_handler( CallbackQueryHandler(callback=chat.delete_data, pattern='^delete_data')) # select area conversation conversation_handler_select_area = ConversationHandler( entry_points=[ CallbackQueryHandler(callback=conversation.select_area, pattern='^select_area$') ], states={ # receive location, ask for radius conversation.STEP0: [ MessageHandler(callback=conversation.set_quest_center_point, filters=Filters.all) ], # receive radius conversation.STEP1: [ MessageHandler(callback=conversation.set_quest_radius, filters=Filters.all) ], # receive button click, ask for location or radius conversation.STEP2: [ CallbackQueryHandler(callback=conversation.change_center_point, pattern='^change_center_point'), CallbackQueryHandler(callback=conversation.change_radius, pattern='^change_radius') ], }, # fallback to overview fallbacks=[ CallbackQueryHandler(callback=chat.start, pattern='^back_to_overview') ], allow_reentry=True, persistent=True, name="select_area") dp.add_handler(conversation_handler_select_area) # choose quest conversation conversation_handler_choose_quest = ConversationHandler( entry_points=[ CallbackQueryHandler(callback=conversation.choose_quest_type, pattern="^choose_quest_type$") ], states={ # receive quest type, ask for quest conversation.STEP0: [ CallbackQueryHandler(callback=conversation.choose_pokemon, pattern="^choose_pokemon"), CallbackQueryHandler(callback=conversation.choose_item, pattern="^choose_item"), CallbackQueryHandler(callback=conversation.choose_task, pattern="^choose_task") ] }, fallbacks=[ CallbackQueryHandler(callback=chat.start, pattern='^back_to_overview', pass_user_data=True) ], allow_reentry=True, persistent=True, name="choose_quest") dp.add_handler(conversation_handler_choose_quest) # hunt quest conversation conversation_handler_start_hunt = ConversationHandler( entry_points=[ CallbackQueryHandler(callback=conversation.start_hunt, pattern="^start_hunt") ], states={ # receive start location, send quest conversation.STEP0: [ CallbackQueryHandler( callback=conversation.continue_previous_hunt, pattern="^continue_previous_hunt"), CallbackQueryHandler(callback=conversation.reset_previous_hunt, pattern="^reset_previous_hunt") ], conversation.STEP1: [ MessageHandler(callback=conversation.set_start_location, filters=Filters.all) ], conversation.STEP2: [ CallbackQueryHandler(callback=conversation.quest_collected, pattern="^quest_collected"), CallbackQueryHandler(callback=conversation.quest_skip, pattern="^quest_skip"), CallbackQueryHandler(callback=conversation.quest_ignore, pattern="^quest_ignore"), CallbackQueryHandler(callback=conversation.end_hunt, pattern="^end_hunt"), CallbackQueryHandler(callback=conversation.enqueue_skipped, pattern="^enqueue_skipped"), CallbackQueryHandler(callback=conversation.continue_hunt, pattern="^continue_hunt"), CallbackQueryHandler(callback=conversation.process_hint, pattern="^hint") ] }, fallbacks=[ CallbackQueryHandler(callback=chat.start, pattern='^back_to_overview') ], allow_reentry=True, persistent=True, name="start_hunt") dp.add_handler(conversation_handler_start_hunt) # catch-all handler that just logs messages dp.add_handler( MessageHandler(callback=utils.dummy_callback, filters=Filters.all)) dp.add_handler( CallbackQueryHandler(callback=utils.dummy_callback, pattern=".*")) # log all errors dp.add_error_handler(error) # start the bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle()