Files
Vladimir Bryzgalov 2de8f0be57 fix(formatter+telegram): drop tel: inline button, mark all 4xx as permanent
Pilot on washanyanya.ru exposed that Telegram Bot API rejects tel:
URLs in inline_keyboard with HTTP 400 "Wrong port number specified
in the URL". Queue was stuck on 5 items retrying the same failure
4 times each.

- MessageFormatter: always returns reply_markup = null. The phone
  number is already rendered in the '📱 ...' line, and mobile
  Telegram clients linkify it automatically.
- TelegramClient::is_permanent: treat all 4xx except 429 as permanent
  client-side errors, so future payload-shape bugs surface in the
  dashboard widget immediately instead of burning five retry cycles.
- MessageFormatterTest updated: the old test asserting tel: in
  reply_markup is replaced with one asserting reply_markup is null
  and the phone still appears in the text body.
- Spec §9 + §19 updated, §19's open question closed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:55:06 +05:00

75 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Cf7stg;
final class TelegramClient
{
/**
* Send a message. Throws TelegramException on error.
*
* @param string|int $chat_id
* @param string $text Pre-escaped HTML
* @param array|null $reply_markup Optional inline_keyboard structure
*/
public static function send_message($chat_id, string $text, ?array $reply_markup = null): array
{
$token = Settings::get_bot_token();
if ($token === '' || $chat_id === '' || $chat_id === null) {
throw new TelegramException('Bot token or chat_id not configured', 0, true);
}
$body = [
'chat_id' => $chat_id,
'text' => $text,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
];
if ($reply_markup !== null) {
$body['reply_markup'] = wp_json_encode($reply_markup);
}
$url = 'https://api.telegram.org/bot' . $token . '/sendMessage';
$response = wp_remote_post($url, [
'timeout' => 10,
'body' => $body,
]);
if (is_wp_error($response)) {
throw new TelegramException('Network: ' . $response->get_error_message(), 0, false);
}
$code = (int)wp_remote_retrieve_response_code($response);
$raw = (string)wp_remote_retrieve_body($response);
$data = json_decode($raw, true);
if ($code === 200 && is_array($data) && !empty($data['ok'])) {
return $data;
}
$desc = is_array($data) ? (string)($data['description'] ?? '') : '';
$retry_after = 0;
if (is_array($data) && isset($data['parameters']['retry_after'])) {
$retry_after = (int)$data['parameters']['retry_after'];
}
$permanent = self::is_permanent($code, $desc);
throw new TelegramException(
sprintf('HTTP %d %s', $code, $desc !== '' ? $desc : 'unknown error'),
$code,
$permanent,
$retry_after
);
}
private static function is_permanent(int $code, string $desc): bool
{
// 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;
}
}