Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
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', если выполнено хотя бы одно:
-
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и подобные служебные ключи в подсчёт не входят — фильтруются по списку служебных префиксов).
- нет
-
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
- Активировать плагин — Activator выставит
dry_run_drop=true(safe default),hard_drop_enabled=true. - На каждом сайте, где плагин обновляется до 1.1.0:
- Сразу включится dry-run mode.
- Все drop-заявки начнут идти в TG с пометкой
☠️ [БЫЛО БЫ DROPPED].
- Через 1–2 недели владелец сайта смотрит, нет ли среди дропов реальных клиентов.
- Если всё ок — снимает чекбокс «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.