Files
cf7-spam-to-telegram/docs/superpowers/specs/2026-04-21-cf7stg-silent-drop-design.md
T
2026-04-21 15:27:15 +05:00

14 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 Upgrade-handler: при апдейте до 1.1.0 — add_option('cf7stg_dry_run_drop', true) (safe default).
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/off).
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. Активировать плагин — Activator выставит dry_run_drop=true (safe default), hard_drop_enabled=true.
  2. На каждом сайте, где плагин обновляется до 1.1.0:
    • Сразу включится dry-run mode.
    • Все drop-заявки начнут идти в TG с пометкой ☠️ [БЫЛО БЫ DROPPED].
  3. Через 1–2 недели владелец сайта смотрит, нет ли среди дропов реальных клиентов.
  4. Если всё ок — снимает чекбокс «Dry-run mode» в настройках. С этого момента — реальный silent drop.

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

  • 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.