diff --git a/docs/superpowers/plans/2026-04-17-cf7-spam-to-telegram.md b/docs/superpowers/plans/2026-04-17-cf7-spam-to-telegram.md index 6e0e0ee..5b25e51 100644 --- a/docs/superpowers/plans/2026-04-17-cf7-spam-to-telegram.md +++ b/docs/superpowers/plans/2026-04-17-cf7-spam-to-telegram.md @@ -978,8 +978,12 @@ final class MessageFormatterTest extends TestCase self::assertStringContainsString('Akismet', $result['text']); } - public function test_tel_button_present_when_phone_valid(): void + public function test_reply_markup_is_null_because_tg_rejects_tel_urls(): void { + // Telegram Bot API rejects tel: URLs in inline_keyboard with + // HTTP 400 "Wrong port number specified in the URL". We no longer + // emit any inline button; the phone number is rendered in the + // message body instead and mobile TG clients auto-linkify it. $fields = ['your-tel' => '+79031234567', 'your-name' => 'Анна']; $classification = Classifier::classify($fields, ['reason' => 'Akismet']); $r = MessageFormatter::build([ @@ -989,9 +993,9 @@ final class MessageFormatterTest extends TestCase 'ip' => '', 'user_agent' => '', 'is_retro' => false, ]); - self::assertNotNull($r['reply_markup']); - $json = json_encode($r['reply_markup']); - self::assertStringContainsString('tel:+79031234567', $json); + self::assertNull($r['reply_markup']); + // Phone must still be present in the text, formatted as Russian pretty. + self::assertStringContainsString('+7 (903) 123-45-67', $r['text']); } public function test_no_tel_button_when_no_phone(): void @@ -1174,16 +1178,11 @@ final class MessageFormatter $text = implode("\n", $lines); $text = self::truncate($text); - $markup = null; - if ($phone !== null) { - $markup = [ - 'inline_keyboard' => [[ - ['text' => '📞 Позвонить: ' . $phone['pretty'], 'url' => 'tel:' . $phone['e164']], - ]], - ]; - } - - return ['text' => $text, 'reply_markup' => $markup]; + // 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 @@ -1766,13 +1765,11 @@ final class TelegramClient private static function is_permanent(int $code, string $desc): bool { - if ($code === 401 || $code === 403) return true; - if ($code === 400) { - $d = strtolower($desc); - if (strpos($d, 'chat not found') !== false) return true; - if (strpos($d, 'bot was blocked') !== false) return true; - if (strpos($d, 'user is deactivated') !== false) return true; - } + // 4xx (except 429 rate-limit) are client-side errors: bad request, + // invalid chat, revoked token, malformed payload — retrying will not + // help. Mark as permanent so the queue gives up immediately and the + // admin is notified via the dashboard widget. + if ($code >= 400 && $code < 500 && $code !== 429) return true; return false; } } diff --git a/docs/superpowers/specs/2026-04-17-cf7-spam-to-telegram-design.md b/docs/superpowers/specs/2026-04-17-cf7-spam-to-telegram-design.md index 042c865..83e02f9 100644 --- a/docs/superpowers/specs/2026-04-17-cf7-spam-to-telegram-design.md +++ b/docs/superpowers/specs/2026-04-17-cf7-spam-to-telegram-design.md @@ -249,8 +249,7 @@ apply_filters('cf7stg_classifier_thresholds', ['client' => 3, 'spam' => -2]); ``` **Inline-клавиатура:** -- Если распарсился валидный RU-телефон — одна кнопка `"📞 Позвонить: {phone_formatted}"` → `url: "tel:{e164}"`. -- Без телефона — `reply_markup = null` (кнопок нет). +- `reply_markup = null` всегда. Telegram Bot API отклоняет `tel:` URLs в inline_keyboard с ошибкой HTTP 400 `Wrong port number specified in the URL`. Номер показывается в теле сообщения (строка `📱 ...`), мобильный TG-клиент автоматически делает его кликабельным. **Site title:** `Settings::get_site_title()` возвращает либо оверрайд из настроек, либо декодированный через `idn_to_utf8()` host из `home_url()`. Например, `xn--80aae0ca7d8bb.xn--p1ai` → `вашаняня.рф`. @@ -397,6 +396,6 @@ $schedules['cf7stg_every_minute'] = ['interval' => 60, 'display' => 'Раз в ## 19. Открытые вопросы (решаем во время реализации) -1. **`tel:`-ссылки в inline-кнопках Telegram.** Bot API документация разрешает только `http/https/tg`, но практика показывает, что `tel:` работает в клиентах. Проверить на первом пилотном сайте; если Telegram отклонит — fallback: показать номер текстом без кнопки, добавить префикс `📞 ` для ясности. +1. ~~**`tel:`-ссылки в inline-кнопках Telegram.**~~ **РЕШЕНО (2026-04-17 пилот).** Telegram Bot API отклонил `tel:` URLs (HTTP 400, `Wrong port number specified in the URL`). Inline-кнопки полностью убраны; номер показывается в теле сообщения, мобильные клиенты сами делают его кликабельным. 2. **Извлечение `flamingo_post_id`.** Flamingo не даёт прямой ссылки submission→post после сохранения. План: после сохранения Flamingo (`wpcf7_submission` priority 20) искать в `wp_posts` записи `post_type=flamingo_inbound`, sort by date DESC, limit 1, фильтр по содержимому `post_content` (сравнение с полями submission). Если совпадение ненадёжное — плагин всё равно работает (`flamingo_post_id = NULL`, dedup ретро-выгрузки пропускается для этой записи, но основной поток не зависит от наличия id). 3. **Акцент HTML vs MarkdownV2.** Если в процессе пилота выяснится, что клиенты часто пишут символы, ломающие HTML даже после экранирования — рассмотреть переход на plain text (без форматирования, эмодзи — unicode-символы, не требуют разметки). diff --git a/includes/class-message-formatter.php b/includes/class-message-formatter.php index 89e83c4..27c24e9 100644 --- a/includes/class-message-formatter.php +++ b/includes/class-message-formatter.php @@ -71,16 +71,11 @@ final class MessageFormatter $text = implode("\n", $lines); $text = self::truncate($text); - $markup = null; - if ($phone !== null) { - $markup = [ - 'inline_keyboard' => [[ - ['text' => '📞 Позвонить: ' . $phone['pretty'], 'url' => 'tel:' . $phone['e164']], - ]], - ]; - } - - return ['text' => $text, 'reply_markup' => $markup]; + // 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 diff --git a/includes/class-telegram-client.php b/includes/class-telegram-client.php index 8089d31..b81c163 100644 --- a/includes/class-telegram-client.php +++ b/includes/class-telegram-client.php @@ -64,13 +64,11 @@ final class TelegramClient private static function is_permanent(int $code, string $desc): bool { - if ($code === 401 || $code === 403) return true; - if ($code === 400) { - $d = strtolower($desc); - if (strpos($d, 'chat not found') !== false) return true; - if (strpos($d, 'bot was blocked') !== false) return true; - if (strpos($d, 'user is deactivated') !== false) return true; - } + // 4xx (except 429 rate-limit) are client-side errors: bad request, + // invalid chat, revoked token, malformed payload — retrying will not + // help. Mark as permanent so the queue gives up immediately and the + // admin is notified via the dashboard widget. + if ($code >= 400 && $code < 500 && $code !== 429) return true; return false; } } diff --git a/tests/MessageFormatterTest.php b/tests/MessageFormatterTest.php index 8eea6e3..11ac568 100644 --- a/tests/MessageFormatterTest.php +++ b/tests/MessageFormatterTest.php @@ -42,8 +42,12 @@ final class MessageFormatterTest extends TestCase self::assertStringContainsString('Akismet', $result['text']); } - public function test_tel_button_present_when_phone_valid(): void + public function test_reply_markup_is_null_because_tg_rejects_tel_urls(): void { + // Telegram Bot API rejects tel: URLs in inline_keyboard with + // HTTP 400 "Wrong port number specified in the URL". We no longer + // emit any inline button; the phone number is rendered in the + // message body instead and mobile TG clients auto-linkify it. $fields = ['your-tel' => '+79031234567', 'your-name' => 'Анна']; $classification = Classifier::classify($fields, ['reason' => 'Akismet']); $r = MessageFormatter::build([ @@ -53,9 +57,9 @@ final class MessageFormatterTest extends TestCase 'ip' => '', 'user_agent' => '', 'is_retro' => false, ]); - self::assertNotNull($r['reply_markup']); - $json = json_encode($r['reply_markup']); - self::assertStringContainsString('tel:+79031234567', $json); + self::assertNull($r['reply_markup']); + // Phone must still be present in the text, formatted as Russian pretty. + self::assertStringContainsString('+7 (903) 123-45-67', $r['text']); } public function test_no_tel_button_when_no_phone(): void