Files
cf7-spam-to-telegram/includes/class-message-formatter.php
T
Vladimir Bryzgalov 714b4712ad fix: wire is_hard_drop_enabled + drop_score_threshold settings into Classifier filter chain
Settings::register_classifier_filters() registers priority-5 callbacks on
cf7stg_classifier_thresholds and cf7stg_hard_drop_rules so the UI options
actually influence Classifier behaviour. Called from Plugin::boot(). User
filters at priority 10 override these defaults as expected. Adds 4 tests
covering the wired behaviour and override priority. Also adds dry_run_drop?
to MessageFormatter::build() @param docblock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:25:20 +05:00

192 lines
7.3 KiB
PHP
Raw Permalink 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.
<?php
declare(strict_types=1);
namespace Cf7stg;
final class MessageFormatter
{
private const TG_MAX_LEN = 4096;
private const LABEL_MAP = [
'client' => ['emoji' => '🟢', 'text' => 'похоже клиент'],
'unclear' => ['emoji' => '🟡', 'text' => 'неясно'],
'spam' => ['emoji' => '🔴', 'text' => 'похоже спам'],
'drop' => ['emoji' => '⛔', 'text' => 'авто-дроп'],
];
/**
* @param array{
* fields:array<string,mixed>,
* classification:array{score:int,label:string,reasons:array},
* site_title:string,
* form_title:string,
* submitted_at:string,
* reason:string,
* ip:string,
* user_agent:string,
* is_retro:bool,
* dry_run_drop?:bool
* } $ctx
*
* @return array{text:string,reply_markup:array|null}
*/
public static function build(array $ctx): array
{
$fields = $ctx['fields'];
$class = $ctx['classification'];
$phone = PhoneParser::parse(self::flatten_fields_for_phone($fields));
$name = self::pick($fields, ['your-name', 'name', 'имя', 'fio']);
if ($name === '') $name = FieldDetector::find_name($fields);
$email = self::pick($fields, ['your-email', 'email', 'e-mail', 'mail']);
if ($email === '') $email = FieldDetector::find_email($fields);
$message = self::pick($fields, ['your-message', 'message', 'comment', 'сообщение', 'вопрос']);
$lines = [];
$is_dry_run_drop = !empty($ctx['dry_run_drop']);
if ($is_dry_run_drop) {
$header = '[БЫЛО БЫ DROPPED]';
} else {
$label_info = self::LABEL_MAP[$class['label']] ?? self::LABEL_MAP['unclear'];
$header = ($ctx['is_retro'] ? '📂 Ретроспектива · ' : '')
. $label_info['emoji'] . ' ' . $label_info['text'];
}
$lines[] = $header;
$lines[] = '🌐 ' . esc_html($ctx['site_title']) . ' · форма «' . esc_html($ctx['form_title']) . '»';
$lines[] = '🗓 ' . esc_html($ctx['submitted_at']);
$lines[] = '';
$lines[] = '📱 ' . ($phone !== null ? esc_html($phone['pretty']) : '(не указан)');
$lines[] = '👤 ' . ($name !== '' ? esc_html($name) : '(не указано)');
$lines[] = '📧 ' . ($email !== '' ? esc_html($email) : '(не указан)');
$lines[] = '';
$lines[] = '💬 ' . ($message !== '' ? esc_html($message) : '(пусто)');
$lines[] = '';
$lines[] = '⚙️ Попало в спам: ' . esc_html($ctx['reason']);
$lines[] = '📊 Классификация: ' . self::format_reasons($class['reasons']);
if ($is_dry_run_drop) {
$rule = '';
foreach ($class['reasons'] as $r) {
$text = (string)($r['text'] ?? '');
if (strpos($text, 'hard-rule:') === 0 || strpos($text, 'score ≤') === 0) {
$rule = $text;
break;
}
}
if ($rule !== '') $lines[] = '🚫 Сработавшее правило: ' . esc_html($rule);
}
$meta_bits = [];
if ($ctx['ip'] !== '') $meta_bits[] = 'IP ' . esc_html($ctx['ip']);
if ($ctx['user_agent'] !== '') $meta_bits[] = 'UA ' . esc_html(self::shorten_ua($ctx['user_agent']));
if ($meta_bits) $lines[] = '🌍 ' . implode(' · ', $meta_bits);
$utms = self::extract_utms($fields);
if ($utms !== '') $lines[] = '🏷 ' . $utms;
$other = self::extract_other_fields($fields);
if ($other !== '') $lines[] = '📋 ' . $other;
$text = implode("\n", $lines);
$text = self::truncate($text);
// Telegram Bot API rejects tel: URLs in inline_keyboard with HTTP 400
// ("Wrong port number specified"), so no inline button is emitted.
// The phone number is already shown in the message body (line '📱 ...')
// and mobile Telegram clients make such numbers tappable automatically.
return ['text' => $text, 'reply_markup' => null];
}
private static function pick(array $fields, array $keys): string
{
foreach ($keys as $k) {
if (isset($fields[$k])) {
$v = $fields[$k];
if (is_array($v)) $v = implode(' ', $v);
return trim((string)$v);
}
}
return '';
}
private static function flatten_fields_for_phone(array $fields): string
{
$out = [];
foreach ($fields as $k => $v) {
if (is_array($v)) $v = implode(' ', array_map('strval', $v));
$out[] = (string)$v;
}
return implode("\n", $out);
}
private static function format_reasons(array $reasons): string
{
if (!$reasons) return '—';
$parts = [];
foreach ($reasons as $r) {
// Drop reasons use sign='×' weight=0 as a sentinel — render text only
// to avoid the meaningless "×0 hard-rule: ..." prefix.
if (($r['sign'] ?? '') === '×' && (int)($r['weight'] ?? 0) === 0) {
$parts[] = (string)$r['text'];
continue;
}
$parts[] = $r['sign'] . $r['weight'] . ' ' . $r['text'];
}
return esc_html(implode(', ', $parts));
}
private static function shorten_ua(string $ua): string
{
$ua = preg_replace('/\s+/', ' ', $ua) ?? $ua;
if (mb_strlen($ua) > 80) $ua = mb_substr($ua, 0, 80) . '…';
return $ua;
}
private static function extract_utms(array $fields): string
{
$keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
$parts = [];
foreach ($keys as $k) {
if (!empty($fields[$k])) {
$v = is_array($fields[$k]) ? implode(' ', $fields[$k]) : (string)$fields[$k];
$parts[] = esc_html($k) . '=' . esc_html($v);
}
}
return implode(' · ', $parts);
}
private static function extract_other_fields(array $fields): string
{
$known = [
'your-name', 'name', 'имя', 'fio',
'your-email', 'email', 'e-mail', 'mail',
'your-tel', 'tel', 'phone', 'телефон',
'your-message', 'message', 'comment', 'сообщение', 'вопрос',
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'_wpcf7', '_wpcf7_version', '_wpcf7_locale', '_wpcf7_unit_tag',
'_wpcf7_container_post', '_wpcf7_posted_data_hash', 'g-recaptcha-response',
];
$parts = [];
foreach ($fields as $k => $v) {
if (in_array($k, $known, true)) continue;
if (strpos($k, 'wpcf7') === 0) continue;
if (is_array($v)) $v = implode(', ', array_map('strval', $v));
$v = trim((string)$v);
if ($v === '') continue;
$parts[] = esc_html($k) . ': ' . esc_html($v);
}
return implode(' · ', $parts);
}
private static function truncate(string $text): string
{
if (mb_strlen($text) <= self::TG_MAX_LEN) return $text;
$suffix = "\n…[обрезано]";
$cut = self::TG_MAX_LEN - mb_strlen($suffix);
return mb_substr($text, 0, $cut) . $suffix;
}
}