async def cmd_addwords(message: types.Message) -> None: words_to_add = [ w for w in set(message.get_args().lower().split()) if all(c in ascii_lowercase for c in w) ] if not words_to_add: return existing = [] rejected = [] rejected_with_reason = [] for w in words_to_add[:]: if check_word_existence(w): existing.append("_" + w.capitalize() + "_") words_to_add.remove(w) async with pool.acquire() as conn: rej = await conn.fetch( "SELECT word, reason FROM wordlist WHERE NOT accepted;") for word, reason in rej: if word not in words_to_add: continue words_to_add.remove(word) word = "_" + word.capitalize() + "_" if reason: rejected_with_reason.append((word, reason)) else: rejected.append(word) text = "" if words_to_add: async with pool.acquire() as conn: await conn.executemany( "INSERT INTO wordlist (word, accepted) VALUES ($1, true)", [(w, ) for w in words_to_add], ) text += f"Added {', '.join(['_' + w.capitalize() + '_' for w in words_to_add])} to the word list.\n" if existing: text += f"{', '.join(existing)} {'is' if len(existing) == 1 else 'are'} already in the word list.\n" if rejected: text += f"{', '.join(rejected)} {'was' if len(rejected) == 1 else 'were'} rejected.\n" for word, reason in rejected_with_reason: text += f"{word} was rejected due to {reason}.\n" msg = await message.reply(text.rstrip()) if not words_to_add: return await update_words() await msg.edit_text(msg.md_text + "\n\nWord list updated.") await bot.send_message( WORD_ADDITION_CHANNEL_ID, f"Added {', '.join(['_' + w.capitalize() + '_' for w in words_to_add])} to the word list.", disable_notification=True, )
async def error_handler(update: types.Update, error: TelegramAPIError) -> None: if isinstance(error, BadRequest) and str(error) == "Reply message not found": return if isinstance(error, MigrateToChat): if update.message.chat.id in GAMES: GAMES[error.migrate_to_chat_id] = GAMES.pop(update.message.chat.id) GAMES[error.migrate_to_chat_id].group_id = error.migrate_to_chat_id async with pool.acquire() as conn: await conn.execute("UPDATE game SET group_id = $1 WHERE group_id = $2;\n" "UPDATE gameplayer SET group_id = $1 WHERE group_id = $2;\n" "DELETE FROM game WHERE group_id = $2;\n" "DELETE FROM gameplayer WHERE group_id = $2;", error.migrate_to_chat_id, update.message.chat.id) return await bot.send_message(ADMIN_GROUP_ID, f"`{error.__class__.__name__} @ " f"{update.message.chat.id if update.message and update.message.chat else 'idk'}`:\n" f"`{str(error)}`") if not update.message or not update.message.chat: return try: await update.message.reply("Error occurred. My owner has been notified.") except TelegramAPIError: pass if update.message.chat.id in GAMES: GAMES[update.message.chat.id].state = GameState.KILLGAME await asyncio.sleep(2) try: del GAMES[update.message.chat.id] await update.message.reply("Game ended forcibly.") except (KeyError, TelegramAPIError): pass
async def successful_payment_handler(message: types.Message) -> None: payment = message.successful_payment donation_id = str(uuid4())[:8] amt = Decimal(payment.total_amount) / 100 dt = datetime.now().replace(microsecond=0) async with pool.acquire() as conn: await conn.execute( """\ INSERT INTO donation ( donation_id, user_id, amount, donate_time, telegram_payment_charge_id, provider_payment_charge_id ) VALUES ($1, $2, $3::NUMERIC, $4, $5, $6);""", donation_id, message.from_user.id, str(amt), dt, payment.telegram_payment_charge_id, payment.provider_payment_charge_id, ) await asyncio.gather( message.answer( (f"Your donation of {amt} HKD is successful.\n" "Thank you for your support! :D\n" f"Donation id: #on9wcbot_{donation_id}"), parse_mode=types.ParseMode.HTML, ), send_admin_group( (f"Received donation of {amt} HKD from {message.from_user.get_mention(as_html=True)} " f"(id: <code>{message.from_user.id}</code>).\n" f"Donation id: #on9wcbot_{donation_id}"), parse_mode=types.ParseMode.HTML, ))
async def cmd_stats(message: types.Message) -> None: rmsg = message.reply_to_message if (message.chat.id < 0 and not message.get_command().partition("@")[2] or rmsg and (rmsg.from_user.is_bot and rmsg.from_user.id != ON9BOT_ID and not rmsg.forward_from or rmsg.forward_from and rmsg.forward_from.is_bot and rmsg.forward_from.id != ON9BOT_ID)): return user = rmsg.forward_from or rmsg.from_user if rmsg else message.from_user async with pool.acquire() as conn: res = await conn.fetchrow("SELECT * FROM player WHERE user_id = $1;", user.id) if not res: await message.reply(f"No statistics for {user.get_mention(as_html=True)}!", parse_mode=types.ParseMode.HTML) return await message.reply( f"\U0001f4ca Statistics for " + user.get_mention(name=user.full_name + (" \u2b50\ufe0f" if user.id in VIP or bool(await amt_donated(user.id)) else ""), as_html=True) + f":\n" f"<b>{res['game_count']}</b> games played\n" f"<b>{res['win_count']} (" f"{'0%' if not res['win_count'] else format(res['win_count'] / res['game_count'], '.0%')})</b> games won\n" f"<b>{res['word_count']}</b> total words played\n" f"<b>{res['letter_count']}</b> total letters played" + (f"\nLongest word used: <b>{res['longest_word'].capitalize()}</b>" if res["longest_word"] else ""), parse_mode=types.ParseMode.HTML )
async def cmd_rejword(message: types.Message) -> None: arg = message.get_args() word, _, reason = arg.partition(" ") if not word: return word = word.lower() async with pool.acquire() as conn: r = await conn.fetchrow( "SELECT accepted, reason FROM wordlist WHERE word = $1;", word) if r is None: await conn.execute( "INSERT INTO wordlist (word, accepted, reason) VALUES ($1, false, $2)", word, reason.strip() or None, ) word = word.capitalize() if r is None: await message.reply(f"_{word}_ rejected.") elif r["accepted"]: await message.reply(f"_{word}_ was accepted.") elif not r["reason"]: await message.reply(f"_{word}_ was already rejected.") else: await message.reply( f"_{word}_ was already rejected due to {r['reason']}.")
async def cmd_stats(message: types.Message) -> None: rmsg = message.reply_to_message if message.chat.id < 0 and not message.get_command().partition("@")[2]: return user = (rmsg.forward_from or rmsg.from_user) if rmsg else message.from_user async with pool.acquire() as conn: res = await conn.fetchrow("SELECT * FROM player WHERE user_id = $1;", user.id) if not res: await message.reply( f"No statistics for {user.get_mention(as_html=True)}!", parse_mode=types.ParseMode.HTML, ) return mention = user.get_mention( name=user.full_name + (" \u2b50\ufe0f" if await has_star(user.id) else ""), as_html=True, ) text = f"\U0001f4ca Statistics for {mention}:\n" text += f"<b>{res['game_count']}</b> games played\n" text += f"<b>{res['win_count']} ({res['win_count'] / res['game_count']:.0%})</b> games won\n" text += f"<b>{res['word_count']}</b> total words played\n" text += f"<b>{res['letter_count']}</b> total letters played\n" if res["longest_word"]: text += f"Longest word: <b>{res['longest_word'].capitalize()}</b>" await message.reply(text.rstrip(), parse_mode=types.ParseMode.HTML)
async def cmd_reqaddword(message: types.Message) -> None: if message.forward_from: return words_to_add = [ w for w in set(message.get_args().lower().split()) if all(c in ascii_lowercase for c in w) ] if not words_to_add: await message.reply( "Function: Request addition of new words. Check @on9wcwa for new words.\n" "Please check the spelling of words before requesting so I can process your requests faster.\n" "Proper nouns are not accepted.\n" "Usage: `/reqaddword wordone wordtwo ...`") return existing = [] rejected = [] rejected_with_reason = [] for w in words_to_add[:]: # Iterate through a copy so removal of elements is possible if check_word_existence(w): existing.append("_" + w.capitalize() + "_") words_to_add.remove(w) async with pool.acquire() as conn: rej = await conn.fetch( "SELECT word, reason FROM wordlist WHERE NOT accepted;") for word, reason in rej: if word not in words_to_add: continue words_to_add.remove(word) word = "_" + word.capitalize() + "_" if reason: rejected_with_reason.append((word, reason)) else: rejected.append(word) text = "" if words_to_add: text += f"Submitted {', '.join(['_' + w.capitalize() + '_' for w in words_to_add])} for approval.\n" await send_admin_group( message.from_user.get_mention( name=message.from_user.full_name + (" \u2b50\ufe0f" if await has_star(message.from_user.id) else ""), as_html=True, ) + " is requesting the addition of " + ", ".join(["<i>" + w.capitalize() + "</i>" for w in words_to_add]) + " to the word list. #reqaddword", parse_mode=types.ParseMode.HTML, ) if existing: text += f"{', '.join(existing)} {'is' if len(existing) == 1 else 'are'} already in the word list.\n" if rejected: text += f"{', '.join(rejected)} {'was' if len(rejected) == 1 else 'were'} rejected.\n" for word, reason in rejected_with_reason: text += f"{word} was rejected due to {reason}.\n" await message.reply(text.rstrip())
async def get_active_groups() -> Dict[str, Any]: async with pool.acquire() as conn: return dict(await conn.fetch( """\ SELECT start_time::DATE d, COUNT(DISTINCT group_id) FROM game WHERE game.start_time::DATE >= $1 GROUP BY d ORDER BY d;""", d - timedelta(days=days - 1), ))
async def get_daily_games() -> Dict[str, Any]: async with pool.acquire() as conn: return dict(await conn.fetch( """\ SELECT start_time::DATE d, COUNT(start_time::DATE) FROM game WHERE start_time::DATE >= $1 GROUP BY d ORDER BY d;""", d - timedelta(days=days - 1), ))
async def cmd_globalstats(message: types.Message) -> None: async with pool.acquire() as conn: group_count, game_count = await conn.fetchrow("SELECT COUNT(DISTINCT group_id), COUNT(*) FROM game;") player_count, word_count, letter_count = await conn.fetchrow( "SELECT COUNT(*), SUM(word_count), SUM(letter_count) FROM player;" ) await message.reply("\U0001f4ca Global statistics\n" f"*{group_count}* groups\n" f"*{player_count}* players\n" f"*{game_count}* games played\n" f"*{word_count}* total words played\n" f"*{letter_count}* total letters played")
async def get_active_players() -> Dict[str, Any]: async with pool.acquire() as conn: return dict(await conn.fetch( """\ SELECT game.start_time::DATE d, COUNT(DISTINCT gameplayer.user_id) FROM gameplayer INNER JOIN game ON gameplayer.game_id = game.id WHERE game.start_time::DATE >= $1 GROUP BY d ORDER BY d;""", d - timedelta(days=days - 1), ))
async def get_global_stats() -> str: async with pool.acquire() as conn: group_cnt, game_cnt = await conn.fetchrow( "SELECT COUNT(DISTINCT group_id), COUNT(*) FROM game;") player_cnt, word_cnt, letter_cnt = await conn.fetchrow( "SELECT COUNT(*), SUM(word_count), SUM(letter_count) FROM player;") return ("\U0001f4ca Global statistics\n" f"*{group_cnt}* groups\n" f"*{player_cnt}* players\n" f"*{game_cnt}* games played\n" f"*{word_cnt}* total words played\n" f"*{letter_cnt}* total letters played")
async def error_handler(update: types.Update, error: TelegramAPIError) -> None: for game in GAMES.values( ): # TODO: Do this for group in which error occurs only asyncio.create_task(game.scan_for_stale_timer()) if isinstance(error, MigrateToChat): if update.message.chat.id in GAMES: # TODO: Test old_gid = GAMES[update.message.chat.id].group_id GAMES[error.migrate_to_chat_id] = GAMES.pop(update.message.chat.id) GAMES[error.migrate_to_chat_id].group_id = error.migrate_to_chat_id asyncio.create_task( send_admin_group( f"Game moved from {old_gid} to {error.migrate_to_chat_id}." )) async with pool.acquire() as conn: await conn.execute( """\ UPDATE game SET group_id = $1 WHERE group_id = $2; UPDATE gameplayer SET group_id = $1 WHERE group_id = $2; DELETE FROM game WHERE group_id = $2; DELETE FROM gameplayer WHERE group_id = $2;""", error.migrate_to_chat_id, update.message.chat.id, ) await send_admin_group(f"Group migrated to {error.migrate_to_chat_id}." ) return send_admin_msg = await send_admin_group( f"`{error.__class__.__name__} @ " f"{update.message.chat.id if update.message and update.message.chat else 'idk'}`:\n" f"`{str(error)}`", ) if not update.message or not update.message.chat: return try: await update.message.reply( "Error occurred. My owner has been notified.") except TelegramAPIError: pass if update.message.chat.id in GAMES: asyncio.create_task( send_admin_msg.reply( f"Killing game in {update.message.chat.id} consequently.")) GAMES[update.message.chat.id].state = GameState.KILLGAME await asyncio.sleep(2) try: del GAMES[update.message.chat.id] await update.message.reply("Game ended forcibly.") except: pass
async def cmd_reqaddwords(message: types.Message) -> None: if message.forward_from: return words_to_add = [ w for w in set(message.get_args().lower().split()) if all(c in ascii_lowercase for c in w) ] if not words_to_add: await message.reply( "Function: Request addition of new words. Check @on9wcwa for new words.\n" "Usage: `/reqaddword wordone wordtwo ...`") return existing = [] rejected = [] rejected_with_reason = [] for w in words_to_add[:]: if w in get_words()[w[0]]: existing.append("_" + w.capitalize() + "_") words_to_add.remove(w) async with pool.acquire() as conn: rej = await conn.fetch( "SELECT word, reason FROM wordlist WHERE NOT accepted;") for word, reason in rej: if word not in words_to_add: continue words_to_add.remove(word) word = "_" + word.capitalize() + "_" if reason: rejected_with_reason.append((word, reason)) else: rejected.append(word) text = "" if words_to_add: text += f"Submitted {', '.join(['_' + w.capitalize() + '_' for w in words_to_add])} for approval.\n" await bot.send_message( ADMIN_GROUP_ID, message.from_user.get_mention( name=message.from_user.full_name + (" \u2b50\ufe0f" if message.from_user.id in VIP or bool(await amt_donated(message.from_user.id)) else ""), as_html=True) + " is requesting the addition of " + ", ".join(["<i>" + w.capitalize() + "</i>" for w in words_to_add]) + " to the word list.", parse_mode=types.ParseMode.HTML) if existing: text += f"{', '.join(existing)} {'is' if len(existing) == 1 else 'are'} already in the word list.\n" if rejected: text += f"{', '.join(rejected)} {'was' if len(rejected) == 1 else 'were'} rejected.\n" for word, reason in rejected_with_reason: text += f"{word} was rejected due to {reason}.\n" await message.reply(text.rstrip())
async def update_db_player(self, game_id: int, player: Player) -> None: async with pool.acquire() as conn: player_exists = bool(await conn.fetchval( "SELECT id FROM player WHERE user_id = $1;", player.user_id)) if player_exists: # Update player in db await conn.execute( """\ UPDATE player SET game_count = game_count + 1, win_count = win_count + $1, word_count = word_count + $2, letter_count = letter_count + $3, longest_word = CASE WHEN longest_word IS NULL THEN $4::TEXT WHEN $4::TEXT IS NULL THEN longest_word WHEN LENGTH($4::TEXT) > LENGTH(longest_word) THEN $4::TEXT ELSE longest_word END WHERE user_id = $5;""", int(player in self.players_in_game ), # Support no winner in some game modes player.word_count, player.letter_count, player.longest_word or None, player.user_id, ) else: # New player, create player in db await conn.execute( """\ INSERT INTO player (user_id, game_count, win_count, word_count, letter_count, longest_word) VALUES ($1, 1, $2, $3, $4, $5::TEXT);""", player.user_id, int(player in self.players_in_game), # No winner in some game modes player.word_count, player.letter_count, player.longest_word or None, ) # Create gameplayer in db await conn.execute( """\ INSERT INTO gameplayer (user_id, group_id, game_id, won, word_count, letter_count, longest_word) VALUES ($1, $2, $3, $4, $5, $6, $7);""", player.user_id, self.group_id, game_id, player in self.players_in_game, player.word_count, player.letter_count, player.longest_word or None, )
async def cmd_sql(message: types.Message) -> None: try: async with pool.acquire() as conn: res = await conn.fetch(message.get_full_command()[1]) except Exception as e: await message.reply(f"`{e.__class__.__name__}: {str(e)}`") return if not res: await message.reply("No results returned.") return text = ["*" + " - ".join(res[0].keys()) + "*"] for r in res: text.append("`" + " - ".join([str(i) for i in r.values()]) + "`") await message.reply("\n".join(text))
async def cmd_groupstats(message: types.Message) -> None: if message.chat.id > 0: await groups_only_command(message) return async with pool.acquire() as conn: player_count, game_count, word_count, letter_count = await conn.fetchrow("""\ SELECT COUNT(DISTINCT user_id), COUNT(DISTINCT game_id), SUM(word_count), SUM(letter_count) FROM gameplayer WHERE group_id = $1;""", message.chat.id) await message.reply(f"\U0001f4ca Statistics for <b>{quote_html(message.chat.title)}</b>\n" f"<b>{player_count}</b> players\n" f"<b>{game_count}</b> games played\n" f"<b>{word_count}</b> total words played\n" f"<b>{letter_count}</b> total letters played", parse_mode=types.ParseMode.HTML)
async def get_cumulative_groups() -> Dict[str, Any]: async with pool.acquire() as conn: return dict(await conn.fetch( """\ SELECT * FROM ( SELECT d, SUM(count) OVER (ORDER BY d) FROM ( SELECT d, COUNT(group_id) FROM ( SELECT DISTINCT group_id, MIN(start_time::DATE) d FROM game GROUP BY group_id ) gd GROUP BY d ) dg ) ds WHERE d >= $1;""", d - timedelta(days=days - 1), ))
async def update_db(self) -> None: async with pool.acquire() as conn: # Insert game instance await conn.execute( """\ INSERT INTO game (group_id, players, game_mode, winner, start_time, end_time) VALUES ($1, $2, $3, $4, $5, $6);""", self.group_id, len(self.players), self.__class__.__name__, self.players_in_game[0].user_id if self.players_in_game else None, self.start_time, self.end_time, ) # Get game id game_id = await conn.fetchval( "SELECT id FROM game WHERE group_id = $1 AND start_time = $2;", self.group_id, self.start_time, ) for player in self.players: # Update db players in parallel asyncio.create_task(self.update_db_player(game_id, player))
async def amt_donated(user_id: int) -> int: async with pool.acquire() as conn: amt = await conn.fetchval( "SELECT SUM(amount) FROM donation WHERE user_id = $1;", user_id) return amt or 0
async def cmd_trends(message: types.Message) -> None: try: days = int(message.get_args() or 7) assert days > 1, "smh" except (ValueError, AssertionError) as e: await message.reply(f"`{e.__class__.__name__}: {str(e)}`") return d = datetime.now().date() tp = [d - timedelta(days=i) for i in range(days - 1, -1, -1)] f = DateFormatter("%b %d" if days < 180 else "%b" if days < 335 else "%b %Y") async with pool.acquire() as conn: daily_games = dict(await conn.fetch("""\ SELECT start_time::DATE d, COUNT(start_time::DATE) FROM game WHERE start_time::DATE >= $1 GROUP BY d ORDER BY d;""", d - timedelta(days=days - 1))) active_players = dict(await conn.fetch("""\ SELECT game.start_time::DATE d, COUNT(DISTINCT gameplayer.user_id) FROM gameplayer INNER JOIN game ON gameplayer.game_id = game.id WHERE game.start_time::DATE >= $1 GROUP BY d ORDER BY d;""", d - timedelta(days=days - 1))) active_groups = dict(await conn.fetch("""\ SELECT start_time::DATE d, COUNT(DISTINCT group_id) FROM game WHERE game.start_time::DATE >= $1 GROUP BY d ORDER BY d;""", d - timedelta(days=days - 1))) cumulative_groups = dict(await conn.fetch("""\ SELECT * FROM ( SELECT d, SUM(count) OVER (ORDER BY d) FROM ( SELECT d, COUNT(group_id) FROM ( SELECT DISTINCT group_id, MIN(start_time::DATE) d FROM game GROUP BY group_id ) gd GROUP BY d ) dg ) ds WHERE d >= $1;""", d - timedelta(days=days - 1))) dt = d - timedelta(days=days) for i in range(days): dt += timedelta(days=1) if dt not in cumulative_groups: if not i: cumulative_groups[dt] = await conn.fetchval( "SELECT COUNT(DISTINCT group_id) FROM game WHERE start_time::DATE <= $1;", dt ) else: cumulative_groups[dt] = cumulative_groups[dt - timedelta(days=1)] cumulative_players = dict(await conn.fetch("""\ SELECT * FROM ( SELECT d, SUM(count) OVER (ORDER BY d) FROM ( SELECT d, COUNT(user_id) FROM ( SELECT DISTINCT user_id, MIN(start_time::DATE) d FROM gameplayer INNER JOIN game ON game_id = game.id GROUP BY user_id ) ud GROUP BY d ) du ) ds WHERE d >= $1;""", d - timedelta(days=days - 1))) dt = d - timedelta(days=days) for i in range(days): dt += timedelta(days=1) if dt not in cumulative_players: if not i: cumulative_players[dt] = await conn.fetchval("""\ SELECT COUNT(DISTINCT user_id) FROM gameplayer INNER JOIN game ON game_id = game.id WHERE start_time <= $1;""", dt) else: cumulative_players[dt] = cumulative_players[dt - timedelta(days=1)] game_mode_play_cnt = await conn.fetch("""\ SELECT COUNT(game_mode), game_mode FROM game WHERE start_time::DATE >= $1 GROUP BY game_mode ORDER BY count;""", d - timedelta(days=days - 1)) total_games = sum(i[0] for i in game_mode_play_cnt) while os.path.exists("trends.jpg"): await asyncio.sleep(0.1) plt.figure(figsize=(15, 8)) plt.subplots_adjust(hspace=0.4) plt.suptitle(f"Trends in the Past {days} Days", size=25) sp = plt.subplot(231) sp.xaxis.set_major_formatter(f) sp.yaxis.set_major_locator(MaxNLocator(integer=True)) # Force y-axis intervals to be integral plt.setp(sp.xaxis.get_majorticklabels(), rotation=45, horizontalalignment='right') plt.title("Games Played", size=18) plt.plot(tp, [daily_games.get(i, 0) for i in tp]) plt.ylim(ymin=0) sp = plt.subplot(232) sp.xaxis.set_major_formatter(f) sp.yaxis.set_major_locator(MaxNLocator(integer=True)) plt.setp(sp.xaxis.get_majorticklabels(), rotation=45, horizontalalignment='right') plt.title("Active Groups", size=18) plt.plot(tp, [active_groups.get(i, 0) for i in tp]) plt.ylim(ymin=0) sp = plt.subplot(233) sp.xaxis.set_major_formatter(f) sp.yaxis.set_major_locator(MaxNLocator(integer=True)) plt.setp(sp.xaxis.get_majorticklabels(), rotation=45, horizontalalignment='right') plt.title("Active Players", size=18) plt.plot(tp, [active_players.get(i, 0) for i in tp]) plt.ylim(ymin=0) plt.subplot(234) labels = [i[1] for i in game_mode_play_cnt] colors = ["dark maroon", "dark peach", "orange", "leather", "mustard", "teal", "french blue", "booger"][ 8 - len(game_mode_play_cnt):] slices, text = plt.pie([i[0] for i in game_mode_play_cnt], labels=[f"{i[0] / total_games:.1%} ({i[0]})" if i[0] / total_games >= 0.03 else "" for i in game_mode_play_cnt], colors=["xkcd:" + c for c in colors], startangle=90) plt.legend(slices, labels, title="Game Modes Played", fontsize="x-small", loc="best") plt.axis("equal") sp = plt.subplot(235) sp.xaxis.set_major_formatter(f) sp.yaxis.set_major_locator(MaxNLocator(integer=True)) plt.setp(sp.xaxis.get_majorticklabels(), rotation=45, horizontalalignment='right') plt.title("Cumulative Groups", size=18) plt.plot(tp, [cumulative_groups[i] for i in tp]) sp = plt.subplot(236) sp.xaxis.set_major_formatter(f) sp.yaxis.set_major_locator(MaxNLocator(integer=True)) plt.setp(sp.xaxis.get_majorticklabels(), rotation=45, horizontalalignment='right') plt.title("Cumulative Players", size=18) plt.plot(tp, [cumulative_players[i] for i in tp]) plt.savefig("trends.jpg", bbox_inches="tight") plt.close("all") async with aiofiles.open("trends.jpg", "rb") as f: await message.reply_photo(f) await aiofiles.os.remove("trends.jpg")