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?
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.