Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ jobs:
with:
python-version: '3.12'
cache: 'pip'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libpango-1.0-0 libpangoft2-1.0-0 poppler-utils
- name: Install requirements
run: |
pip install --upgrade pip
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ WORKDIR /opt/src
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

RUN apk add --no-cache poppler-utils pango font-noto

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

Expand Down
2 changes: 1 addition & 1 deletion fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ kill_timeout = "30s"

[[vm]]
size = "shared-cpu-1x"
memory = "768mb"
memory = "512mb"
processes = ["worker"]
4 changes: 0 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ aiogram==3.30.0
APScheduler==3.11.3
pytz==2026.3.post1
wikipedia==1.4.0
pillow==12.3.0
weasyprint==69.0
pandas==3.0.5
python-dateutil==2.9.0.post0
pdf2image==1.17.0
pyYAML==6.0.3
SQLAlchemy==2.0.51
asyncpg==0.31.0
48 changes: 29 additions & 19 deletions src/handlers/tables_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
from aiogram import Bot, Router, flags, types
from aiogram.enums import ChatAction
from aiogram.filters import Command, CommandObject
from aiogram.types import error_event
from aiogram.types import ReplyParameters, error_event
from aiogram.utils import formatting

from src.bot.settings import settings
from src.database import Request
from src.utility.tools import table
from src.utility.rich_table import table_message

table_router = Router(name=__name__)


@table_router.message(Command("show_queue"))
@flags.chat_action(action=ChatAction.UPLOAD_PHOTO)
@flags.chat_action(action=ChatAction.TYPING)
async def show_queue(message: types.Message, request: Request) -> None:
"""Show the queue of trophies."""

Expand All @@ -33,17 +33,21 @@ async def show_queue(message: types.Message, request: Request) -> None:
)

if len(data) > 0:
media = table(
data, columns=["Nickname", "Game", "Platform"], caption="Очередь трофеев"
await message.bot.send_rich_message(
chat_id=chat_id,
rich_message=table_message(
data,
columns=["Nickname", "Game", "Platform"],
caption="Очередь трофеев",
),
reply_parameters=ReplyParameters(message_id=message.message_id),
)
if len(media) > 0:
await message.reply_media_group(media=media)
else:
await message.reply(text="Список пуст!")


@table_router.message(Command("top"))
@flags.chat_action(action=ChatAction.UPLOAD_PHOTO)
@flags.chat_action(action=ChatAction.TYPING)
async def top(message: types.Message, command: CommandObject, request: Request) -> None:
"""Show the top of the chat."""

Expand All @@ -62,7 +66,6 @@ async def top(message: types.Message, command: CommandObject, request: Request)
data = []
try:
data = await request.get_top(chat_id=chat_id, date=date)
print(data)
except Exception as e:
await message.reply(
text=f"Не удалось получить топ от {date}\n\n"
Expand All @@ -71,15 +74,20 @@ async def top(message: types.Message, command: CommandObject, request: Request)
)

if len(data) > 0:
media = table(data, columns=["Nickname", "Trophies"], caption=caption)
if len(media) > 0:
await message.reply_media_group(media=media)
# Pilot of the native table block; the other commands still send images.
await message.bot.send_rich_message(
chat_id=chat_id,
rich_message=table_message(
data, columns=["Nickname", "Trophies"], caption=caption
),
reply_parameters=ReplyParameters(message_id=message.message_id),
)
else:
await message.reply("Список пуст!")


@table_router.message(Command("history"))
@flags.chat_action(action=ChatAction.UPLOAD_PHOTO)
@flags.chat_action(action=ChatAction.TYPING)
async def get_history(message: types.Message, command: CommandObject, request: Request):
"""Get the history of the user's trophies."""

Expand Down Expand Up @@ -107,13 +115,15 @@ async def get_history(message: types.Message, command: CommandObject, request: R

if len(data) > 0:
display_name = ("@" + username) if username else message.from_user.full_name
media = table(
data,
columns=["Game", "Date", "Platform"],
caption=f"Список всех трофеев: {display_name}",
await message.bot.send_rich_message(
chat_id=chat_id,
rich_message=table_message(
data,
columns=["Game", "Date", "Platform"],
caption=f"Список всех трофеев: {display_name}",
),
reply_parameters=ReplyParameters(message_id=message.message_id),
)
if len(media) > 0:
await message.reply_media_group(media=media)
else:
await message.reply("Список пуст!")

Expand Down
30 changes: 18 additions & 12 deletions src/scheduler/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from src.database.connect import Request
from src.database.schemas import Scores
from src.keyboards.inline import GameSurveyCallbackData, build_start_survey_keyboard
from src.utility.tools import table
from src.utility.rich_table import heading_block, rich_message, table_block


async def change_avatar(bot: Bot, request: Request, chat_id: int, where_run: dict):
Expand Down Expand Up @@ -79,7 +79,7 @@ async def finish_survey(bot: Bot, request: Request, chat_id: int):
# Get Last day of the previous month
date_end = now.replace(day=1, hour=0, minute=0, second=0)

media = []
tables = []
trophy_ids = set()
for score in (Scores.game, Scores.picture, Scores.difficulty):
match score:
Expand All @@ -100,29 +100,35 @@ async def finish_survey(bot: Bot, request: Request, chat_id: int):

trophy_ids.update(row[0] for row in results)

media.extend(table(data=[(row[1], row[2], row[3]) for row in results],
columns=["Hunter", "Game", "Score"],
name=text))
tables.append(table_block(data=[(row[1], row[2], row[3]) for row in results],
columns=["Hunter", "Game", "Score"],
caption=text))
await request.add_survey_history(score_type=score.key,
data=[(row[0], row[3]) for row in results])

if len(media) == 0:
if len(tables) == 0:
logging.warning(
(
f"{__name__}: The survey has not been conducted "
f"or has already been completed in chat {chat_id}"
)
)
else:
# Send tables with survey results
# aiogram models are frozen, so replace the entry instead of mutating it.
media[0] = media[0].model_copy(
update={"caption": f"Результаты опроса за {date_start.strftime('%m.%Y')}"}
# One message titled by a heading, with a table per category.
survey_message = await bot.send_rich_message(
chat_id=chat_id,
rich_message=rich_message(
[
heading_block(
f"Результаты опроса за {date_start.strftime('%m.%Y')}"
),
*tables,
]
),
)
survey_message = await bot.send_media_group(chat_id=chat_id, media=media)

# Pin Survey Message
await bot.pin_chat_message(chat_id=chat_id, message_id=survey_message[0].message_id)
await bot.pin_chat_message(chat_id=chat_id, message_id=survey_message.message_id)

# Delete scores for calculated trophy_ids
await request.delete_scores(trophy_ids=trophy_ids)
Expand Down
93 changes: 93 additions & 0 deletions src/utility/rich_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Build Telegram rich messages that carry native tables.

Telegram has carried table blocks since Bot API 10.1, so the rows are handed
over as data and drawn by the client. That replaces rendering them to a PNG
through pandas, WeasyPrint, poppler and Pillow: the result is selectable and
searchable, and the bot needs no native libraries at all.
"""

from collections.abc import Iterable, Sequence
from typing import Any

from aiogram.types import (
InputRichBlockSectionHeading,
InputRichBlockTable,
InputRichBlockUnion,
InputRichMessage,
RichBlockTableCell,
)

# Numeric columns read better flush right; everything else stays left.
RIGHT_ALIGNED_COLUMNS = frozenset({"Trophies", "Score"})

# Header of the row-number column, matching the 1-based index of the old tables.
ROW_NUMBER_HEADER = "#"


def _cell(value: Any, align: str, is_header: bool = False) -> RichBlockTableCell:
"""Wrap a single value into a table cell."""

return RichBlockTableCell(
align=align,
valign="middle",
text=str(value),
# Passing False would still mark the cell, so send nothing instead.
is_header=True if is_header else None,
)


def table_block(
data: Iterable[Sequence[Any]],
columns: Sequence[str],
caption: str | None = None,
) -> InputRichBlockTable:
"""Lay the rows out as one table block.

The first column numbers the rows, the way the rendered tables did through
the DataFrame index.
"""

alignments = [
"right" if column in RIGHT_ALIGNED_COLUMNS else "left" for column in columns
]

header = [_cell(ROW_NUMBER_HEADER, "right", is_header=True)]
header += [
_cell(column, alignment, is_header=True)
for column, alignment in zip(columns, alignments)
]

rows = []
for number, row in enumerate(data, start=1):
cells = [_cell(number, "right")]
cells += [_cell(value, alignment) for value, alignment in zip(row, alignments)]
rows.append(cells)

return InputRichBlockTable(
cells=[header, *rows],
is_bordered=True,
is_striped=True,
caption=caption,
)


def heading_block(text: str, size: int = 3) -> InputRichBlockSectionHeading:
"""A section heading, used to title a message holding several tables."""

return InputRichBlockSectionHeading(text=text, size=size)


def rich_message(blocks: Sequence[InputRichBlockUnion]) -> InputRichMessage:
"""Wrap blocks into a message ready for sendRichMessage."""

return InputRichMessage(blocks=list(blocks))


def table_message(
data: Iterable[Sequence[Any]],
columns: Sequence[str],
caption: str | None = None,
) -> InputRichMessage:
"""Shorthand for a message holding a single table."""

return rich_message([table_block(data, columns, caption)])
70 changes: 0 additions & 70 deletions src/utility/tools.py

This file was deleted.

Loading
Loading