Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

129 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lovegw

Telegram-бот, который зеркалит раздел заметок (заметки) сайта знакомств love.ngs.ru в Telegram-каналы и связывает взаимодействие обратно: залогиненные в Telegram пользователи отвечают в группе обсуждения, а бот публикует их ответ комментарием на сайте от их имени. Отдельный ЛС-бот РюмкинЪ умеет вход на сайт, публикацию заметок и подписки на ключевые слова. Личную переписку сайта можно увести отдельному боту (talks_token): он ведёт только диалоги — /talks, /talk <номер>, доставку входящих ЛС и ответы реплаем.

Написан на Go (модуль lovegw, каталог go/). Хранилище — SQLite без CGo; запись сквозная, поэтому состояние переживает kill -9. Скрапинг — на net/http + goquery по HTML сайта. Помимо демона, у бинарника есть офлайн-слой архива: массовая выгрузка заметок в отдельную archive.db и аналитика «персонажей» поверх неё (склейка альт-анкет, интересы, отношения, отчёт).

Быстрый старт

cd go
go build ./...
go run ./cmd/lovegw run       # демон: зеркало + мост ответов + ЛС-бот
go run ./cmd/lovegw doctor    # диагностика конфига/БД/сайта/токенов/очереди

Конфиг — go/config.json (шаблон go/config.example.json); токены можно задать через переменные окружения LOVEGW_MIRROR_TOKEN / LOVEGW_DM_TOKEN / LOVEGW_MAX_TOKEN / LOVEGW_MAX_DM_TOKEN, путь к БД — LOVEGW_DB_PATH, прокси — LOVEGW_TG_PROXY. На Windows демоном управляют батники start.bat / stop.bat / status.bat / restart.bat.

Сеть. love.ngs.ru за геоблоком DDoS-Guard — с не-российских IP отдаёт 403, поэтому демон и все команды, ходящие на сайт, должны работать с российского IP. При этом Telegram Bot API из России недоступен: для раздельной сети задайте telegram_proxy в конфиге — проксируется только Bot API, а сайт идёт напрямую.

Команды

Все команды — подкоманды одного бинарника: go run ./cmd/lovegw <команда> … (или собранный lovegw <команда> …). Флаги можно писать и до, и после позиционных аргументов (crawl notes -save-html dir работает). У всех команд, читающих конфиг, есть -config config.json (путь по умолчанию — относительно текущего каталога, поэтому запускать удобнее из go/).

Демон и эксплуатация

lovegw run    [-seed]
lovegw doctor [-post-test]
lovegw repost <note_id> [<note_id> …]
lovegw import [-notes notes.json] [-sessions sessions_export.json] [-subscribers subscribers.json]
  • run — основной демон: зеркалирование ленты и комментариев, мост «ответ в группе → комментарий на сайте», ЛС-бот РюмкинЪ — всё под одним errgroup. -seed на первом запуске запоминает видимые заметки без постинга (иначе при пустой БД случится залп из всей текущей ленты).
  • doctor — диагностика: конфиг, БД, доступ к сайту, токены ботов, очередь обновлений. -post-test дополнительно проверяет цепочку «пост в канал → автофорвард в группу» тихим самоудаляющимся сообщением (безопасно на живом канале).
  • repost — удаляет заметку из Telegram (пост, тред, комментарии, иллюстрации) и из БД, чтобы демон перепостил её заново по текущей логике. Отладочная команда — запускать при остановленном демоне.
  • import — разовый идемпотентный импорт JSON-состояния старой Python-версии в SQLite (INSERT OR IGNORE); можно указывать любое подмножество файлов.

Отладка парсинга

lovegw crawl notes                [-save-html dir]
lovegw crawl comments <note_id>   [-save-html dir]

Разбирает ленту или страницу комментариев и печатает JSON. -save-html записывает сырую страницу как фикстуру парсера — при дрейфе вёрстки перезаписывайте фикстуры в internal/love/testdata/ этой же командой:

lovegw crawl notes -save-html internal/love/testdata
lovegw crawl comments <note_id> -save-html internal/love/testdata

Архив (archive.db)

Отдельная БД (по умолчанию data/archive.db, флаг -db) для офлайн-анализа: users/notes/comments, дерево ответов через parent_id, типажи дедуплицированы по id анкеты.

lovegw grab   [-db archive.db] [-json] [-out dir] [-save-html dir] [-view tree|linear] [-max-pages N] <note_id>
lovegw export [-db archive.db] [-out dir] <note_id>
lovegw backfill [-db archive.db] [-proxy] [-workers N] [-interval-ms MS] [-from ID] [-to ID] [-start-page N] [-refresh] [-limit N]
  • grab — разовая выгрузка одной заметки со всеми страницами комментариев с сайта в archive.db (только чтение, без кук). -json дополнительно пишет денормализованный снимок <id>.json в -out.
  • export — полностью офлайн: собирает заметку из archive.db во вложенное JSON-дерево <id>.json (для визуализаций и внешних инструментов).
  • backfill — массовая выгрузка диапазона заметок: перечисляет живые id обходом ленты и внахлёст скармливает их пулу воркеров. Идемпотентна — после остановки или 403 достаточно перезапустить. -proxy гонит сайт через telegram_proxy (беречь основной IP), -from/-to — границы id (по умолчанию от 240866 — старее комментарии уже удалены), -start-page — с какой страницы ленты начинать (экономит тысячи страниц на старом диапазоне), -refresh — пере-обходить уже загруженные.

Персонажи (personas)

Слой распознавания личностей поверх archive.db: вероятностная, ревью-гейтед склейка альт-анкет одного человека в «личности», плюс интересы и отношения из текстов. Общие флаги: -db archive.db, -out dump (каталог выгрузок).

Основной конвейер склейки — текстовые самораскрытия:

lovegw personas flag [-patterns file]        # скан комментариев по шаблонам самораскрытия → disclosure_hits
lovegw personas candidates [-limit N]        # выгрузка непроработанных помет с контекстом (hits.jsonl + users_index.json)
# … внешний LLM-проход по hits.jsonl → links.json …
lovegw personas link [-in links.json]        # импорт извлечённых связей в alias_candidates
lovegw personas cluster [-min-score F] [-max-persona N] [-min-density F]
                                             # склейка связных компонент в personas + отчёт
lovegw personas set <persona_id> <confirmed|rejected|pending>   # вердикт после ревью

cluster защищён гардами от транзитивной переклейки: -max-persona (компонент крупнее — не склеивать) и -min-density (минимальная плотность рёбер для компонент больше 4 анкет).

Дополнительные сигналы склейки:

lovegw personas avatars fetch  [-proxy] [-workers N] [-limit N] [-refresh]   # скачать аватары, посчитать dHash
lovegw personas avatars cluster [-max-dist D] [-generic-max N]               # пары по похожести аватаров
lovegw personas stylometry build   [-min-chars N] [-dims N] [-genre all|notes] # стилевые профили авторов
lovegw personas stylometry cluster [-min-cosine F] [-top-k N] [-max-pairs N] # пары по близости стиля
lovegw personas lexis build        [-lex-min-tokens N] [-lex-dims N] [-genre all|notes] # лексические TF-IDF-профили (2-й сигнал атрибуции)
lovegw personas ensemble [-ens-top-k N] [-handoff-days D] [-ens-floor F]     # композит: стиль + handoff + круг общения

Диагностика:

lovegw personas portrait [-top N] <p<id>|u<id>|user_id>   # портрет личности/анкеты: стиль, собеседники
lovegw personas diag <id> <id># ground-truth сверка набора анкет (стиль/собеседники/время)
lovegw personas attribute [-top N] [-note id] [-in text.txt] [-lex-weight F] [-active-days N] [-min-author-notes N] [-genre notes] [текст …]
                                                          # скоринг авторства: чьё письмо ближе к анонимному тексту (стиль + лексика);
                                                          # фильтр кандидатов: -active-days N убирает мёртвые анкеты, -min-author-notes N —
                                                          # чистых комментаторов без заметок (атрибутируем-то заметку)
lovegw personas attribute [-author p<id>] [-lex-weight F] [-genre notes]  # пакет: все заметки личности + сводка попаданий
lovegw personas calibrate -notes id,id,… [-author p<id>] [-active-days N] [-min-author-notes N] [-genre notes]  # leave-one-out: предсказуемость + эффект фильтров
lovegw personas verify -suspect p<id> [-in txt|-notes ids|-note id] [-null N] [-active-days N] [-min-author-notes N] [-genre notes]
                                                          # «это он? да/нет» с калиброванным порогом (FPR 5%/1%);
                                                          # фильтры сужают фон до правдоподобных кандидатов

Жанр эталона -genre (по умолчанию all — комментарии+заметки, обратная совместимость): у большинства авторов профиль на 2/3 состоит из комментариев — другой жанр, чем заметка-запрос (короткая реплика vs текст), это системная ошибка сопоставления. -genre notes строит и грузит эталон только из заметок (register-matched): и запрос, и профиль — один регистр. Note-слой есть только у писавших заметки, поэтому -min-author-notes для него встроен по построению. Порядок: сначала stylometry build -genre notes (+ при желании lexis build -genre notes), затем attribute/calibrate/verify -genre notes. Для all (комментарии) флаг не нужен — это дефолт.

Факты (интересы) и отношения — тот же паттерн «scan/score → candidates → внешний LLM → import»:

lovegw personas facts scan [-topics file] [-min-hits N] [-min-notes N] [-evidence N]
lovegw personas facts candidates [-min-hits N] [-min-notes N]      # → facts.jsonl на LLM-разметку
lovegw personas facts import [-in facts_llm.json]                  # → identity_facts

lovegw personas relations score [-rel-min-replies N]               # тон направленных пар по эвристикам
lovegw personas relations candidates [-cand-replies N] [-band-min N] [-band-top N] [-exchanges N]
                                                                   # → pairs.jsonl на LLM-разметку
lovegw personas relations import [-in relations_llm.json]          # → relation_edges

Пол анкет — обходом их профилей через мобильную версию сайта (десктопная банит серию запросов почти мгновенно, мобильный vhost терпит):

lovegw personas gender [-active-days N] [-tg-user id] [-limit N]

Ходит под сессией сайта из БД бота (-tg-user, по умолчанию admin_tg_user_id; нужен /login в РюмкинЪ). Идемпотентно — обходятся только анкеты без пола, волны 403 пережидаются с нарастающей паузой, так что остаток добирается простым перезапуском.

Манера письма (voice) — читаемая стилевая карта участника:

lovegw personas voice card <p<id>|u<id>|user_id> [-genre notes|all] [-recent N]
                           [-samples N] [-band N] [-seed N] [-top-words N]

Профили атрибуции — хешированные необратимые векторы: по ним нельзя ни сказать, как звучит человек, ни проверить их глазами. voice card меряет манеру по сырому тексту: квантили длин, предложения и абзацы, пунктуация на 1000 рун, скобочная подпись отдельно от частот («)» и «))» — разные люди), регистр и финальный знак, разметка сайта и смайлы, обращения «Ник, …», часы активности и характерная лексика. Карта детерминирована: тот же -seed на той же БД даёт ту же выборку. LLM и конфиг не нужны.

Корпус режется на образцы (-samples) и непересекающийся held-out (-band) — второй нужен будущему замкнутому циклу генерации как эталонная полоса, и пересечение сломало бы замер. -samples 0 — режим, в котором дословные тексты участника вообще никуда не уходят. -recent ограничивает замер последними текстами (индекс idx_comments_author не покрывает text, у плотного комментатора это десятки тысяч чтений с диска); в отчёте всегда видно и замер, и полный знаменатель.

Вес характерных слов берётся у корзины хэша из готового lexis_meta, а не у слова, — это экономит проход по 10,7 млн комментариев. Читать так: вес корзины — нижняя оценка редкости слова (частое слово в корзине тянет его вниз), поэтому верх списка надёжен, а низ не значит ничего.

Генерация с замкнутым циклом:

lovegw personas voice note <token> [-topic ""] [-drafts N] [-rounds N]
                           [-control p<id>] [-accept F] [-max-copy F]
lovegw personas voice comment <token> -reply-to <comment_id> [-control p<id>]
lovegw personas voice comment <token> -note <note_id>        [-control p<id>]

-reply-to — ответ внутри ветки (адресат и обращение «Ник,» подставляются инструментом), -note — комментарий первого уровня к самой заметке: адресата нет, а корневые реплики треда идут в промпт как «уже сказанное».

Образцы и словарь берутся от рода текста, который пишем, а не от жанра эталона: комментарий учится на комментариях, даже когда слой атрибуции all. Образец-реплика подаётся парой «на что отвечает → что ответил» (цель ответа — из слоя comment_reply, иначе parent_id, иначе сама заметка): короткая реплика в отрыве от чужих слов манеру не показывает. Число образцов по умолчанию считается по медиане корпуса (короткие реплики — 24, средние — 12, длинные — 6); -samples N перебивает, -samples 0 — промпт вообще без дословных текстов.

Список характерных слов подаётся не как цель: рядом печатается норма самого автора (слов из списка на 100 слов), дифф ругает и перебор, и недобор, а черновик с частотой выше нормы ×3 отбраковывается до скоринга. Иначе модель набивает список и поднимает лексический косинус, обрушив стилевой.

Карта и образцы уходят в Claude (llm.api_key / LOVEGW_LLM_KEY, через telegram_proxy), K черновиков возвращаются одним запросом, каждый прогоняется через собственную атрибуцию архива. Голый ранг ничего не значит — на машинном тексте атрибуция заведомо слаба, — поэтому он читается против эталонной полосы: рангов реальных held-out текстов того же человека. Критерий приёмки — квантиль полосы (-accept, по умолчанию 0.25), а не порог по рангу: у одного автора его собственные заметки лежат на 1–26, у другого на 40–3000.

Полоса оптимистична: её тексты входят в стиль-профиль автора, а черновик — нет. Это считается (доля профиля, которую даёт один текст полосы) и печатается; выше 5 % полоса не строится вовсе, и отчёт говорит почему. Кроме контаминации полоса отказывается ещё в двух случаях: больше половины её текстов короче порога объёма, либо её медиана лежит дальше половины списка — тогда слой не узнаёт и настоящие тексты автора, и сравнивать не с чем. Живой замер на комментариях (u175869, эталон all): медиана полосы 8391 из 9215 при 26 коротких текстах из 30 — жанру комментария нужен свой слой профилей.

-control <token> строит вторую полосу и скорит те же черновики против чужой личности. Правило чтения: если контроль не ниже цели, атрибутор на машинном тексте людей не различает и вердикт цикла отбрасывается целиком. Без -control отчёт честно предупреждает, что прогон не интерпретируется.

Второй раунд получает не «попробуй ещё», а измеримый дифф: дельты длины, предложений и пунктуации, отсутствующие «))», лишняя финальная точка, неиспользованные характерные слова и слова, которых у автора нет ни разу. Раундов максимум 3 — цикл взбирается по хешированной метрике, и дальше модель начинает набивать признаки вместо манеры. Черновик, пересказывающий образец, отбраковывается стражем копирования (-max-copy).

Инструмент исследовательский: пишет только в <out>/voice/, каждый артефакт несёт марку машинной генерации, пути публикации на сайт или в мессенджеры нет (закреплено тестом на список импортов, а не комментарием).

Итоговый отчёт:

lovegw personas report [-report-top N] [-active-days N] [-html]

Сводка «персонажей»: топ личностей, интересы, отношения. -active-days N ограничивает когорту активными за последние N суток, -html дополнительно собирает characters.html.

Развёртывание (Docker)

Боевой запуск — в контейнере (образ ~23 МБ, distroless/static). Артефакты и пошаговая инструкция — в deploy/ (подробно — deploy/README.md): multi-stage go/Dockerfile, docker-compose.yml, systemd-юнит, шаблон секретов.

cd deploy
docker compose up -d --build

Хост нужен с российским IP (иначе сайт отдаёт 403); Telegram Bot API идёт через SOCKS5-прокси (telegram_proxy / LOVEGW_TG_PROXY). Конфиг и БД (data/lovegw.db) в репозиторий не входят — переносятся на хост отдельно.

Разработка

cd go
go vet ./...
go test ./...

Селекторы вёрстки сайта собраны в одном const-блоке internal/love/parse.go; тесты гоняются на реальных фикстурах в internal/love/testdata/. Обязательный селектор, не нашедший элемент, возвращает типизированную MarkupError (детекция дрейфа вёрстки). Идентификаторы — по-английски, комментарии и строки бота — по-русски.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages