# Feed API (v2.0.0) REST API для социальной ленты: публикация постов, лента подписок, рекомендации, трендинг, хэштеги. Введено в **v2.0.0**, заменило channel-модель. ## Архитектура Лента устроена гибридно — **метаданные on-chain**, **тела постов off-chain** в relay feed-mailbox: ``` ┌─────────────────────┐ ┌──────────────────────────┐ │ on-chain state │ │ feed mailbox (BadgerDB)│ │ │ │ │ │ post: → │ │ feedpost: → │ │ {author, size, │ POST │ {content, attachment, │ │ content_hash, │ /feed/publish │ hashtags, type, ts} │ │ hosting_relay, │───────────────────►│ │ │ reply_to?, │ │ feedview: → │ │ deleted?} │ │ uint64 (counter) │ │ │ │ │ │ follow:: │ │ feedtag::: │ │ like::│ │ (hashtag index) │ │ likecount: │ │ │ └─────────────────────┘ └──────────────────────────┘ ``` Разделение решает два противоречия: on-chain-состояние обеспечивает provable авторство + экономические стимулы (оплата за байт); off-chain тела позволяют хранить мегабайты картинок без раздувания истории блоков. Hosting relay получает плату автора (через CREATE_POST tx); другие ноды могут реплицировать через gossipsub (роадмап). ## Оплата постов Автор поста платит `BasePostFee + Size × PostByteFee` µT за публикацию. Полный fee кредитуется на баланс hosting-релея. | Константа | Значение | Примечание | |-----------|----------|-----------| | `BasePostFee` | 1 000 µT (0.001 T) | Совпадает с MinFee (block-validation floor) | | `PostByteFee` | 1 µT/byte | Размер = контент + attachment + ~128 B метаданных | | `MaxPostSize` | 256 KiB | Hard cap, больше — 413 на `/feed/publish` | Пример: - 200-байтовый текстовый пост → ~1 328 µT (~0.0013 T) - Текст + 30 KiB WebP-картинка → ~31 848 µT (~0.032 T) - Видео 200 KiB → ~205 128 µT (~0.21 T) ## Server-side metadata scrubbing **Обязательная** для всех аттачментов на `/feed/publish`: - **Изображения** (JPEG/PNG/GIF/WebP): decode → downscale до 1080 px → re-encode как JPEG Q=75 через stdlib. EXIF/GPS/ICC/XMP/MakerNote стираются by construction (stdlib JPEG encoder не пишет их). - **Видео/аудио**: переадресуется во внешний **FFmpeg-сайдкар** (контейнер `docker/media-sidecar/`). Если сайдкар не настроен через `DCHAIN_MEDIA_SIDECAR_URL` и оператор не выставил `DCHAIN_ALLOW_UNSCRUBBED_VIDEO`, видео-аплоад отклоняется с 503. - **MIME mismatch**: если magic-байты не соответствуют заявленному `Content-Type` — 400. Защита от PDF'а, замаскированного под картинку. ## Endpoints ### `POST /feed/publish` Загружает тело поста, запускает скраббинг, сохраняет в feed-mailbox, возвращает метаданные для последующей on-chain CREATE_POST tx. **Request:** ```json { "post_id": "", "author": "", "content": "Text of the post", "content_type": "text/plain", "attachment_b64": "", "attachment_mime": "image/jpeg", "reply_to": "", "quote_of": "", "sig": "", "ts": 1710000000 } ``` `sig = Ed25519.Sign(author_priv, "publish:::")` где `raw_content_hash = sha256(content + raw_attachment_bytes)` — клиентский хэш **до** серверного скраба. `ts` в пределах ±5 минут от серверного времени (anti-replay). **Response:** ```json { "post_id": "", "hosting_relay": "", "content_hash": "", "size": 1234, "hashtags": ["golang", "dchain"], "estimated_fee_ut": 2234 } ``` Клиент затем собирает CREATE_POST tx с этими полями и сабмитит на `/api/tx`. См. `buildCreatePostTx` в клиентской библиотеке. ### `GET /feed/post/{id}` Тело поста со статами. **Response:** ```json { "post_id": "...", "author": "...", "content": "...", "content_type": "text/plain", "hashtags": ["golang"], "created_at": 1710000000, "reply_to": "", "quote_of": "", "attachment": "", "attachment_mime": "image/jpeg" } ``` **Errors:** 404 если не найден; 410 если on-chain soft-deleted. ### `GET /feed/post/{id}/attachment` Сырые байты аттачмента с корректным `Content-Type`. Используется нативным image-loader'ом клиента: ```html ``` `Cache-Control: public, max-age=3600, immutable` — content-addressed ресурс, кэш безопасен. ### `POST /feed/post/{id}/view` Инкремент off-chain счётчика просмотров. Fire-and-forget, не требует подписи (per-view tx был бы нереалистичен). **Response:** `{"post_id": "...", "views": 42}` ### `GET /feed/post/{id}/stats?me=` Агрегат для отображения. `me` опционален — если передан, в ответе будет `liked_by_me`. **Response:** ```json { "post_id": "...", "views": 127, "likes": 42, "liked_by_me": true } ``` ### `GET /feed/author/{pub}?limit=N&before=` Посты конкретного автора, newest-first. Пагинация: передавайте `before=` самого старого уже загруженного поста, чтобы получить следующую страницу. **Response:** ```json {"author": "", "count": 20, "posts": [FeedPostItem]} ``` ### `GET /feed/timeline?follower=&limit=N&before=` Лента подписок: merged посты всех, на кого подписан follower, newest- first. Требует, чтобы on-chain FOLLOW-транзакции уже скоммитились. Пагинация через `before` идентично `/feed/author`. ### `GET /feed/trending?window=24&limit=N` Топ постов за последние N часов, ранжированы по `likes × 3 + views`. `window` — часы (1..168), по умолчанию 24. Пагинация **не поддерживается** — это топ-срез, не список. ### `GET /feed/foryou?pub=&limit=N` Рекомендации. Простая эвристика v2.0.0: 1. Кандидаты: посты последних 48 часов 2. Исключить: авторы, на которых уже подписан; уже лайкнутые; свои 3. Ранжировать: `likes × 3 + views + 1` (seed чтобы свежее вылезало) Никаких ML — база для `v2.2.0 Feed algorithm` (half-life decay, mutual-follow boost, hashtag-affinity). ### `GET /feed/hashtag/{tag}?limit=N` Посты, помеченные хэштегом `#tag`. Tag case-insensitive. Хэштеги авто-индексируются на `/feed/publish` из текста (regex `#[A-Za-z0-9_\p{L}]{1,40}`, dedup, cap 8 per post). ## On-chain события Клиент сабмитит эти транзакции через обычный `/api/tx` endpoint. ### `CREATE_POST` ```json { "type": "CREATE_POST", "fee": , "payload": { "post_id": "...", "content_hash": "", "size": 1234, "hosting_relay": "", "reply_to": "", "quote_of": "" } } ``` Валидации on-chain: `size > 0 && size <= MaxPostSize`, `fee >= BasePostFee + size × PostByteFee`, `reply_to` и `quote_of` взаимно исключают друг друга. Дубликат `post_id` отклоняется. ### `DELETE_POST` ```json {"type": "DELETE_POST", "fee": 1000, "payload": {"post_id": "..."}} ``` Только автор может удалить. Soft-delete (post остаётся в chain, но помечен `deleted=true`; читатели получают 410). ### `FOLLOW` / `UNFOLLOW` ```json {"type": "FOLLOW", "to": "", "fee": 1000, "payload": {}} ``` Двусторонний on-chain индекс: `follow::` + `followin::`. `UNFOLLOW` зеркально удаляет оба ключа. ### `LIKE_POST` / `UNLIKE_POST` ```json {"type": "LIKE_POST", "fee": 1000, "payload": {"post_id": "..."}} ``` Дубль-лайк отклоняется. Кэшированный counter `likecount:` хранит O(1) счётчик. ## Pricing / economics (резюме) | Операция | Cost | Куда уходит | |----------|------|-------------| | `CREATE_POST` | 1000 + size×1 µT | Hosting relay | | `DELETE_POST` | 1000 µT | Block validator | | `FOLLOW` / `UNFOLLOW` | 1000 µT | Block validator | | `LIKE_POST` / `UNLIKE_POST` | 1000 µT | Block validator | | `/feed/post/{id}/view` | 0 | Off-chain counter | Причина такого разделения: основные бизнес-действия (публикация) едут на «хостера контента», а мелкие социальные tx (лайк/подписка) — на валидатора сети, как обычные tx. ## Клиентские вспомогательные функции В `client-app/lib/feed.ts`: - `publishPost(params)` — подпись + POST /feed/publish - `publishAndCommit(params)` — publish + CREATE_POST tx в одну операцию - `fetchTimeline`, `fetchForYou`, `fetchTrending`, `fetchAuthorPosts`, `fetchHashtag`, `fetchPost`, `fetchStats`, `bumpView` - `buildCreatePostTx`, `buildDeletePostTx`, `buildFollowTx`, `buildUnfollowTx`, `buildLikePostTx`, `buildUnlikePostTx` - `likePost`, `unlikePost`, `followUser`, `unfollowUser`, `deletePost` (build + submitTx bundled)