Search code examples
pythonasynchronouspython-telegram-bot

How to send message to user in a syncronous function inside a separate thread?


I'm currently working on a telegram bot. I want to create a game which will act fully like a CLI game, but you need to send messages to telegram bot to play it. And I ran into one problem, my game works on an infinite while loop and to run that game in a non-blocking manner I decided to put it in a separate thread. The problem is that context.bot.send_message(...) doesn't send any messages in a thread and I suspect that it's because context.bot.send_message(...) is async and is being called from a sync context.

Here's the code I wrote:

async def start_game_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if context.user_data.get("game") is not None:
        await context.bot.send_message(chat_id=update.effective_chat.id, parse_mode="markdown", text="Game has already started!")
        return 

    game = Game()

    context.user_data["game"] = game
    
    game_process = threading.Thread(
        target=game.start,
        args=(
            functools.partial( 
                context.bot.send_message, # this is basically a `plugin_func()` from the next code block
                chat_id=update.effective_chat.id,
                parse_mode="markdown",
                text=f"```text {context.user_data['game'].get_graphics()}```"
            ),
        )
    )

    game_process.start()

This function starts the game and passes the function context.bot.send_message(...) to the thread so that it could be plugged in inside the main while loop of the game. And here's a start function itself:

class Game:
    ...

    def start(self, plugin_func: Callable) -> None:
        self._game_started = True

        while self._game_started and not self._game_finished:
            self.__update_player_pos()
            self.__update_surroundings_position()
            plugin_func() # This is the place where it gives a warning
            print(self.get_graphics())
            time.sleep(self.game_pacing)

And this is the warning message I get:

RuntimeWarning: coroutine 'ExtBot.send_message' was never awaited
  plugin_func()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

The problem is that the game starts and print(self.get_graphics()) prints out current state of the game perfectly. But the plugin_func does not send any messages in my telegram bot.

I think that the problem is that the plugin_func is async and I try to call it from a sync context, but I don't want to change my start function to asyncronous so what should I do?


Solution

  • I did not find a solution without changing the code inside a Game class. But here's what I ended up changing (explanations after the code):

    This is my main.py:

    async def start_game_handler(update: Update, context: ContextTypes.DEFAULT_TYPE, application):
        if context.user_data.get("game") is not None:
            await context.bot.send_message(chat_id=update.effective_chat.id, parse_mode="markdown", text="Game has already started!")
            return 
    
        game = Game()
    
        context.user_data["game"] = game
        chat_id = update.effective_chat.id
        
        game_process = threading.Thread(
            target=game.start,
            args=(
                functools.partial(
                    context.bot.send_message,
                    parse_mode="markdown",
                    chat_id=chat_id
                ),
            )
        )
    
        game_process.start()
    
        print(f"Field:\n{context.user_data['game'].get_graphics()}")
    

    My Game class:

    class Game:
        ...
    
        def start(self, plugin_func: Callable) -> None:
            self._game_started = True
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
    
            while self._game_started and not self._game_finished:
                self.__update_player_pos()
                self.__update_surroundings_position()
                asyncio.get_event_loop().run_until_complete(plugin_func(text=f"```text{self.get_graphics()}```"))
                print(self.get_graphics())
                time.sleep(self.game_pacing)
                
            loop.close()
    

    So because I'm calling start function inside a separate thread I need to somehow create a async event loop inside of it. And if I were to create an event loop every time my plugin_func would be called, it just wouldn't be that efficient in my opinion. So instead I created event loop outside of the while loop and called my plugin_func using this event loop that I created.