Metadata-Version: 2.5
Name: s-socialkit
Version: 0.0.9
Summary: Общий слой соцсетей: контракт видов (кто умеет publish/content/media/comments/feed/metrics/search/profile/session/export), сущности домена (материал, комментарий, показатели, цель публикации) и живость входа. То, что одинаково у Telegram, VK, Instagram, TikTok, YouTube, Rutube, Дзена и остальных, — в одном месте, а разговор с сервисом остаётся за навыком.
Author: Dmitry
License: MIT
Keywords: capabilities,comments,content,metrics,publishing,social
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.7
Requires-Dist: s-corekit>=0.0.10
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# socialkit (`s-socialkit`)

> Общий слой домена соцсетей — то, что одинаково у Telegram, VK, Instagram,
> TikTok, Threads, YouTube, Rutube, Дзена, vc.ru, ok.ru, Boosty, Reddit,
> Pinterest и MAX. Аналог `aichatkit` для ИИ-чатов.

## Зачем

Соцсети работают так же, как ИИ-чаты: **навык объявляет способности видами, а
оркестратор зовёт вид, а не сервис**. Тогда веер («найти конкурентов во всех
соцсетях») получается сам собой — спросили всех, кто объявил вид, — а новая
соцсеть подключается установкой пакета, без правки ядра.

До этого единицей подключения была ВСЯ СЕТЬ: маршрут веера лежал таблицей
`CROSSPOST_ROUTES`, перечень сетей — закрытым `NetworkId` на шестнадцать
значений, а способность выяснялась двумя десятками булевых флагов уже ПОСЛЕ
загрузки адаптера.

## Состав 0.0.1

| Модуль | Что даёт |
|---|---|
| `capabilities` | контракт видов: перечень, таблица вид→метод, правило имени точки входа `<сервис>_<вид>`, поля ответа, машинная проверка однородности вызова |
| `entities` | сущности домена: материал, комментарий, показатели, цель публикации, автор, файл, страницы выборок. Приехали из ядра `bublictr` |
| `liveness` | живость входа — ТОНКИЙ реэкспорт `corekit.diagnosis.access` (три исхода, правило живёт в основании) |
| `content_types` | кто каким ВИДОМ какие ТИПЫ материала умеет: язык объявления, чтение из метаданных (без импорта навыков), вопрос «кто умеет карусель» |
| `trail` | СЛЕД вызова вида: четыре ступени ВОКРУГ вызова одной строкой `@traced` (механизм — `corekit.trail`), общий идентификатор прохода, рекурсивная вычистка секретов, приёмник в локальный `*.jsonl` |

### Типы материала

`video` (горизонтальное long-form) · `reel` (короткое вертикальное) · `post` ·
`story` · `carousel` · `article` · `poll`.

Порог тот же, что у видов: тип заводится, когда его умеют трое и больше
(пересчитано по тринадцати навыкам соцсетей). Поэтому здесь нет трансляции — её
умеет один Rutube; нет и «аудио» — звук приезжает вложением в пост, а это
`MediaKind`.

Навык объявляет соответствие вид→типы ТОЧКАМИ ВХОДА группы
`socialkit.content_types`, где значение — сам перечень строкой:

```toml
[project.entry-points."socialkit.content_types"]
instagram_publish = "post:image, carousel:image+video, reel:video, story:image+video"
```

Так перечень читается из метаданных, не импортировав ни одного навыка. После
двоеточия — чем тип бывает наполнен: карусель у Instagram и альбома Telegram
принимает внутрь ролик, у VK — только фотографии, и эта разница объявлена
словами, а не подразумевается.

### Виды

**Ядро (веерится):** `publish` · `content` · `media` · `comments` · `feed` ·
`metrics` · `search` · `profile` · `session` · `export`.

**Второй эшелон (объявлен, волну не блокирует):** `engagement` · `schedule` ·
`members` · `messages` · `collection` · `events`.

Порог объявления — повторяемость намерения (умеют трое и больше), а не похожесть
кода. Уникальное (`market` ВКонтакте, `star revenue` Телеграма, протокол MAX)
остаётся своей командой навыка: вид с одним носителем — это переименованный
сервис.

### Правила контракта

- вид и метод зовутся ОДНИМ словом — помнить соответствие не приходится;
- имя сервиса живёт ТОЛЬКО в координате точки входа `<сервис>_<вид>`;
- **вызов однороден: `ctx` есть у любого вида и везде со значением по
  умолчанию.** У чатов это разъехалось, и наивный вызов падал `TypeError` на
  первом же сервисе. Проверяется машиной: `socialkit.uniformity_problems(навык)`;
- сырой ответ сервиса всегда лежит в `raw`;
- «не знаю» говорится прямо: `None`, а не ноль и не выдуманный вердикт.

## Чего в ките нет и не будет

Транспорта (`netkit`), браузера, минта и хранилища сессий (`librarykit`), пула
аккаунтов (`accountpoolkit`), троттлинга и реестра адресов (`adapterkit`), веера
(это оркестратор). Кит про ЗНАНИЕ домена, а не второй фреймворк.

## Установка

```sh
pip install s-socialkit
```

## Использование

```python
from socialkit import CAPABILITY_METHOD, CORE_KINDS, capability_id, uniformity_problems
from socialkit import ContentItem, ContentType, Visibility

# навык объявляет точку входа
capability_id("vk", "publish")            # → "vk_publish"

# оркестратор зовёт вид, не зная имени сервиса
метод = getattr(навык, CAPABILITY_METHOD["publish"])
метод(ContentItem(service="vk", type=ContentType.POST, visibility=Visibility.PUBLIC))

# навык проверяет себя сам: объявленное обязано быть позываемым
assert uniformity_problems(навык) == []
```

Спросить по ТИПУ материала — ничего не загружая и не зная имён сетей:

```python
from socialkit import ContentType, MediaKind, who_can

who_can(ContentType.CAROUSEL)                              # → ('instagram_publish', ...)
who_can(ContentType.CAROUSEL, media=MediaKind.VIDEO_FILE)  # карусель с роликом внутри
who_can(ContentType.REEL, kind="content")                  # кто ПЕРЕЧИСЛЯЕТ вертикальные
```

Оставить след вызова — одной строкой, не зная про основание:

```python
from socialkit import SESSION, set_sink, to_jsonl, traced

set_sink(to_jsonl("~/.sessions/trail.jsonl"))   # локально; наружу — тот же файл

class VkSession:
    service, kind = "vk", SESSION

    @traced                       # четыре ступени вокруг вызова, а не в теле
    async def session(self, ctx=None, **_):
        ...
```

Одна запись прохода (`intent` → `access` → `exchange` → `outcome`) выглядит так:

```json
{"stage": "outcome", "trace_id": "e2f1…", "service": "vk", "kind": "session",
 "verdict": "refused", "why": "вход отвергнут", "evidence": "форма входа в ответе"}
```

`trace_id` приезжает из `ExecutionContext`, поэтому сценарий из десяти видов в
разных сетях читается одним `grep` по идентификатору. Секреты в записи не
попадают никогда — ни доводом, ни вложенным заголовком.

## Разработка

```sh
python -m pytest -q     # 90 тестов
python -m ruff check .
```
