['emoji' => '🟢', 'text' => 'похоже клиент'], 'unclear' => ['emoji' => '🟡', 'text' => 'неясно'], 'spam' => ['emoji' => '🔴', 'text' => 'похоже спам'], 'drop' => ['emoji' => '⛔', 'text' => 'авто-дроп'], ]; /** * @param array{ * fields:array, * 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; } }