Skip to content

Take the photo token by position, not from the upload URL's photoIds - #107

Open
farafonoff wants to merge 2 commits into
MaxApiTeam:mainfrom
farafonoff:fix/photo-upload-index-token
Open

farafonoff wants to merge 2 commits into
MaxApiTeam:mainfrom
farafonoff:fix/photo-upload-index-token

Conversation

@farafonoff

@farafonoff farafonoff commented Oct 4, 2026 •

Copy link
Copy Markdown

MAX stopped returning a photoIds query parameter on the photo upload URL, so parse_qs(urlparse(url).query)["photoIds"][0] raises KeyError and every photo upload fails with UploadError: Photo upload URL does not contain photoIds. Not intermittent, not rate limiting: no photo can upload at all. Reported as happening against the live API on 2026-09-30, ~2 days after the last successful photo forward.

Captured from the official MAX client, one photo:

REQUEST  op=80 PHOTO_UPLOAD  {"count": 1}
RESPONSE op=80  {"url": "https://iu.oneme.ru/uploadImage?r=<token>"}
POST     that url  ->  200  {"photos": {"0": {"token": "..."}}}

The official client annotates this itself ("phase 1/3 url has no media id").

The id was never needed. A multi-photo message is several independent count: 1 PHOTO_UPLOAD requests, each with its own one-shot URL and its own single-entry result, whose tokens then go out together in one MSG_SEND -- verified with a two-photo capture (seq 80/81 issued in parallel, two POSTs, two photos["0"] results, one MSG_SEND with both photoTokens). So the url's photo id only ever served to index that one dict. Read it positionally.

Keying is deliberately not hardcoded to "0": whatever key comes back is fine, as long as there is exactly one photo. A result with zero or several entries now raises instead of guessing -- without an id in the URL there is no way to tell which entry is ours, and silently attaching photo 1 of 2 to a message is far worse than a failed upload.

Videos and files are untouched: they take url plus video_id/file_id and token straight from their own response models and never parsed an id out of a URL.

Tests: the four new cases (no photoIds in the URL, a result keyed by photo id, an ambiguous multi-photo result, an empty result, and a two-photo album being two separate count=1 uploads) all fail against the previous code with KeyError: 'photoIds' and pass here. The existing test was asserting the old ?photoIds=photo-1 URL shape and no longer described the protocol.

Описание

Кратко, что делает этот PR. Например, добавляет новый метод, исправляет баг, улучшает документацию.

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

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

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

Ссылка на issue, если есть: #

Тестирование

Покажите пример кода, который проверяет изменения:

import pymax

# пример использования нового функционала


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Исправления**
  * Исправлена загрузка фотографий по одноразовым ссылкам: токен теперь берётся из ответа сервера, а не извлекается из URL. Это позволяет корректно обрабатывать ссылки с разными параметрами.
  * Если сервер возвращает ноль или несколько фотографий для одной загрузки, операция завершается ошибкой вместо возврата неоднозначного результата.
  * Фотографии альбома загружаются отдельными запросами, и для каждой возвращается собственный токен.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

MAX stopped returning a `photoIds` query parameter on the photo upload URL,
so `parse_qs(urlparse(url).query)["photoIds"][0]` raises KeyError and **every**
photo upload fails with `UploadError: Photo upload URL does not contain
photoIds`. Not intermittent, not rate limiting: no photo can upload at all.
Reported as happening against the live API on 2026-09-30, ~2 days after the
last successful photo forward.

Captured from the official MAX client, one photo:

    REQUEST  op=80 PHOTO_UPLOAD  {"count": 1}
    RESPONSE op=80  {"url": "https://iu.oneme.ru/uploadImage?r=<token>"}
    POST     that url  ->  200  {"photos": {"0": {"token": "..."}}}

The official client annotates this itself ("phase 1/3 url has no media id").

The id was never needed. A multi-photo message is several *independent*
`count: 1` PHOTO_UPLOAD requests, each with its own one-shot URL and its own
single-entry result, whose tokens then go out together in one MSG_SEND --
verified with a two-photo capture (seq 80/81 issued in parallel, two POSTs,
two `photos["0"]` results, one MSG_SEND with both photoTokens). So the url's
photo id only ever served to index that one dict. Read it positionally.

Keying is deliberately not hardcoded to "0": whatever key comes back is fine,
as long as there is exactly one photo. A result with zero or several entries
now raises instead of guessing -- without an id in the URL there is no way to
tell which entry is ours, and silently attaching photo 1 of 2 to a message is
far worse than a failed upload.

Videos and files are untouched: they take url plus video_id/file_id and token
straight from their own response models and never parsed an id out of a URL.

Tests: the four new cases (no photoIds in the URL, a result keyed by photo id,
an ambiguous multi-photo result, an empty result, and a two-photo album being
two separate count=1 uploads) all fail against the previous code with
KeyError: 'photoIds' and pass here. The existing test was asserting the old
`?photoIds=photo-1` URL shape and no longer described the protocol.
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d13e0f43-606f-4cf2-8e88-81dd851ecc8a
📥 Commits

Reviewing files that changed from the base of the PR and between f560e63 and 433d096.

📒 Files selected for processing (2)
  • src/pymax/api/uploads/service.py
  • tests/api/test_upload_service.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/api/test_upload_service.py
  • src/pymax/api/uploads/service.py

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.


Walkthrough

upload_photo больше не извлекает ID фото из URL. Метод возвращает токен, если ответ содержит ровно одну фотографию, и выбрасывает UploadError при пустом или многозаписном ответе. Тесты проверяют отдельные запросы для фотографий альбома.

Changes

Загрузка фотографий

Layer / File(s) Summary
Выбор токена и проверка загрузки
src/pymax/api/uploads/service.py, tests/api/test_upload_service.py
upload_photo получает токен из единственной записи ответа и выбрасывает UploadError при другом количестве записей. Тесты проверяют одноразовые URL, разные ключи результата и отдельные запросы с count: 1 для фотографий альбома.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 433d0

Photo uploads no longer depend on an ID in the upload URL. The inspected paths show no remaining issue that needs resolution before merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f560e

Photo uploads now reject empty or ambiguous results before publishing an attachment. The change is narrowly scoped, but correct photo association still depends on the remote upload service binding each response to its request.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — An incorrectly associated token could affect the caller-selected message attachment, self-profile photo, or group-profile photo. These are existing consumers; the inspected change introduces no additional destination or client-side token-sharing mechanism. Server-side authorization determines any broader asset or account exposure and was not established here.

Trust Boundaries and Controls

  • observed — The client takes the upload destination from PHOTO_UPLOAD, posts the caller's photo to that destination, requires HTTP success, validates the response model, and rejects non-singleton photo collections before returning a publication token. The new extractor neither chooses the destination nor changes downstream publication opcodes.

Resilience and Maintainability Implications

  • inferred — URLs, response models, and extracted tokens remain local to each upload invocation, with no shared extractor state through which repeated or concurrent calls could cross-select tokens. The existing message recovery path reuses the constructed frame rather than re-uploading or replacing photo tokens.
  • inferred — Upload creation and publication remain separate operations. A later album failure, cancellation, or publication failure can leave earlier remote uploads without publication; the inspected client flow contains no compensating deletion. This is an existing lifecycle limitation, not an established security regression from token selection, and remote expiration remains unknown.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Заголовок точно и кратко описывает основное изменение: получение токена фото из единственной записи ответа, а не из параметра URL.
Description check ✅ Passed Описание содержит причину сбоя, объясняет новое поведение и перечисляет проверки. Тип изменения отмечен. Ссылка на задачу и пример кода оставлены как шаблонные заполнители, но описание в целом полное.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Кролик проверил ответ,
Токен найден среди фото.
Если запись там одна,
Сервис возвращает токен.
Если записей нет иль много,
UploadError возникнет.
Альбом грузится по снимку.

Comment @coderabbitai help to get the list of available commands.

Docstring coverage over the functions in this diff was 5/8. Adds them for
`UploadService` (what the service is for, and that videos/files/voices only
complete once the server emits their processing event), `UploadService.__init__`,
`upload_photo` (a 105-line public method that had none -- what it requests, why
the token is read positionally rather than by id, and everything that raises
UploadError), and the empty-result test. 8/8 now.

This branch has not been deployed

No deployments
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.

1 participant