mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-03 10:30:51 +08:00
437 lines
13 KiB
PHP
437 lines
13 KiB
PHP
<?php
|
||
|
||
namespace Plugin\Telegram;
|
||
|
||
use App\Models\Order;
|
||
use App\Models\Ticket;
|
||
use App\Models\User;
|
||
use App\Services\Plugin\AbstractPlugin;
|
||
use App\Services\Plugin\HookManager;
|
||
use App\Services\TelegramService;
|
||
use App\Services\TicketService;
|
||
use App\Utils\Helper;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class Plugin extends AbstractPlugin
|
||
{
|
||
protected array $commands = [];
|
||
protected TelegramService $telegramService;
|
||
|
||
protected array $commandConfigs = [
|
||
'/start' => ['description' => '开始使用', 'handler' => 'handleStartCommand'],
|
||
'/bind' => ['description' => '绑定账号', 'handler' => 'handleBindCommand'],
|
||
'/traffic' => ['description' => '查看流量', 'handler' => 'handleTrafficCommand'],
|
||
'/getlatesturl' => ['description' => '获取订阅链接', 'handler' => 'handleGetLatestUrlCommand'],
|
||
'/unbind' => ['description' => '解绑账号', 'handler' => 'handleUnbindCommand'],
|
||
];
|
||
|
||
public function boot(): void
|
||
{
|
||
$this->telegramService = new TelegramService();
|
||
$this->registerDefaultCommands();
|
||
|
||
$this->filter('telegram.message.handle', [$this, 'handleMessage'], 10);
|
||
$this->listen('telegram.message.unhandled', [$this, 'handleUnknownCommand'], 10);
|
||
$this->listen('telegram.message.error', [$this, 'handleError'], 10);
|
||
$this->filter('telegram.bot.commands', [$this, 'addBotCommands'], 10);
|
||
$this->listen('ticket.create.after', [$this, 'sendTicketNotify'], 10);
|
||
$this->listen('ticket.reply.user.after', [$this, 'sendTicketNotify'], 10);
|
||
$this->listen('payment.notify.success', [$this, 'sendPaymentNotify'], 10);
|
||
}
|
||
|
||
public function sendPaymentNotify(Order $order): void
|
||
{
|
||
if (!$this->getConfig('enable_payment_notify', true)) {
|
||
return;
|
||
}
|
||
|
||
$payment = $order->payment;
|
||
if (!$payment) {
|
||
Log::warning('支付通知失败:订单关联的支付方式不存在', ['order_id' => $order->id]);
|
||
return;
|
||
}
|
||
|
||
$message = sprintf(
|
||
"💰成功收款%s元\n" .
|
||
"———————————————\n" .
|
||
"支付接口:%s\n" .
|
||
"支付渠道:%s\n" .
|
||
"本站订单:`%s`",
|
||
$order->total_amount / 100,
|
||
Helper::escapeMarkdown($payment->payment),
|
||
Helper::escapeMarkdown($payment->name),
|
||
$order->trade_no
|
||
);
|
||
$this->telegramService->sendMessageWithAdmin($message, true);
|
||
}
|
||
|
||
public function sendTicketNotify(Ticket $ticket): void
|
||
{
|
||
if (!$this->getConfig('enable_ticket_notify', true)) {
|
||
return;
|
||
}
|
||
|
||
$message = $ticket->messages()->latest()->first();
|
||
$user = User::find($ticket->user_id);
|
||
if (!$user)
|
||
return;
|
||
$user->load('plan');
|
||
$transfer_enable = $this->transferToGBString($user->transfer_enable);
|
||
$remaining_traffic = $this->transferToGBString($user->transfer_enable - $user->u - $user->d);
|
||
$u = $this->transferToGBString($user->u);
|
||
$d = $this->transferToGBString($user->d);
|
||
$expired_at = $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '长期有效';
|
||
$money = $user->balance / 100;
|
||
$affmoney = $user->commission_balance / 100;
|
||
$plan = $user->plan;
|
||
$ip = request()?->ip() ?? '';
|
||
$region = $ip ? (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? (new \Ip2Region())->simple($ip) : 'NULL') : '';
|
||
$TGmessage = "📮 *工单提醒* #{$ticket->id}\n";
|
||
$TGmessage .= "━━━━━━━━━━━━━━━━━━━━\n";
|
||
$TGmessage .= "📧 邮箱: `{$user->email}`\n";
|
||
$TGmessage .= "📍 位置: `{$region}`\n";
|
||
|
||
if ($plan) {
|
||
$TGmessage .= "📦 套餐: `" . Helper::escapeMarkdown($plan->name) . "`\n";
|
||
$TGmessage .= "📊 流量: `{$remaining_traffic}G / {$transfer_enable}G` (剩余/总计)\n";
|
||
$TGmessage .= "⬆️⬇️ 已用: `{$u}G / {$d}G`\n";
|
||
$TGmessage .= "⏰ 到期: `{$expired_at}`\n";
|
||
} else {
|
||
$TGmessage .= "📦 套餐: `未订购任何套餐`\n";
|
||
}
|
||
|
||
$TGmessage .= "💰 余额: `{$money}元`\n";
|
||
$TGmessage .= "💸 佣金: `{$affmoney}元`\n";
|
||
$TGmessage .= "━━━━━━━━━━━━━━━━━━━━\n";
|
||
$TGmessage .= "📝 *主题*: `" . Helper::escapeMarkdown($ticket->subject) . "`\n";
|
||
$TGmessage .= "💬 *内容*: `" . Helper::escapeMarkdown($message->message) . "`";
|
||
$this->telegramService->sendMessageWithAdmin($TGmessage, true);
|
||
}
|
||
|
||
protected function registerDefaultCommands(): void
|
||
{
|
||
foreach ($this->commandConfigs as $command => $config) {
|
||
$this->registerTelegramCommand($command, [$this, $config['handler']]);
|
||
}
|
||
|
||
$this->registerReplyHandler('/(📮.*?工单提醒.*?#?|工单ID: ?)(\\d+)/', [$this, 'handleTicketReply']);
|
||
}
|
||
|
||
public function registerTelegramCommand(string $command, callable $handler): void
|
||
{
|
||
$this->commands['commands'][$command] = $handler;
|
||
}
|
||
|
||
public function registerReplyHandler(string $regex, callable $handler): void
|
||
{
|
||
$this->commands['replies'][$regex] = $handler;
|
||
}
|
||
|
||
/**
|
||
* 发送消息给用户
|
||
*/
|
||
protected function sendMessage(object $msg, string $message): void
|
||
{
|
||
$this->telegramService->sendMessage($msg->chat_id, $message, 'markdown');
|
||
}
|
||
|
||
/**
|
||
* 检查是否为私聊
|
||
*/
|
||
protected function checkPrivateChat(object $msg): bool
|
||
{
|
||
if (!$msg->is_private) {
|
||
$this->sendMessage($msg, '请在私聊中使用此命令');
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取绑定的用户
|
||
*/
|
||
protected function getBoundUser(object $msg): ?User
|
||
{
|
||
$user = User::where('telegram_id', $msg->chat_id)->first();
|
||
if (!$user) {
|
||
$this->sendMessage($msg, '请先绑定账号');
|
||
return null;
|
||
}
|
||
return $user;
|
||
}
|
||
|
||
public function handleStartCommand(object $msg): void
|
||
{
|
||
$welcomeTitle = $this->getConfig('start_welcome_title', '🎉 欢迎使用 XBoard Telegram Bot!');
|
||
$botDescription = $this->getConfig('start_bot_description', '🤖 我是您的专属助手,可以帮助您:\\n• 绑定您的 XBoard 账号\\n• 查看流量使用情况\\n• 获取最新订阅链接\\n• 管理账号绑定状态');
|
||
$footer = $this->getConfig('start_footer', '💡 提示:所有命令都需要在私聊中使用');
|
||
|
||
$welcomeText = $welcomeTitle . "\n\n" . $botDescription . "\n\n";
|
||
|
||
$user = User::where('telegram_id', $msg->chat_id)->first();
|
||
if ($user) {
|
||
$welcomeText .= "✅ 您已绑定账号:{$user->email}\n\n";
|
||
$welcomeText .= $this->getConfig('start_unbind_guide', '📋 可用命令:\\n/traffic - 查看流量使用情况\\n/getlatesturl - 获取订阅链接\\n/unbind - 解绑账号');
|
||
} else {
|
||
$welcomeText .= $this->getConfig('start_bind_guide', '🔗 请先绑定您的 XBoard 账号:\\n1. 登录您的 XBoard 账户\\n2. 复制您的订阅链接\\n3. 发送 /bind + 订阅链接') . "\n\n";
|
||
$welcomeText .= $this->getConfig('start_bind_commands', '📋 可用命令:\\n/bind [订阅链接] - 绑定账号');
|
||
}
|
||
|
||
$welcomeText .= "\n\n" . $footer;
|
||
$welcomeText = str_replace('\\n', "\n", $welcomeText);
|
||
|
||
$this->sendMessage($msg, $welcomeText);
|
||
}
|
||
|
||
public function handleMessage(bool $handled, array $data): bool
|
||
{
|
||
list($msg) = $data;
|
||
if ($handled)
|
||
return $handled;
|
||
|
||
try {
|
||
return match ($msg->message_type) {
|
||
'message' => $this->handleCommandMessage($msg),
|
||
'reply_message' => $this->handleReplyMessage($msg),
|
||
default => false
|
||
};
|
||
} catch (\Exception $e) {
|
||
Log::error('Telegram 命令处理意外错误', [
|
||
'command' => $msg->command ?? 'unknown',
|
||
'chat_id' => $msg->chat_id ?? 'unknown',
|
||
'error' => $e->getMessage(),
|
||
'file' => $e->getFile(),
|
||
'line' => $e->getLine()
|
||
]);
|
||
|
||
if (isset($msg->chat_id)) {
|
||
$this->telegramService->sendMessage($msg->chat_id, '系统繁忙,请稍后重试');
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
protected function handleCommandMessage(object $msg): bool
|
||
{
|
||
if (!isset($this->commands['commands'][$msg->command])) {
|
||
return false;
|
||
}
|
||
|
||
call_user_func($this->commands['commands'][$msg->command], $msg);
|
||
return true;
|
||
}
|
||
|
||
protected function handleReplyMessage(object $msg): bool
|
||
{
|
||
if (!isset($this->commands['replies'])) {
|
||
return false;
|
||
}
|
||
|
||
foreach ($this->commands['replies'] as $regex => $handler) {
|
||
if (preg_match($regex, $msg->reply_text, $matches)) {
|
||
call_user_func($handler, $msg, $matches);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public function handleUnknownCommand(array $data): void
|
||
{
|
||
list($msg) = $data;
|
||
if (!$msg->is_private || $msg->message_type !== 'message')
|
||
return;
|
||
|
||
$helpText = $this->getConfig('help_text', '未知命令,请查看帮助');
|
||
$this->telegramService->sendMessage($msg->chat_id, $helpText);
|
||
}
|
||
|
||
public function handleError(array $data): void
|
||
{
|
||
list($msg, $e) = $data;
|
||
Log::error('Telegram 消息处理错误', [
|
||
'chat_id' => $msg->chat_id ?? 'unknown',
|
||
'command' => $msg->command ?? 'unknown',
|
||
'message_type' => $msg->message_type ?? 'unknown',
|
||
'error' => $e->getMessage(),
|
||
'file' => $e->getFile(),
|
||
'line' => $e->getLine()
|
||
]);
|
||
}
|
||
|
||
public function handleBindCommand(object $msg): void
|
||
{
|
||
if (!$this->checkPrivateChat($msg)) {
|
||
return;
|
||
}
|
||
|
||
$subscribeUrl = $msg->args[0] ?? null;
|
||
if (!$subscribeUrl) {
|
||
$this->sendMessage($msg, '参数有误,请携带订阅地址发送');
|
||
return;
|
||
}
|
||
|
||
$token = $this->extractTokenFromUrl($subscribeUrl);
|
||
if (!$token) {
|
||
$this->sendMessage($msg, '订阅地址无效');
|
||
return;
|
||
}
|
||
|
||
$user = User::where('token', $token)->first();
|
||
if (!$user) {
|
||
$this->sendMessage($msg, '用户不存在');
|
||
return;
|
||
}
|
||
|
||
if ($user->telegram_id) {
|
||
$this->sendMessage($msg, '该账号已经绑定了Telegram账号');
|
||
return;
|
||
}
|
||
|
||
$user->telegram_id = $msg->chat_id;
|
||
if (!$user->save()) {
|
||
$this->sendMessage($msg, '设置失败');
|
||
return;
|
||
}
|
||
|
||
HookManager::call('user.telegram.bind.after', [$user]);
|
||
$this->sendMessage($msg, '绑定成功');
|
||
}
|
||
|
||
protected function extractTokenFromUrl(string $url): ?string
|
||
{
|
||
$parsedUrl = parse_url($url);
|
||
|
||
if (isset($parsedUrl['query'])) {
|
||
parse_str($parsedUrl['query'], $query);
|
||
if (isset($query['token'])) {
|
||
return $query['token'];
|
||
}
|
||
}
|
||
|
||
if (isset($parsedUrl['path'])) {
|
||
$pathParts = explode('/', trim($parsedUrl['path'], '/'));
|
||
$lastPart = end($pathParts);
|
||
return $lastPart ?: null;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public function handleTrafficCommand(object $msg): void
|
||
{
|
||
if (!$this->checkPrivateChat($msg)) {
|
||
return;
|
||
}
|
||
|
||
$user = $this->getBoundUser($msg);
|
||
if (!$user) {
|
||
return;
|
||
}
|
||
|
||
$transferUsed = $user->u + $user->d;
|
||
$transferTotal = $user->transfer_enable;
|
||
$transferRemaining = $transferTotal - $transferUsed;
|
||
$usagePercentage = $transferTotal > 0 ? ($transferUsed / $transferTotal) * 100 : 0;
|
||
|
||
$text = sprintf(
|
||
"📊 流量使用情况\n\n已用流量:%sG\n总流量:%sG\n剩余流量:%sG\n使用率:%.2f%%",
|
||
$this->transferToGBString($transferUsed),
|
||
$this->transferToGBString($transferTotal),
|
||
$this->transferToGBString($transferRemaining),
|
||
$usagePercentage
|
||
);
|
||
|
||
$this->sendMessage($msg, $text);
|
||
}
|
||
|
||
public function handleGetLatestUrlCommand(object $msg): void
|
||
{
|
||
if (!$this->checkPrivateChat($msg)) {
|
||
return;
|
||
}
|
||
|
||
$user = $this->getBoundUser($msg);
|
||
if (!$user) {
|
||
return;
|
||
}
|
||
|
||
$subscribeUrl = Helper::getSubscribeUrl($user->token);
|
||
$text = sprintf("🔗 您的订阅链接:\n\n%s", $subscribeUrl);
|
||
|
||
$this->sendMessage($msg, $text);
|
||
}
|
||
|
||
public function handleUnbindCommand(object $msg): void
|
||
{
|
||
if (!$this->checkPrivateChat($msg)) {
|
||
return;
|
||
}
|
||
|
||
$user = $this->getBoundUser($msg);
|
||
if (!$user) {
|
||
return;
|
||
}
|
||
|
||
$user->telegram_id = null;
|
||
if (!$user->save()) {
|
||
$this->sendMessage($msg, '解绑失败');
|
||
return;
|
||
}
|
||
|
||
$this->sendMessage($msg, '解绑成功');
|
||
}
|
||
|
||
public function handleTicketReply(object $msg, array $matches): void
|
||
{
|
||
$user = $this->getBoundUser($msg);
|
||
if (!$user) {
|
||
return;
|
||
}
|
||
|
||
if (!isset($matches[2]) || !is_numeric($matches[2])) {
|
||
Log::warning('Telegram 工单回复正则未匹配到工单ID', ['matches' => $matches, 'msg' => $msg]);
|
||
$this->sendMessage($msg, '未能识别工单ID,请直接回复工单提醒消息。');
|
||
return;
|
||
}
|
||
|
||
$ticketId = (int) $matches[2];
|
||
$ticket = Ticket::where('id', $ticketId)->first();
|
||
if (!$ticket) {
|
||
$this->sendMessage($msg, '工单不存在');
|
||
return;
|
||
}
|
||
|
||
$ticketService = new TicketService();
|
||
$ticketService->replyByAdmin(
|
||
$ticketId,
|
||
$msg->text,
|
||
$user->id
|
||
);
|
||
|
||
$this->sendMessage($msg, "工单 #{$ticketId} 回复成功");
|
||
}
|
||
|
||
/**
|
||
* 添加 Bot 命令到命令列表
|
||
*/
|
||
public function addBotCommands(array $commands): array
|
||
{
|
||
foreach ($this->commandConfigs as $command => $config) {
|
||
$commands[] = [
|
||
'command' => $command,
|
||
'description' => $config['description']
|
||
];
|
||
}
|
||
|
||
return $commands;
|
||
}
|
||
|
||
private function transferToGBString(float $transfer_enable, int $decimals = 2): string
|
||
{
|
||
return number_format(Helper::transferToGB($transfer_enable), $decimals, '.', '');
|
||
}
|
||
|
||
} |