Files
cf7-spam-to-telegram/docs/superpowers/specs/2026-04-21-cf7stg-silent-drop-design.md
T
Vladimir Bryzgalov 287d313f1b docs(spec): drop skull emoji, clarify dry-run default on fresh install + upgrade
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:30:38 +05:00

15 KiB
Raw Blame History

CF7 Spam → Telegram — Silent Drop (1.1.0)

Дата: 2026-04-21 Версия плагина: 1.0.4 → 1.1.0 Trigger: на сайте домработница.рус через CF7 идут гарантированно мусорные заявки (имя из 2 латинских букв, пустые сообщения, поля заполнены 2-буквенными плейсхолдерами). Они помечаются Akismet как spam, попадают в Flamingo Spam и оттуда в TG-канал плагина. Владелец хочет, чтобы такие заявки не отправлялись в TG вообще — пользователь по-прежнему видит стандартный CF7 spam-message, заявка по-прежнему лежит в Flamingo Spam, но в Telegram-канале её нет.

Scope

В scope:

  • Новая метка Classifier 'drop' (поверх client/unclear/spam).
  • Hard-rule override (по правилам, минуя score) и soft-порог по score.
  • Безопасный rollout через dry-run (по умолчанию ON после апгрейда).
  • Счётчик дропов: dashboard widget + блок в Settings.
  • Filter API для расширения извне.

Вне scope:

  • Любые правки Akismet, Flamingo, основной формы CF7.
  • Удаление заявок из Flamingo Spam.
  • Чёрные списки IP/UA, ML-классификация, обучение на ретро.
  • Авто-обновление плагина из git-сервера (выделено в отдельный эпик).

Поведение

Новая метка drop

Classifier возвращает label='drop', если выполнено хотя бы одно:

  1. Hard-rule override (если включён, дефолт ON): У заявки нет ни одного позитивного сигнала:

    • нет cyrillic_name (имя на кириллице),
    • нет meaningful_text (осмысленный кириллический текст ≥ 10 символов в message-полях),
    • нет валидного email где-либо в полях (проверка через is_email() от WP — любой синтаксически валидный, не только ru-домены, gmail.com тоже считается); И хотя бы одно:
    • имя (значение ключей your-name/name/имя/fio либо результат FieldDetector::find_name()) непустое и ≤ 3 символов из [A-Za-z0-9],
    • в форме ≥ 3 непустых полей имеют значение длиной ≤ 2 символов из [A-Za-z0-9] (паттерн ботов-плейсхолдеров; acceptance-data и подобные служебные ключи в подсчёт не входят — фильтруются по списку служебных префиксов).
  2. Score-порог (всегда работает): score ≤ thresholds['drop'] (дефолт -5).

'drop' имеет приоритет: если hard-rule сработала, label='drop' независимо от score. Если score ≤ 5, label='drop' независимо от других правил.

Поток обработки в CF7Source::enqueue_from_submission

CF7 submit (status=spam)
  → CF7Source::on_submission (приоритет 20)
  → Classifier::classify($fields, ['reason' => $reason])
  → label = result['label']
  → Если label === 'drop':
       Если dry_run_drop ON:
         Settings::increment_drop_counter('dry_run')
         Queue::enqueue с payload['dry_run_drop']=true, label='drop'
       Иначе:
         Settings::increment_drop_counter('real')
         return  ← никакой записи в Queue, никаких хуков
  → Иначе (label != 'drop'):
       Применяется filter cf7stg_should_enqueue(true, $label, $fields, $submission)
       Если filter вернул false → return
       Queue::enqueue как сейчас

Что видит пользователь

Drop, dry-run OFF: стандартный CF7 spam-message на сайте, заявка в Flamingo Spam, в TG ничего.

Drop, dry-run ON: то же на сайте + сообщение в TG с заголовком [БЫЛО БЫ DROPPED] и строкой Сработавшее правило: hard-rule (нет позитивных + короткое латинское имя) или Сработавшее правило: score ≤ -5.

Не drop: всё работает как сейчас.

Архитектура

Изменяемые файлы

Файл Что меняется
includes/class-classifier.php DEFAULT_THRESHOLDS += 'drop' => -5. Новый метод evaluate_hard_drop() + private helpers has_positive_signals(), has_short_latin_name(), count_placeholder_fields(), has_valid_email_anywhere(). Логика возврата label расширена — 'drop' имеет приоритет. Hard-rule конфигурируется через filter cf7stg_hard_drop_rules.
includes/sources/class-cf7-source.php В enqueue_from_submission() добавляется ветка обработки label === 'drop' (см. data flow выше). Добавляется применение filter cf7stg_should_enqueue для не-drop случаев.
includes/class-message-formatter.php Если payload['dry_run_drop'] === true — заголовок становится [БЫЛО БЫ DROPPED] (вместо текущего «Спам заявки для проверки»), добавляется строка с триггером правила.
includes/class-settings.php Геттеры/сеттеры для новых опций. Метод increment_drop_counter($mode) с rolling сменой даты. Метод reset_drop_counters().
includes/class-activator.php На активации (как fresh install, так и upgrade до 1.1.0): add_option('cf7stg_dry_run_drop', true), add_option('cf7stg_drop_score_threshold', -5), add_option('cf7stg_hard_drop_enabled', true), add_option('cf7stg_drop_counters', $defaults). add_option сохранит существующее значение, если опция уже есть — то есть пользователи, уже выключившие dry-run, его не получат обратно.
admin/class-settings-page.php + admin/views/settings.php Новый блок «Silent drop»: чекбокс dry-run, score-порог, чекбокс hard-rule, статистика, кнопка «Сбросить счётчики».
admin/class-dashboard-widget.php + admin/views/dashboard-widget.php Строка со счётчиком дропов: при dry-run on — Dry-run: за сегодня было бы дропнуто 3, всего 17; при dry-run off — Дропнуто сегодня: 3 (вчера 5, всего 124). Без эмодзи.
cf7-spam-to-telegram.php CF7STG_VERSION = '1.1.0' в header и константе.
readme.txt Stable tag: 1.1.0 + запись в Changelog.

Новые опции WP

Опция Тип Дефолт
cf7stg_dry_run_drop bool true
cf7stg_drop_score_threshold int -5
cf7stg_hard_drop_enabled bool true
cf7stg_drop_counters array ['total'=>0, 'real_today'=>0, 'real_yesterday'=>0, 'dry_today'=>0, 'dry_yesterday'=>0, 'today_date'=>'YYYY-MM-DD', 'reset_at'=>'YYYY-MM-DD']

Filter API

Filter Аргументы Назначение
cf7stg_classifier_thresholds (уже есть) array $thresholds Переопределить пороги (client, spam, drop).
cf7stg_hard_drop_rules (новый) array ['short_latin_name'=>bool, 'placeholder_fields'=>bool] Точечно отключать отдельные правила hard-rule.
cf7stg_should_enqueue (новый) bool $should, string $label, array $fields, $submission Вернуть false → не enqueue (override извне). Применяется только к НЕ-drop случаям; drop-кейсы решаются раньше.

Тесты

PHPUnit, suite unit (как сейчас). Все новые тесты — TDD (RED → GREEN → REFACTOR).

tests/ClassifierDropTest.php

  • test_hard_drop_triggers_on_screenshot_1_full_form — фикстура из реального скрина 1 → label='drop', в reasons строка про hard-rule.
  • test_hard_drop_triggers_on_screenshot_2_full_form — скрин 2.
  • test_hard_drop_triggers_on_screenshot_3_popup_form — скрин 3.
  • test_hard_drop_skipped_when_cyrillic_name_presentyour-name='Иван' + остальное мусор → label != 'drop'.
  • test_hard_drop_skipped_when_meaningful_text_present — содержательный кириллический message → label != 'drop'.
  • test_hard_drop_skipped_when_valid_email_present — параметризованно: pasha@gmail.com, info@some-domain.com, name@yandex.ru → ни один не drop.
  • test_hard_drop_invalid_email_does_not_protectemail='not-an-email' → drop, если остальные триггеры выполняются.
  • test_hard_drop_skipped_on_form_without_name_when_no_placeholder_rule — нет name-полей и < 3 placeholder-полей → не drop (форма «обратный звонок»).
  • test_hard_drop_triggers_on_form_without_name_via_placeholder_rule — нет name-полей, но 4 поля с 2-буквенным мусором → drop.
  • test_score_drop_threshold_minus_5 — синтетический набор → label='drop' через score.
  • test_filter_disables_hard_drop_rule_short_latin_name — фильтр cf7stg_hard_drop_rules отключает short_latin_name → скрин 1 не падает (если другое правило не сработает).
  • test_filter_thresholds_can_change_drop_score — фильтр меняет drop-порог на −3.

tests/SettingsCounterTest.php

  • test_increment_real_counter — пустой state → total=1, real_today=1.
  • test_increment_dry_run_counter — то же для 'dry_run'.
  • test_rolling_today_to_yesterday_on_date_change — смена даты → today смещается в yesterday, обнуляется.
  • test_reset_counters_sets_new_reset_at — reset обнуляет всё, ставит reset_at = current_date().

tests/CF7SourceDropTest.php (интеграция с моками)

  • test_drop_label_with_dry_run_off_does_not_enqueue — мок Classifier=drop, dry-run off → Queue::enqueue НЕ вызван, increment_drop_counter('real') вызван 1 раз.
  • test_drop_label_with_dry_run_on_enqueues_with_marker — то же, dry-run on → enqueue с payload['dry_run_drop']=true, increment_drop_counter('dry_run') вызван.
  • test_unclear_label_still_enqueues_normally — регрессия.
  • test_filter_should_enqueue_can_block_non_drop — фильтр для form_id=42 → false → не enqueue, счётчики не трогаются.

tests/MessageFormatterDryRunTest.php

  • test_dry_run_drop_prefix_in_titlepayload['dry_run_drop']=true → текст начинается с [БЫЛО БЫ DROPPED].
  • test_dry_run_drop_includes_triggered_rule_line — есть строка Сработавшее правило: ....
  • test_non_drop_payload_unchanged — без флага вывод как раньше.

Fixtures

tests/fixtures/spam-screenshot-{1,2,3}.php — массивы $fields ровно как в TG-сообщениях (text-785/787/786/788/789/790, text-347/tel-185, и т.п.). Используются и в Classifier-тестах, и для будущих ретро-проверок.

Error handling

  • Classifier::evaluate_hard_drop() — чистая функция от $fields, не бросает. Если $fields пустой/невалидный — возвращает false (нет drop).
  • Settings::increment_drop_counter() — оборачивается в try/catch на уровне Source: если что-то упало, логируем через error_log() и продолжаем (не блокируем основной поток CF7).
  • Если опция cf7stg_drop_counters повреждена/имеет неверную структуру — пересоздаётся из дефолта.
  • Filter-возвраты валидируются: ожидаем bool/array; неверные типы → fallback к дефолту.
  • Дефолт dry_run_drop=true — даже если admin никогда не открыл настройки, реальный drop не включится.

Rollout

  1. Установка/активация (fresh install и upgrade до 1.1.0) — Activator выставит dry_run_drop=true, hard_drop_enabled=true, drop_score_threshold=-5, пустые счётчики. Если опция уже была — её значение сохраняется (через add_option).
  2. С момента активации:
    • Все drop-заявки идут в TG с пометкой [БЫЛО БЫ DROPPED] (а не реально дропаются).
    • Счётчик dry_today инкрементируется на каждом drop.
  3. Через 1–2 недели владелец сайта смотрит TG-канал и счётчик в Settings — нет ли среди дропов реальных клиентов.
  4. Если всё ок — снимает чекбокс «Dry-run mode» в настройках. С этого момента drop-заявки в TG не уходят, инкрементируется только real_today.

Версионирование

  • 1.0.41.1.0 (semver minor: новая фича, обратная совместимость).
  • Changelog в readme.txt:
    = 1.1.0 =
    * New: Silent drop — заявки, удовлетворяющие hard-rule (нет позитивных
      сигналов + короткое латинское имя или ≥3 полей-плейсхолдеров) или
      score ≤ -5, не отправляются в TG. По умолчанию при установке/апгрейде
      включён dry-run режим (помечает сообщение «[БЫЛО БЫ DROPPED]»);
      реальный дроп включается чекбоксом в настройках.
    * New: Settings → блок «Silent drop» с переключателями dry-run, hard-rule,
      score-порогом и статистикой счётчиков.
    * New: Dashboard widget показывает количество дропов.
    * New: filter `cf7stg_should_enqueue` для внешних override-правил.
    * New: filter `cf7stg_hard_drop_rules` для точечного отключения hard-rules.