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

174 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_present``your-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_protect``email='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_title``payload['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.4``1.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.
```