# Открытые вопросы: вложения источников (media → Storage) и просмотр исходника Дата: 2026-09-11. Статус: решения владельца получены (см. §0). ## 0. Решения владельца (2026-09-11) - **А) Медиа-посты пропускаем.** Сообщения без текста (только медиа/вложение) в систему не попадают. Извлечение вложений Telegram и выгрузка их в Storage не делаются. Generic-контракт по-прежнему умеет нести `DataRef` — этим смогут пользоваться другие источники (файл/диск/таблица) и ручные вложения карточки. - **Б) Проверка без живого Telegram** — реализуем с юнит-тестами на фейковой сессии/фейковом Storage, без реального API. - **В)** Объяснение термина — в §1.5. **Решение: делаем.** Реализован remote-просмотр: `TelegramService.ReadSource`, `ITelegramGateway.ReadSourceAsync`, `TelegramSourceContentProvider` (Kind=telegram) в ядре, `GET /api/cards/{id}/source` и кнопка «Обновить из источника» в подробной карточке. ## 1.5. Что такое «remote-просмотр исходника» Карточка хранит **ссылку на источник** (`SourceRef`) и **содержимое** (`SourceContent`). Содержимое попадает в карточку в момент приёма. «Просмотр исходника» — это возможность по кнопке догрузить/показать **оригинальное сообщение у источника** (то, что было в канале/письме/строке), если контент в карточке устарел или урезан. Сейчас содержимое уже отдаётся в `CardDto.content` и через `GET /api/cards/{id}/source`. Для локальных источников этого достаточно. Для **внешних** источников (например Telegram) данные лежат не в ядре, а в сервисе-владельце; чтобы их догрузить, ядру нужен провайдер `ISourceContentProvider` для `kind`, который ходит по gRPC к сервису-владельцу (условный RPC `ReadSource(dialogId, msgId)`) и возвращает исходный текст/медиа. Это и есть «remote-просмотр» — расширение extension-point, которое не требуется до появления реальной необходимости (напр. если карточки хранят урезанный текст или нужно открыть живой первоисточник). ## 1. Что уже готово (не требует решений) - Единый контракт источника несёт вложения: `SourceContent.Data: IReadOnlyList` — ссылки на объекты Storage-сервиса (`DataRef.Id/Ref/Kind/MimeType/...`). - Контракт входящего потока (`src/contracts/sources.proto`, `PushSource`) передаёт `DataRefProto`/`ContactRefProto` — источник может прислать вложения сразу со ссылками. - Storage-сервис (`src/storage-service/Deal.Storage`, `storage.proto`) умеет `Upload/Download/Stat/Delete`, сам определяет `kind`/`mimeType`/размеры (контент-снифинг), бэкенд — MinIO. - Ядро хранит `SourceContent` карточки (в т.ч. `Data`) и отдаёт его в `CardDto.content` и через `GET /api/cards/{id}/source` (extension-point `ISourceContentProvider` + `SourceContentResolver`). - Фронт рендерит вложения: `SourceContentView.vue` (image/video/audio/document/archive по `kind`), ссылки и контакты — списками. ## 2. Проблемная часть (требует живого Telegram) Извлечение и выгрузка медиа из Telegram не проверяемы офлайн: 1. **Медиа-сообщения сейчас отбрасываются.** `TlMessageMapper.ToMessage` (`src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs`) принимает только `Message` с непустым `message` (текстом). Посты с одним вложением и подписью (`media` + `caption`) не попадают в поток вообще. Нужно: определять `Message.media`, читать `caption`, тип/размеры/длительность. 2. **Скачивание и выгрузка.** Требуется `client.DownloadMedia(...)` (WTelegram) → поток → `StorageService.Upload(meta + data)` → `DataRefProto`. В telegram-сервисе нет gRPC-клиента Storage и соответствующей конфигурации в compose (endpoint/токен). Проверить можно только с реальным Telegram-аккаунтом и живым MinIO. 3. **Подпись без текста.** Даже если вложение извлечено, в посте может не быть текста: нужен ли такой пост «карточкой» (сейчас `PipelineIngestService` пропускает записи без `Content.Text`)? Предлагается принимать запись, если есть текст **или** вложения/ссылки/контакты, а классификацию медиа-онли строить по подписи (`caption`) и метаданным. Требуется подтверждение продуктовой логики. 4. **Просмотр исходника из другого контура.** «Открыть исходник» для remote-источников (Telegram — это лишь один из них) требует провайдера `ISourceContentProvider`, который ходит по gRPC к сервису-владельцу источника (новый RPC, например `ReadSource(dialogId, msgId)`), возвращая текст/медиа. Это тоже живой Telegram. ## 3. Предлагаемый план (после подтверждения) 1. `TelegramMessage` расширить моделью `TelegramAttachment` (caption, fileName, mimeType, size, width, height, durationSec, `Task Open(cancellationToken)`), заполнять в `TlMessageMapper` из `Message.media`/`Document`/`Photo`. 2. В telegram-сервисе добавить `StorageClient` (gRPC, `Deal.Grpc.Storage`) + `SourceAttachmentUploader`: загрузка каждого вложения → `DataRefProto`. 3. `DialogProtoMapper.ToSourceRequest(message, dataRefs)` — прокинуть `content.data` и `caption`. 4. `PipelineIngestService`: принимать запись при непустом тексте **или** непустых `Data`/`Links`/`Contacts` (нужно продуктовое решение по п.2.3). 5. `ISourceContentProvider` для `kind="telegram"` — gRPC-провайдер к telegram-сервису (RPC `ReadSource`). 6. Настройки: endpoint/токен Storage в `deploy/compose.dev.yml`/`compose.prod.yml` для telegram-сервиса. ## 4. Что нужно от владельца - А) Делать ли медиа-сообщения без текста карточками (по подписи/метаданным), или пропускать? - Б) Для проверки вложений нужны живые Telegram api_id/api_hash и работающий MinIO — будет ли прогон на вашей стороне, или реализуем «слепо» с юнит-тестами на фейковой сессии и фейковом Storage? - В) Нужен ли remote-просмотр исходника (`ReadSource`) в этом объёме, или достаточно того, что содержимое хранится в карточке? Пока эти пункты не закрыты, они вынесены в `backlog.md` (`TD-STORE-ATTACH`, `TD-SOURCE-PROVIDER`), а generic-часть (контракт, Storage-сервис, хранение, API, рендер) реализована.