|
|
@@ -195,6 +195,65 @@ abstract class BaseService
|
|
|
return app(Api::class);
|
|
|
}
|
|
|
|
|
|
+ protected static function sendTelegramRequest(callable $callback, string $action, array $context = [])
|
|
|
+ {
|
|
|
+ $maxAttempts = max(1, (int)config('services.telegram.retry_attempts', 3));
|
|
|
+ $delaySeconds = max(1, (int)config('services.telegram.retry_delay_seconds', 3));
|
|
|
+
|
|
|
+ for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
|
|
+ try {
|
|
|
+ return $callback();
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ $safeMessage = self::sanitizeTelegramError($exception->getMessage());
|
|
|
+ $logContext = array_merge($context, [
|
|
|
+ 'action' => $action,
|
|
|
+ 'attempt' => $attempt,
|
|
|
+ 'max_attempts' => $maxAttempts,
|
|
|
+ 'error' => $safeMessage,
|
|
|
+ 'exception' => get_class($exception),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($attempt >= $maxAttempts || !self::isRetryableTelegramError($safeMessage)) {
|
|
|
+ Log::channel('issue')->warning('Telegram请求失败', $logContext);
|
|
|
+ throw new \RuntimeException($safeMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ $retryAfter = self::getTelegramRetryAfter($safeMessage, $attempt, $delaySeconds);
|
|
|
+ Log::channel('issue')->warning('Telegram请求失败,准备重试', $logContext + [
|
|
|
+ 'retry_after_seconds' => $retryAfter,
|
|
|
+ ]);
|
|
|
+ sleep($retryAfter);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function sanitizeTelegramError(string $message): string
|
|
|
+ {
|
|
|
+ return preg_replace('/bot\d+:[A-Za-z0-9_-]+/', 'bot[redacted]', $message);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function isRetryableTelegramError(string $message): bool
|
|
|
+ {
|
|
|
+ return str_contains($message, 'Too Many Requests')
|
|
|
+ || str_contains($message, 'retry after')
|
|
|
+ || str_contains($message, 'cURL error 7')
|
|
|
+ || str_contains($message, 'cURL error 28')
|
|
|
+ || str_contains($message, 'Failed to connect')
|
|
|
+ || str_contains($message, 'Connection timed out')
|
|
|
+ || str_contains($message, 'Operation timed out');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function getTelegramRetryAfter(string $message, int $attempt, int $delaySeconds): int
|
|
|
+ {
|
|
|
+ $maxDelaySeconds = max(1, (int)config('services.telegram.retry_max_delay_seconds', 10));
|
|
|
+
|
|
|
+ if (preg_match('/retry after (\d+)/i', $message, $matches)) {
|
|
|
+ return min($maxDelaySeconds, (int)$matches[1] + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ return min($maxDelaySeconds, $delaySeconds * $attempt);
|
|
|
+ }
|
|
|
+
|
|
|
// /**
|
|
|
// * @description: 群组通知(自动分段发送,支持中文与多字节字符)
|
|
|
// * @param string $text 通知内容
|
|
|
@@ -278,7 +337,10 @@ abstract class BaseService
|
|
|
if (count($keyboard) > 0) {
|
|
|
$botMsg['reply_markup'] = json_encode(['inline_keyboard' => $keyboard]);
|
|
|
}
|
|
|
- $response = self::telegram()->sendPhoto($botMsg);
|
|
|
+ $response = self::sendTelegramRequest(fn() => self::telegram()->sendPhoto($botMsg), 'sendPhoto', [
|
|
|
+ 'chat_id' => $botMsg['chat_id'],
|
|
|
+ 'has_image' => true,
|
|
|
+ ]);
|
|
|
} else {
|
|
|
foreach ($array as $key => $line) {
|
|
|
if (empty(str_ireplace(" ", "", str_ireplace("\n", '', $line)))) {
|
|
|
@@ -306,7 +368,11 @@ abstract class BaseService
|
|
|
];
|
|
|
if ($index > 0) {
|
|
|
$res[] = $botMsg;
|
|
|
- self::telegram()->sendMessage($botMsg);
|
|
|
+ self::sendTelegramRequest(fn() => self::telegram()->sendMessage($botMsg), 'sendMessage', [
|
|
|
+ 'chat_id' => $botMsg['chat_id'],
|
|
|
+ 'chunk_index' => $index,
|
|
|
+ 'has_image' => false,
|
|
|
+ ]);
|
|
|
} else {
|
|
|
if (count($keyboard) > 0) {
|
|
|
$botMsg['reply_markup'] = json_encode(['inline_keyboard' => $keyboard]);
|
|
|
@@ -318,15 +384,26 @@ abstract class BaseService
|
|
|
$botMsg['protect_content'] = true;
|
|
|
|
|
|
$res[] = $botMsg;
|
|
|
- $response = self::telegram()->sendPhoto($botMsg);
|
|
|
+ $response = self::sendTelegramRequest(fn() => self::telegram()->sendPhoto($botMsg), 'sendPhoto', [
|
|
|
+ 'chat_id' => $botMsg['chat_id'],
|
|
|
+ 'chunk_index' => $index,
|
|
|
+ 'has_image' => true,
|
|
|
+ ]);
|
|
|
} else {
|
|
|
$res[] = $botMsg;
|
|
|
- $response = self::telegram()->sendMessage($botMsg);
|
|
|
+ $response = self::sendTelegramRequest(fn() => self::telegram()->sendMessage($botMsg), 'sendMessage', [
|
|
|
+ 'chat_id' => $botMsg['chat_id'],
|
|
|
+ 'chunk_index' => $index,
|
|
|
+ 'has_image' => false,
|
|
|
+ ]);
|
|
|
}
|
|
|
if ($isTop === true) {
|
|
|
- self::telegram()->pinChatMessage([
|
|
|
+ self::sendTelegramRequest(fn() => self::telegram()->pinChatMessage([
|
|
|
'chat_id' => "@{$bettingGroup}",
|
|
|
'message_id' => $response->get('message_id')
|
|
|
+ ]), 'pinChatMessage', [
|
|
|
+ 'chat_id' => "@{$bettingGroup}",
|
|
|
+ 'message_id' => $response->get('message_id'),
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
@@ -371,10 +448,16 @@ abstract class BaseService
|
|
|
$botMsg['photo'] = InputFile::create($image);
|
|
|
$botMsg['caption'] = $text;
|
|
|
$botMsg['protect_content'] = false; // 防止转发
|
|
|
- self::telegram()->sendPhoto($botMsg);
|
|
|
+ self::sendTelegramRequest(fn() => self::telegram()->sendPhoto($botMsg), 'sendPhoto', [
|
|
|
+ 'chat_id' => $chatId,
|
|
|
+ 'has_image' => true,
|
|
|
+ ]);
|
|
|
} else {
|
|
|
$botMsg['text'] = $text;
|
|
|
- self::telegram()->sendMessage($botMsg);
|
|
|
+ self::sendTelegramRequest(fn() => self::telegram()->sendMessage($botMsg), 'sendMessage', [
|
|
|
+ 'chat_id' => $chatId,
|
|
|
+ 'has_image' => false,
|
|
|
+ ]);
|
|
|
}
|
|
|
}
|
|
|
|