| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- <?php
- namespace Longyi\Feishu\Services;
- use Illuminate\Support\Facades\Http;
- use Illuminate\Support\Facades\Log;
- /**
- * 飞书群自定义机器人客户端。
- *
- * 只负责把一条已经构造好的消息投递到 webhook,不关心业务语义。
- */
- class FeishuBot
- {
- public function __construct(
- protected ?string $webhookUrl = null,
- protected ?string $secret = null,
- protected ?int $timeout = null,
- ) {
- $this->webhookUrl ??= (string) config('feishu.webhook_url', '');
- $this->secret ??= (string) config('feishu.secret', '');
- $this->timeout ??= (int) config('feishu.timeout', 5);
- }
- /**
- * 开关打开且配置了 webhook 才算可用。
- */
- public function isEnabled(): bool
- {
- return (bool) config('feishu.enabled', false) && $this->webhookUrl !== '';
- }
- /**
- * 投递一条飞书消息。
- *
- * @param array $message 形如 ['msg_type' => 'interactive', 'card' => [...]]
- *
- * @throws \RuntimeException 飞书返回业务错误时抛出,交给调用方重试
- */
- public function send(array $message): bool
- {
- if (! $this->isEnabled()) {
- return false;
- }
- if ($this->secret !== '') {
- $timestamp = time();
- $message['timestamp'] = (string) $timestamp;
- $message['sign'] = $this->sign($timestamp);
- }
- $response = Http::timeout($this->timeout)
- ->asJson()
- ->post($this->webhookUrl, $message);
- /*
- * 飞书对业务错误同样返回 HTTP 200,必须看响应体里的 code,
- * 否则加签失败、机器人被停用这类问题会被当成发送成功。
- */
- $body = $response->json() ?? [];
- $code = $body['code'] ?? $body['StatusCode'] ?? null;
- if ($response->successful() && (int) $code === 0) {
- return true;
- }
- throw new \RuntimeException(sprintf(
- 'Feishu webhook rejected the message (http %d, code %s): %s',
- $response->status(),
- var_export($code, true),
- $body['msg'] ?? $body['StatusMessage'] ?? 'unknown error',
- ));
- }
- /**
- * 发送但不抛异常,用于绝对不能影响调用方的场景。
- */
- public function sendQuietly(array $message): bool
- {
- try {
- return $this->send($message);
- } catch (\Throwable $e) {
- Log::warning('Feishu notification failed: '.$e->getMessage());
- return false;
- }
- }
- /**
- * 加签:对 "{timestamp}\n{secret}" 做 HMAC-SHA256(空消息体),再 Base64。
- */
- public function sign(int $timestamp): string
- {
- return base64_encode(
- hash_hmac('sha256', '', $timestamp."\n".$this->secret, true)
- );
- }
- }
|