Exemple #1
0
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,
    )
Exemple #2
0
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
Exemple #3
0
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,
        ))
Exemple #4
0
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
    )
Exemple #5
0
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']}.")
Exemple #6
0
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)
Exemple #7
0
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())
Exemple #8
0
 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),
         ))
Exemple #9
0
 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),
         ))
Exemple #10
0
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")
Exemple #11
0
 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")
Exemple #13
0
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
Exemple #14
0
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())
Exemple #15
0
    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,
            )
Exemple #16
0
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))
Exemple #17
0
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)
Exemple #18
0
 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),
         ))
Exemple #19
0
 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))
Exemple #20
0
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
Exemple #21
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")