|
@@ -6,6 +6,7 @@ use Illuminate\Database\Query\Builder;
|
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
use Illuminate\Validation\ValidationException;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
use RuntimeException;
|
|
use RuntimeException;
|
|
@@ -47,7 +48,7 @@ class TelegramChannelService
|
|
|
'id' => $legacy ? 'prohibited' : 'sometimes|required|integer|min:1',
|
|
'id' => $legacy ? 'prohibited' : 'sometimes|required|integer|min:1',
|
|
|
'name' => 'nullable|string|max:255',
|
|
'name' => 'nullable|string|max:255',
|
|
|
'chatId' => 'required|string|max:64',
|
|
'chatId' => 'required|string|max:64',
|
|
|
- 'bot_id' => 'nullable|integer|min:1|prohibits:bot_token',
|
|
|
|
|
|
|
+ 'bot_id' => 'sometimes|nullable|integer|min:1|prohibits:bot_token',
|
|
|
'bot_username' => ['nullable', 'string', 'max:64', 'regex:/^@?[a-z][a-z0-9_]*$/iD'],
|
|
'bot_username' => ['nullable', 'string', 'max:64', 'regex:/^@?[a-z][a-z0-9_]*$/iD'],
|
|
|
'bot_token' => ['nullable', 'string', 'max:255', 'regex:/^[1-9][0-9]*:[a-z0-9_-]+$/iD'],
|
|
'bot_token' => ['nullable', 'string', 'max:255', 'regex:/^[1-9][0-9]*:[a-z0-9_-]+$/iD'],
|
|
|
'enabled' => 'sometimes|required|boolean',
|
|
'enabled' => 'sometimes|required|boolean',
|
|
@@ -188,7 +189,21 @@ class TelegramChannelService
|
|
|
public function publish(int $id, bool $pin): array
|
|
public function publish(int $id, bool $pin): array
|
|
|
{
|
|
{
|
|
|
$row = $this->find($id);
|
|
$row = $this->find($id);
|
|
|
- $channel = $this->present($row, (int) $row->default_channel_id === $id, true);
|
|
|
|
|
|
|
+ return $this->publishContent($row, $this->storedMessage($row), $pin);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function publishMessage(int $id, array $message, bool $pin, string $target): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $row = $this->find($id);
|
|
|
|
|
+ if (($row->telegram_chat_id ?? $row->target) !== $target) {
|
|
|
|
|
+ throw ValidationException::withMessages(['channel_id' => '频道标识已变更,请重新创建发送记录']);
|
|
|
|
|
+ }
|
|
|
|
|
+ return $this->publishContent($row, $message, $pin);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function publishContent(object $row, array $message, bool $pin): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $channel = $this->present($row, false) + $message;
|
|
|
if (!$channel['enabled']) {
|
|
if (!$channel['enabled']) {
|
|
|
throw ValidationException::withMessages(['id' => '请先启用频道']);
|
|
throw ValidationException::withMessages(['id' => '请先启用频道']);
|
|
|
}
|
|
}
|
|
@@ -323,6 +338,136 @@ class TelegramChannelService
|
|
|
}, 3);
|
|
}, 3);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ public function privateMenu(int $id, string $language): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $menus = json_decode($this->findBot($id)->private_menus ?? '{}', true);
|
|
|
|
|
+ return $menus[$language] ?? ['reply' => '', 'buttons' => []];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function savePrivateMenu(int $id, string $language, array $input): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $menu = Validator::make($input, [
|
|
|
|
|
+ 'reply' => 'required|string|max:4096',
|
|
|
|
|
+ 'buttons' => 'present|array|list|max:20',
|
|
|
|
|
+ 'buttons.*' => 'required|array|list|max:8',
|
|
|
|
|
+ 'buttons.*.*' => 'required|string|max:128',
|
|
|
|
|
+ ])->validate();
|
|
|
|
|
+
|
|
|
|
|
+ return DB::transaction(function () use ($id, $language, $menu) {
|
|
|
|
|
+ $bot = DB::table('telegram_bots')->where('id', $id)->lockForUpdate()->first()
|
|
|
|
|
+ ?? throw ValidationException::withMessages(['id' => '机器人不存在']);
|
|
|
|
|
+ $menus = json_decode($bot->private_menus ?? '{}', true);
|
|
|
|
|
+ $menus[$language] = $menu;
|
|
|
|
|
+ DB::table('telegram_bots')->where('id', $id)->update([
|
|
|
|
|
+ 'private_menus' => json_encode($menus, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
|
|
|
|
|
+ 'updated_at' => now(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ return $menu;
|
|
|
|
|
+ }, 3);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function registerPrivateWebhook(int $id): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $bot = $this->findBot($id);
|
|
|
|
|
+ $token = $this->storedToken($bot);
|
|
|
|
|
+ if ($token === config('services.telegram.token')) {
|
|
|
|
|
+ return ['system_bot' => true];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $secret = $bot->webhook_secret;
|
|
|
|
|
+ if ($secret === null) {
|
|
|
|
|
+ DB::table('telegram_bots')->where('id', $id)->whereNull('webhook_secret')
|
|
|
|
|
+ ->update(['webhook_secret' => bin2hex(random_bytes(32))]);
|
|
|
|
|
+ $bot = $this->findBot($id);
|
|
|
|
|
+ $secret = $bot->webhook_secret;
|
|
|
|
|
+ }
|
|
|
|
|
+ $webhookUrl = rtrim((string) config('app.url'), '/') . '/api/telegramBot/' . $id . '/webhook';
|
|
|
|
|
+ try {
|
|
|
|
|
+ $telegram = $this->telegram($token);
|
|
|
|
|
+ $existingUrl = (string) $telegram->getWebhookInfo()->get('url');
|
|
|
|
|
+ } catch (\Exception) {
|
|
|
|
|
+ throw ValidationException::withMessages(['id' => '无法读取 Telegram 接入状态,请检查 Bot Token']);
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($existingUrl !== '' && $existingUrl !== $webhookUrl) {
|
|
|
|
|
+ throw ValidationException::withMessages(['id' => '机器人已接入其他 Webhook,请确认停用原接入后重试']);
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ $registered = $telegram->setWebhook([
|
|
|
|
|
+ 'url' => $webhookUrl,
|
|
|
|
|
+ 'secret_token' => $secret,
|
|
|
|
|
+ 'allowed_updates' => ['message'],
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } catch (\Exception) {
|
|
|
|
|
+ $registered = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!$registered) {
|
|
|
|
|
+ throw ValidationException::withMessages(['id' => '无法接入 Telegram,请检查 Bot Token 和站点 HTTPS 配置']);
|
|
|
|
|
+ }
|
|
|
|
|
+ return ['system_bot' => false];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function handlePrivateWebhook(int $id, string $secret, array $update): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $bot = DB::table('telegram_bots')->where('id', $id)->first();
|
|
|
|
|
+ abort_unless($bot?->webhook_secret && hash_equals($bot->webhook_secret, $secret), 404);
|
|
|
|
|
+ if (is_int($update['update_id'] ?? null)) {
|
|
|
|
|
+ rescue(fn () => Log::channel('telegram_webhook')->info('Telegram 收到更新', ['bot_id' => $id, 'update' => $update]));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $message = $update['message'] ?? [];
|
|
|
|
|
+ $chatId = $message['chat']['id'] ?? null;
|
|
|
|
|
+ if (($message['chat']['type'] ?? null) !== 'private' || !is_int($chatId)
|
|
|
|
|
+ || !is_string($message['text'] ?? null) || !preg_match('/^\/start(?:\s|$)/D', $message['text'])) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $language = strtolower(substr((string) ($message['from']['language_code'] ?? ''), 0, 2));
|
|
|
|
|
+ $menu = $this->configuredPrivateMenu($bot, $language);
|
|
|
|
|
+ if ($menu === null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ $this->sendPrivateMenu($this->telegram($this->storedToken($bot)), $chatId, $menu);
|
|
|
|
|
+ } catch (\Exception) {
|
|
|
|
|
+ Log::warning('Telegram 私聊菜单发送失败', ['bot_id' => $id]);
|
|
|
|
|
+ abort(503);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function sendSystemPrivateMenu($chatId, string $language): bool
|
|
|
|
|
+ {
|
|
|
|
|
+ $token = config('services.telegram.token');
|
|
|
|
|
+ if (!$token) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (DB::table('telegram_bots')->whereNotNull('private_menus')->cursor() as $bot) {
|
|
|
|
|
+ if ($token === $this->storedToken($bot) && ($menu = $this->configuredPrivateMenu($bot, $language)) !== null) {
|
|
|
|
|
+ $this->sendPrivateMenu($this->telegram(null), $chatId, $menu);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function configuredPrivateMenu(object $bot, string $language): ?array
|
|
|
|
|
+ {
|
|
|
|
|
+ $menus = json_decode($bot->private_menus ?? '{}', true);
|
|
|
|
|
+ return $menus[$language] ?? $menus['en'] ?? $menus['zh'] ?? ($menus['vi'] ?? null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function sendPrivateMenu(Api $telegram, $chatId, array $menu): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $keyboard = $menu['buttons'] === []
|
|
|
|
|
+ ? ['remove_keyboard' => true]
|
|
|
|
|
+ : ['keyboard' => $menu['buttons'], 'resize_keyboard' => true];
|
|
|
|
|
+ $telegram->sendMessage([
|
|
|
|
|
+ 'chat_id' => $chatId,
|
|
|
|
|
+ 'text' => $menu['reply'],
|
|
|
|
|
+ 'reply_markup' => json_encode($keyboard, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private function findBot(int $id): object
|
|
private function findBot(int $id): object
|
|
|
{
|
|
{
|
|
|
return DB::table('telegram_bots')->where('id', $id)->first()
|
|
return DB::table('telegram_bots')->where('id', $id)->first()
|
|
@@ -364,10 +509,14 @@ class TelegramChannelService
|
|
|
|
|
|
|
|
private function resolve(string $target, #[\SensitiveParameter] ?string $token): array
|
|
private function resolve(string $target, #[\SensitiveParameter] ?string $token): array
|
|
|
{
|
|
{
|
|
|
|
|
+ if ($token === null && !config('services.telegram.token')) {
|
|
|
|
|
+ throw ValidationException::withMessages(['bot_id' => '系统机器人尚未配置,请选择已添加的机器人']);
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
$chat = $this->telegram($token)->getChat(['chat_id' => $target]);
|
|
$chat = $this->telegram($token)->getChat(['chat_id' => $target]);
|
|
|
} catch (\Exception) {
|
|
} catch (\Exception) {
|
|
|
- throw ValidationException::withMessages(['chatId' => '无法读取频道,请检查频道标识及机器人是否已加入']);
|
|
|
|
|
|
|
+ $bot = $token === null ? '系统机器人' : '所选机器人';
|
|
|
|
|
+ throw ValidationException::withMessages(['chatId' => "无法读取频道,请检查频道标识,并确认{$bot}已加入目标频道"]);
|
|
|
}
|
|
}
|
|
|
$id = (string) $chat->get('id');
|
|
$id = (string) $chat->get('id');
|
|
|
if ($chat->get('type') !== 'channel' || !preg_match('/^-[1-9]\d{1,19}$/D', $id)) {
|
|
if ($chat->get('type') !== 'channel' || !preg_match('/^-[1-9]\d{1,19}$/D', $id)) {
|
|
@@ -413,7 +562,7 @@ class TelegramChannelService
|
|
|
return preg_match('/^@?[a-z][a-z0-9_]{3,31}$/iD', $value) ? '@' . strtolower(ltrim($value, '@')) : null;
|
|
return preg_match('/^@?[a-z][a-z0-9_]{3,31}$/iD', $value) ? '@' . strtolower(ltrim($value, '@')) : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static function message(array $input): array
|
|
|
|
|
|
|
+ public static function message(array $input): array
|
|
|
{
|
|
{
|
|
|
$data = Validator::make($input, [
|
|
$data = Validator::make($input, [
|
|
|
'type' => 'required|in:text,image,video',
|
|
'type' => 'required|in:text,image,video',
|