Files
dchain/docs/api/feed.md
vsecoder 1a8731f479 docs: update README + api docs + architecture for v2.0.0 feed
README
  - Mention social feed in the one-line description and feature bullets
  - Add relay + feed endpoint tables to the API overview (was
    previously empty on messaging)
  - List media/ package in the repo structure

docs/api/
  - New docs/api/feed.md: full reference for /feed/publish, fetch,
    stats, view, author, timeline, trending, foryou, hashtag; all
    on-chain CREATE_POST / DELETE_POST / FOLLOW / LIKE payloads;
    fee economics; server-side scrubbing contract.
  - docs/api/relay.md rewritten: /relay/broadcast is now the primary
    E2E path with a complete envelope schema; /relay/send kept but
    flagged ⚠ NOT E2E; DELETE /relay/inbox/{id} documented with the
    new Ed25519 signed-auth body.
  - docs/api/README.md index: added feed.md row.

docs/architecture.md
  - L2 Transport layer description updated to include the feed
    mailbox alongside the 1:1 relay mailbox.
  - New "Социальная лента (v2.0.0)" section right after the 1:1
    message flow: ASCII diagram of publish + on-chain commit +
    timeline fetch, economic summary, metadata-scrub summary.

docs/node/README.md
  - Removed stale chan:/chan-member: keys from the BadgerDB schema
    reference; replaced with the v2.0.0 feed keys (post:,
    postbyauthor:, follow:, followin:, like:, likecount:).

docs/update-system.md
  - Example features[] array updated to match the actual node output
    (channels_v1 removed, feed_v2 / media_scrub / relay_broadcast added).

Node feature flags
  - api_well_known_version.go: dropped channels_v1 tag (the
    /api/channels/:id endpoint was removed in the feed refactor);
    added feed_v2, media_scrub, relay_broadcast so clients can
    feature-detect the v2.0.0 surface.
  - Comment example updated channels_v2/v1 → feed_v3/v2.

Client
  - CLIENT_REQUIRED_FEATURES expanded to include the v2.0.0 feature
    flags the client now depends on (feed_v2, media_scrub,
    relay_broadcast); checkNodeVersion() will flag older nodes as
    unsupported and surface an upgrade prompt.

All 7 Go test packages green; tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:06:06 +03:00

280 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:<id> → │ │ feedpost:<id> → │
│ {author, size, │ POST │ {content, attachment, │
│ content_hash, │ /feed/publish │ hashtags, type, ts} │
│ hosting_relay, │───────────────────►│ │
│ reply_to?, │ │ feedview:<id> → │
│ deleted?} │ │ uint64 (counter) │
│ │ │ │
│ follow:<A>:<B> │ │ feedtag:<tag>:<ts>:<id> │
│ like:<post>:<liker>│ │ (hashtag index) │
│ likecount:<post> │ │ │
└─────────────────────┘ └──────────────────────────┘
```
Разделение решает два противоречия: 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": "<hex 16 B>",
"author": "<ed25519_hex>",
"content": "Text of the post",
"content_type": "text/plain",
"attachment_b64": "<base64 image/video/audio>",
"attachment_mime": "image/jpeg",
"reply_to": "<optional parent post_id>",
"quote_of": "<optional quoted post_id>",
"sig": "<base64 Ed25519 sig>",
"ts": 1710000000
}
```
`sig = Ed25519.Sign(author_priv, "publish:<post_id>:<raw_content_hash_hex>:<ts>")`
где `raw_content_hash = sha256(content + raw_attachment_bytes)`
клиентский хэш **до** серверного скраба.
`ts` в пределах ±5 минут от серверного времени (anti-replay).
**Response:**
```json
{
"post_id": "<echo>",
"hosting_relay": "<ed25519_hex of this node>",
"content_hash": "<hex sha256 of post-scrub bytes>",
"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": "<base64 bytes>",
"attachment_mime": "image/jpeg"
}
```
**Errors:** 404 если не найден; 410 если on-chain soft-deleted.
### `GET /feed/post/{id}/attachment`
Сырые байты аттачмента с корректным `Content-Type`. Используется
нативным image-loader'ом клиента:
```html
<img src="http://node/feed/post/abc/attachment" />
```
`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=<pub>`
Агрегат для отображения. `me` опционален — если передан, в ответе
будет `liked_by_me`.
**Response:**
```json
{
"post_id": "...",
"views": 127,
"likes": 42,
"liked_by_me": true
}
```
### `GET /feed/author/{pub}?limit=N&before=<ts>`
Посты конкретного автора, newest-first.
Пагинация: передавайте `before=<created_at>` самого старого уже
загруженного поста, чтобы получить следующую страницу.
**Response:**
```json
{"author": "<pub>", "count": 20, "posts": [FeedPostItem]}
```
### `GET /feed/timeline?follower=<pub>&limit=N&before=<ts>`
Лента подписок: 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=<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": <estimated_fee_ut>,
"payload": {
"post_id": "...",
"content_hash": "<base64 32-byte sha256>",
"size": 1234,
"hosting_relay": "<ed25519_hex>",
"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": "<target_ed25519>", "fee": 1000, "payload": {}}
```
Двусторонний on-chain индекс: `follow:<A>:<B>` + `followin:<B>:<A>`.
`UNFOLLOW` зеркально удаляет оба ключа.
### `LIKE_POST` / `UNLIKE_POST`
```json
{"type": "LIKE_POST", "fee": 1000, "payload": {"post_id": "..."}}
```
Дубль-лайк отклоняется. Кэшированный counter `likecount:<post>`
хранит 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)