Files
Xboard/plugins/Telegram/Plugin.php

437 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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, '.', '');
}
}