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Отдельная БД (по умолчанию 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— пере-обходить уже загруженные.
Слой распознавания личностей поверх 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.
Боевой запуск — в контейнере (образ ~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 (детекция дрейфа вёрстки).
Идентификаторы — по-английски, комментарии и строки бота — по-русски.