Skip to content

Dev/2.2.0#64

Merged
ink-developer merged 22 commits into
mainfrom
dev/2.2.0
Jun 13, 2026
Merged

Dev/2.2.0#64
ink-developer merged 22 commits into
mainfrom
dev/2.2.0

Conversation

@ink-developer

@ink-developer ink-developer commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Описание

Подготавливает релиз PyMax 2.2.0.

  • Добавлены получение и редактирование сообщений.
  • Добавлены события typing, presence, read и reaction update.
  • Добавлены вступление в каналы и автоматическая SMS-регистрация.
  • Исправлены UTF-16 позиции Markdown для emoji.
  • Улучшена обработка удаления сообщений и частичных presence-событий.
  • Тип идентификатора в read_message() теперь сохраняется.
  • Обновлены документация, версия пакета и release notes.
  • Добавлена поддержка Python 3.14.

Тип изменений

  • Исправление бага
  • Новая функциональность
  • Улучшение документации
  • Рефакторинг

Связанные задачи / Issue

#62

Summary by CodeRabbit

Release Notes: PyMax 2.2.0

  • New Features

    • Message editing with text and attachment support
    • Message retrieval by ID or list of IDs
    • New event handlers: message read status, typing notifications, user presence, reaction updates
    • Channel joining via invite links
    • Account registration auto-completion with RegistrationConfig
    • Python 3.14 support
  • Bug Fixes

    • UTF-16 emoji positioning in formatted messages
    • Message deletion recognition across clients
    • Consistent message ID typing between TCP and WebSocket clients
  • Documentation

    • Comprehensive migration guide and API updates
    • New event type documentation

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

PyMax 2.2.0 adds four new event types (message read, typing, presence, reaction), message retrieval and editing APIs, SMS registration completion via RegistrationConfig, and a new API model binding infrastructure. Entity offsets in markdown are now computed using UTF-16 code units. Documentation, tests, and version metadata have been updated accordingly.

Changes

PyMax 2.2.0 Release

Layer / File(s) Summary
Event type models and infrastructure
src/pymax/types/events/{mark,typing,presence,reaction}.py, src/pymax/types/events/message.py, src/pymax/types/events/__init__.py, src/pymax/dispatch/enums.py
Four new event models (MessageReadEvent, TypingEvent, PresenceEvent, ReactionUpdateEvent) introduced; MessageDeleteEvent updated with normalize_payload() validator to handle multiple opcode payload shapes; module exports and enum members expanded.
Registration configuration and SMS auth flow
src/pymax/config.py, src/pymax/api/auth/{payloads,service}.py, src/pymax/auth/sms.py
New RegistrationConfig model for SMS account completion (first/last name); ClientConfig and ExtraConfig extended with optional registration_config field; AuthService.confirm_registration() method added; SmsAuthFlow branches on register_token and calls confirm_registration() when config present.
API model binding infrastructure and integration
src/pymax/api/binding.py, src/pymax/api/{auth,chats,messages,self,users}/service.py
New binding.py module with bind_api_model() and bind_api_models() functions for recursive domain-object binding with cycle detection; integrated across AuthService (login), ChatService, MessageService, SelfService, UserService to replace per-model .bind() calls.
Message retrieval, editing, and domain integration
src/pymax/api/messages/{enums,payloads,service}.py, src/pymax/infra/{chat,message}.py, src/pymax/types/domain/{chat,message}.py
MessageService adds get_messages(), get_message(), edit_message() with attachment support, markdown formatting, and chat_id backfilling; MessagePayloadKey enum and payloads extended; MessageMixin, Chat, Message domain wrappers added; read_message() signature updated to accept int|str.
Event dispatch and routing wiring
src/pymax/dispatch/{dispatcher,router,enums,mapping,resolvers}.py
Dispatcher and Router gain handler registration methods for new events; EventType enum expanded; EVENT_MAP routes new opcodes; EventMapper refactored to use bind_api_model() for message/chat events; new resolver functions added; message status mapping updated.
Client handler methods and domain binding
src/pymax/base.py, src/pymax/infra/chat.py, src/pymax/infra/message.py, src/pymax/types/domain/{chat,message,presence}.py
BaseClient gains on_message_read(), on_typing(), on_presence(), on_reaction_update() handler methods; registration_config forwarded to ClientConfig; Chat adds join_channel(), get_message(), get_messages(); Message adds edit(); pinned_message binding added; Presence.status made optional.
Markdown entity offset calculation using UTF-16 code units
src/pymax/formatting/markdown.py
Formatter adds BMP_MAX constant and _code_units_len() method; entity offset/length calculations switched from Python string code points to UTF-16 code units; LINK, HEADING, QUOTE text scanning, and character fallthrough updated to increment cursor by 2 for non-BMP surrogate pairs.
Documentation, configuration, version updates, and test coverage
pyproject.toml, src/pymax/__init__.py, docs/**, tests/**, .github/workflows/tests.yml
Version bumped to 2.2.0; Python 3.14 classifier and GitHub Actions test workflow added; docs URL changed to docs.pymax.org; tooling configs expanded (uv, pyright, ruff); comprehensive API/router/event documentation pages and release notes added; test suites expanded with registration, binding, event dispatch, message operation coverage; code formatting normalized throughout.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Possibly related PRs

  • MaxApiTeam/PyMax#63: Both PRs modify src/pymax/formatting/markdown.py to compute markdown entity offsets using UTF-16 code units, with identical changes to BMP_MAX, _code_units_len(), and cursor advancement logic for non-BMP characters.
  • MaxApiTeam/PyMax#34: The main PR's additions to read_message, ReadMessagesPayload, and read-receipt event wiring build directly on the retrieved PR's introduction of message read-state types and event infrastructure.

Poem

🐰 Hop! New events spring to life—
Read, type, presence, reactions in flight!
Messages edit with UTF-16 care,
Registration complete in the SMS air!
Binding binds deep, tests glow bright! 🌟

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/2.2.0

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pymax/session/store.py (2)

74-84: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix SQL injection risk in ALTER TABLE statement (line 84).

The _ensure_column method builds an SQL query using an f-string without parameterization. While the current hardcoded calls are safe, this pattern violates SQL injection prevention best practices and is vulnerable if the method is called with untrusted input.

Although column names cannot be parameterized in SQL, they should be validated or safely quoted:

🔒️ Proposed fix using identifier quoting
     async def _ensure_column(
         self,
         conn: aiosqlite.Connection,
         name: str,
         definition: str,
     ) -> None:
         async with conn.execute("PRAGMA table_info(sessions)") as cursor:
             columns = {row["name"] for row in await cursor.fetchall()}

         if name not in columns:
-            await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
+            # Use square brackets to safely quote identifier; definition is internal (hardcoded in _initialize_db)
+            await conn.execute(f"ALTER TABLE sessions ADD COLUMN [{name}] {definition}")

Alternatively, if stricter validation is preferred:

     async def _ensure_column(
         self,
         conn: aiosqlite.Connection,
         name: str,
         definition: str,
     ) -> None:
+        # Whitelist allowed column names to prevent injection
+        if name not in {
+            "mt_instance_id", "chats_sync", "contacts_sync",
+            "drafts_sync", "presence_sync", "config_hash"
+        }:
+            raise ValueError(f"Unknown session column: {name}")
+
         async with conn.execute("PRAGMA table_info(sessions)") as cursor:
             columns = {row["name"] for row in await cursor.fetchall()}

         if name not in columns:
-            await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
+            await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pymax/session/store.py` around lines 74 - 84, The ALTER TABLE
construction in _ensure_column builds SQL with an f-string which is unsafe;
validate and safely quote the column identifier before interpolating and also
validate the definition string. Update _ensure_column to: (1) validate name
against a strict identifier regex (e.g. ^[A-Za-z_][A-Za-z0-9_]*$) and raise on
mismatch; (2) escape and quote the identifier for SQLite by wrapping in double
quotes and doubling any internal double quotes (name_escaped = '"' +
name.replace('"', '""') + '"'); (3) validate definition against an allowed
pattern or whitelist (e.g. only types/keywords you expect) or raise if it
contains suspicious chars; then use conn.execute(f'ALTER TABLE sessions ADD
COLUMN {name_escaped} {definition}') knowing the identifier is safe and
definition has been validated. Ensure these checks are applied in the
_ensure_column function and raise a clear error for invalid inputs.

Source: Linters/SAST tools


147-164: ⚠️ Potential issue | 🟡 Minor

Update call-site check for load_session_by_device_id.

load_session_by_device_id uses a parameterized WHERE device_id = ? query as expected, but this repository never calls it outside unit tests (App only calls store.load_session() on startup). If the protocol flow is supposed to retrieve sessions by device_id during runtime/reconnect, this method isn’t currently wired—either connect the expected call path or remove/de-scope the unused API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pymax/session/store.py` around lines 147 - 164, The repository defines
load_session_by_device_id in SessionStore but no runtime code calls it (only
tests), so either wire it into the session/reconnect flow or remove it; to fix,
locate the reconnect/startup path in App (where store.load_session() is called)
and replace or augment that call to use
store.load_session_by_device_id(device_id) when a device_id is available (ensure
device_id is passed into the App startup/reconnect routine and handle None
result the same way load_session() did), or if the repo design never requires
device lookup by id, remove the load_session_by_device_id method and update its
unit tests to use the public store.load_session() API instead.
🧹 Nitpick comments (1)
src/pymax/types/events/message.py (1)

67-74: Opcode-128 delete normalization expects top-level chatId in exercised web “removed message” payloads. The dispatcher test fixture for the web removed-message delete event supplies top-level "chatId" even when "message" omits it, so the current data.get("chatId", ...) path won’t fail in the covered case. For robustness against payloads where chatId exists only under message, add a fallback to extract chatId/chat_id from message in the opcode-128 branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pymax/types/events/message.py` around lines 67 - 74, In the opcode-128
branch where you extract message/message_id/chat_id, add a fallback to pull
chatId/chat_id from the nested message when top-level chatId is missing: after
computing message (and message_id), set chat_id = data.get("chatId",
data.get("chat_id")) or (message.get("chatId", message.get("chat_id")) if
isinstance(message, dict) else getattr(message, "chatId", getattr(message,
"chat_id", None))); keep the existing early-return check that returns data if
chat_id or message_id is still None. This ensures the branch handles payloads
that only include chatId inside message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/tests.yml:
- Around line 22-23: Update the GitHub Actions workflow step that currently
reads "name: Checkout repository" with "uses: actions/checkout@v6" and any other
actions referenced by tag (e.g., "astral-sh/setup-uv@v8.1.0") to use specific
commit SHAs instead of tags, and add "with: persist-credentials: false" under
each actions/checkout step to disable credential persistence; ensure you replace
the tag references with the corresponding full commit SHA for actions/checkout
and astral-sh/setup-uv and add the persist-credentials setting for every
checkout step in the workflow.

---

Outside diff comments:
In `@src/pymax/session/store.py`:
- Around line 74-84: The ALTER TABLE construction in _ensure_column builds SQL
with an f-string which is unsafe; validate and safely quote the column
identifier before interpolating and also validate the definition string. Update
_ensure_column to: (1) validate name against a strict identifier regex (e.g.
^[A-Za-z_][A-Za-z0-9_]*$) and raise on mismatch; (2) escape and quote the
identifier for SQLite by wrapping in double quotes and doubling any internal
double quotes (name_escaped = '"' + name.replace('"', '""') + '"'); (3) validate
definition against an allowed pattern or whitelist (e.g. only types/keywords you
expect) or raise if it contains suspicious chars; then use conn.execute(f'ALTER
TABLE sessions ADD COLUMN {name_escaped} {definition}') knowing the identifier
is safe and definition has been validated. Ensure these checks are applied in
the _ensure_column function and raise a clear error for invalid inputs.
- Around line 147-164: The repository defines load_session_by_device_id in
SessionStore but no runtime code calls it (only tests), so either wire it into
the session/reconnect flow or remove it; to fix, locate the reconnect/startup
path in App (where store.load_session() is called) and replace or augment that
call to use store.load_session_by_device_id(device_id) when a device_id is
available (ensure device_id is passed into the App startup/reconnect routine and
handle None result the same way load_session() did), or if the repo design never
requires device lookup by id, remove the load_session_by_device_id method and
update its unit tests to use the public store.load_session() API instead.

---

Nitpick comments:
In `@src/pymax/types/events/message.py`:
- Around line 67-74: In the opcode-128 branch where you extract
message/message_id/chat_id, add a fallback to pull chatId/chat_id from the
nested message when top-level chatId is missing: after computing message (and
message_id), set chat_id = data.get("chatId", data.get("chat_id")) or
(message.get("chatId", message.get("chat_id")) if isinstance(message, dict) else
getattr(message, "chatId", getattr(message, "chat_id", None))); keep the
existing early-return check that returns data if chat_id or message_id is still
None. This ensures the branch handles payloads that only include chatId inside
message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0334e72e-6b5a-45e3-9398-04b413038158

📥 Commits

Reviewing files that changed from the base of the PR and between 6cd0525 and b0c5fce.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (84)
  • .github/workflows/tests.yml
  • docs/api/client-client.rst
  • docs/api/client-config.rst
  • docs/api/client-web.rst
  • docs/api/client.rst
  • docs/auth.rst
  • docs/client.rst
  • docs/index.rst
  • docs/messages.rst
  • docs/release-2-2-0.rst
  • docs/router.rst
  • docs/types/index.rst
  • docs/types/message_read_event.rst
  • docs/types/presence_event.rst
  • docs/types/reaction_update_event.rst
  • docs/types/typing_event.rst
  • pyproject.toml
  • src/pymax/__init__.py
  • src/pymax/api/auth/payloads.py
  • src/pymax/api/auth/service.py
  • src/pymax/api/binding.py
  • src/pymax/api/chats/service.py
  • src/pymax/api/messages/enums.py
  • src/pymax/api/messages/payloads.py
  • src/pymax/api/messages/service.py
  • src/pymax/api/models.py
  • src/pymax/api/response.py
  • src/pymax/api/self/service.py
  • src/pymax/api/session/payloads.py
  • src/pymax/api/session/service.py
  • src/pymax/api/uploads/payloads.py
  • src/pymax/api/uploads/service.py
  • src/pymax/api/users/service.py
  • src/pymax/app.py
  • src/pymax/auth/qr.py
  • src/pymax/auth/sms.py
  • src/pymax/base.py
  • src/pymax/client.py
  • src/pymax/client_web.py
  • src/pymax/config.py
  • src/pymax/connection/connection.py
  • src/pymax/connection/readers/tcp.py
  • src/pymax/dispatch/dispatcher.py
  • src/pymax/dispatch/enums.py
  • src/pymax/dispatch/mapping.py
  • src/pymax/dispatch/resolvers.py
  • src/pymax/dispatch/router.py
  • src/pymax/formatting/markdown.py
  • src/pymax/infra/chat.py
  • src/pymax/infra/message.py
  • src/pymax/logging.py
  • src/pymax/protocol/tcp/compression.py
  • src/pymax/protocol/tcp/framing.py
  • src/pymax/protocol/ws/protocol.py
  • src/pymax/session/protocol.py
  • src/pymax/session/store.py
  • src/pymax/telemetry/navigation.py
  • src/pymax/telemetry/service.py
  • src/pymax/transport/tcp.py
  • src/pymax/types/domain/attachments/unknown.py
  • src/pymax/types/domain/auth.py
  • src/pymax/types/domain/chat.py
  • src/pymax/types/domain/message.py
  • src/pymax/types/domain/presence.py
  • src/pymax/types/domain/sync.py
  • src/pymax/types/events/__init__.py
  • src/pymax/types/events/mark.py
  • src/pymax/types/events/message.py
  • src/pymax/types/events/presence.py
  • src/pymax/types/events/reaction.py
  • src/pymax/types/events/typing.py
  • tests/api/test_auth_service.py
  • tests/api/test_chat_user_self_session_services.py
  • tests/api/test_message_service.py
  • tests/api/test_upload_service.py
  • tests/app/test_app_runtime.py
  • tests/auth/test_auth_flows.py
  • tests/conftest.py
  • tests/connection/test_connection.py
  • tests/connection/test_readers_and_transports.py
  • tests/dispatch/test_dispatcher.py
  • tests/domain/test_bound_models.py
  • tests/files/test_files_and_formatting.py
  • tests/protocol/test_protocols.py

Comment on lines +22 to +23
- name: Checkout repository
uses: actions/checkout@v6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/tests.yml"

echo "== uses entries not pinned to 40-char SHA =="
rg -nP '^\s*uses:\s*[^@\s]+@(?!(?:[0-9a-fA-F]{40})\b)' "$file" || true

echo
echo "== checkout blocks (verify persist-credentials: false is present) =="
rg -nP '^\s*uses:\s*actions/checkout@' -A4 "$file" || true

Repository: MaxApiTeam/PyMax

Length of output: 596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/tests.yml"

echo "== occurrences of persist-credentials =="
rg -n "persist-credentials" "$file" || true

echo
echo "== full blocks around checkout steps (lines 15-35 and 40-60) =="
nl -ba "$file" | sed -n '1,120p' | sed -n '15,60p'

Repository: MaxApiTeam/PyMax

Length of output: 228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/tests.yml"

echo "== occurrences of persist-credentials =="
rg -n "persist-credentials" "$file" || true

echo
echo "== checkout-related region (15-35) =="
cat -n "$file" | sed -n '15,35p'

echo
echo "== checkout-related region (45-65) =="
cat -n "$file" | sed -n '45,65p'

Repository: MaxApiTeam/PyMax

Length of output: 1447


Pin GitHub Actions to commit SHAs and disable checkout credential persistence.

  • .github/workflows/tests.yml uses actions/checkout@v6 and astral-sh/setup-uv@v8.1.0 (tag-based, not full commit SHAs).
  • Both actions/checkout steps lack with: persist-credentials: false.
🔧 Suggested hardening diff
-      - name: Checkout repository
-        uses: actions/checkout@v6
+      - name: Checkout repository
+        uses: actions/checkout@<full-length-commit-sha>
+        with:
+          persist-credentials: false
@@
-      - name: Install uv
-        uses: astral-sh/setup-uv@v8.1.0
+      - name: Install uv
+        uses: astral-sh/setup-uv@<full-length-commit-sha>
@@
-      - name: Checkout repository
-        uses: actions/checkout@v6
+      - name: Checkout repository
+        uses: actions/checkout@<full-length-commit-sha>
+        with:
+          persist-credentials: false
@@
-      - name: Install uv
-        uses: astral-sh/setup-uv@v8.1.0
+      - name: Install uv
+        uses: astral-sh/setup-uv@<full-length-commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 22-23: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 23-23: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/tests.yml around lines 22 - 23, Update the GitHub Actions
workflow step that currently reads "name: Checkout repository" with "uses:
actions/checkout@v6" and any other actions referenced by tag (e.g.,
"astral-sh/setup-uv@v8.1.0") to use specific commit SHAs instead of tags, and
add "with: persist-credentials: false" under each actions/checkout step to
disable credential persistence; ensure you replace the tag references with the
corresponding full commit SHA for actions/checkout and astral-sh/setup-uv and
add the persist-credentials setting for every checkout step in the workflow.

Source: Linters/SAST tools

@ink-developer ink-developer merged commit 78b7ffe into main Jun 13, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants