mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-03 10:30:51 +08:00
feat: enhance plugin management
- Add command support for plugin management - Optimize plugin management page layout - Add email copy functionality for users - Convert payment methods and Telegram Bot to plugin system
This commit is contained in:
105
plugins/AlipayF2f/Plugin.php
Normal file
105
plugins/AlipayF2f/Plugin.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\AlipayF2f;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Plugin\AlipayF2f\library\AlipayF2F;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function ($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['AlipayF2F'] = [
|
||||
'name' => $this->getConfig('display_name', '支付宝当面付'),
|
||||
'icon' => $this->getConfig('icon', '💙'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'app_id' => [
|
||||
'label' => '支付宝APPID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '支付宝开放平台应用的APPID'
|
||||
],
|
||||
'private_key' => [
|
||||
'label' => '支付宝私钥',
|
||||
'type' => 'textarea',
|
||||
'required' => true,
|
||||
'description' => '应用私钥,用于签名'
|
||||
],
|
||||
'public_key' => [
|
||||
'label' => '支付宝公钥',
|
||||
'type' => 'textarea',
|
||||
'required' => true,
|
||||
'description' => '支付宝公钥,用于验签'
|
||||
],
|
||||
'product_name' => [
|
||||
'label' => '自定义商品名称',
|
||||
'type' => 'string',
|
||||
'description' => '将会体现在支付宝账单中'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
try {
|
||||
$gateway = new AlipayF2F();
|
||||
$gateway->setMethod('alipay.trade.precreate');
|
||||
$gateway->setAppId($this->getConfig('app_id'));
|
||||
$gateway->setPrivateKey($this->getConfig('private_key'));
|
||||
$gateway->setAlipayPublicKey($this->getConfig('public_key'));
|
||||
$gateway->setNotifyUrl($order['notify_url']);
|
||||
$gateway->setBizContent([
|
||||
'subject' => $this->getConfig('product_name') ?? (admin_setting('app_name', 'XBoard') . ' - 订阅'),
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'] / 100
|
||||
]);
|
||||
$gateway->send();
|
||||
return [
|
||||
'type' => 0,
|
||||
'data' => $gateway->getQrCodeUrl()
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
throw new ApiException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
if ($params['trade_status'] !== 'TRADE_SUCCESS')
|
||||
return false;
|
||||
|
||||
$gateway = new AlipayF2F();
|
||||
$gateway->setAppId($this->getConfig('app_id'));
|
||||
$gateway->setPrivateKey($this->getConfig('private_key'));
|
||||
$gateway->setAlipayPublicKey($this->getConfig('public_key'));
|
||||
|
||||
try {
|
||||
if ($gateway->verify($params)) {
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
plugins/AlipayF2f/config.json
Normal file
8
plugins/AlipayF2f/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "AlipayF2F",
|
||||
"code": "alipay_f2f",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "AlipayF2F payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
150
plugins/Btcpay/Plugin.php
Normal file
150
plugins/Btcpay/Plugin.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Btcpay;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['BTCPay'] = [
|
||||
'name' => $this->getConfig('display_name', 'BTCPay'),
|
||||
'icon' => $this->getConfig('icon', '₿'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'btcpay_url' => [
|
||||
'label' => 'API接口所在网址',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '包含最后的斜杠,例如:https://your-btcpay.com/'
|
||||
],
|
||||
'btcpay_storeId' => [
|
||||
'label' => 'Store ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'BTCPay商店标识符'
|
||||
],
|
||||
'btcpay_api_key' => [
|
||||
'label' => 'API KEY',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '个人设置中的API KEY(非商店设置中的)'
|
||||
],
|
||||
'btcpay_webhook_key' => [
|
||||
'label' => 'WEBHOOK KEY',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Webhook通知密钥'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'jsonResponse' => true,
|
||||
'amount' => sprintf('%.2f', $order['total_amount'] / 100),
|
||||
'currency' => 'CNY',
|
||||
'metadata' => [
|
||||
'orderId' => $order['trade_no']
|
||||
]
|
||||
];
|
||||
|
||||
$params_string = @json_encode($params);
|
||||
$ret_raw = $this->curlPost($this->getConfig('btcpay_url') . 'api/v1/stores/' . $this->getConfig('btcpay_storeId') . '/invoices', $params_string);
|
||||
$ret = @json_decode($ret_raw, true);
|
||||
|
||||
if (empty($ret['checkoutLink'])) {
|
||||
throw new ApiException("error!");
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => $ret['checkoutLink'],
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$payload = trim(request()->getContent());
|
||||
$headers = getallheaders();
|
||||
$headerName = 'Btcpay-Sig';
|
||||
$signraturHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
$json_param = json_decode($payload, true);
|
||||
|
||||
$computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->getConfig('btcpay_webhook_key'));
|
||||
|
||||
if (!$this->hashEqual($signraturHeader, $computedSignature)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
}
|
||||
|
||||
$context = stream_context_create(array(
|
||||
'http' => array(
|
||||
'method' => 'GET',
|
||||
'header' => "Authorization:" . "token " . $this->getConfig('btcpay_api_key') . "\r\n"
|
||||
)
|
||||
));
|
||||
|
||||
$invoiceDetail = file_get_contents($this->getConfig('btcpay_url') . 'api/v1/stores/' . $this->getConfig('btcpay_storeId') . '/invoices/' . $json_param['invoiceId'], false, $context);
|
||||
$invoiceDetail = json_decode($invoiceDetail, true);
|
||||
|
||||
$out_trade_no = $invoiceDetail['metadata']["orderId"];
|
||||
$pay_trade_no = $json_param['invoiceId'];
|
||||
|
||||
return [
|
||||
'trade_no' => $out_trade_no,
|
||||
'callback_no' => $pay_trade_no
|
||||
];
|
||||
}
|
||||
|
||||
private function curlPost($url, $params = false)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||||
curl_setopt(
|
||||
$ch,
|
||||
CURLOPT_HTTPHEADER,
|
||||
array('Authorization:' . 'token ' . $this->getConfig('btcpay_api_key'), 'Content-Type: application/json')
|
||||
);
|
||||
$result = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function hashEqual($str1, $str2)
|
||||
{
|
||||
if (function_exists('hash_equals')) {
|
||||
return \hash_equals($str1, $str2);
|
||||
}
|
||||
|
||||
if (strlen($str1) != strlen($str2)) {
|
||||
return false;
|
||||
} else {
|
||||
$res = $str1 ^ $str2;
|
||||
$ret = 0;
|
||||
|
||||
for ($i = strlen($res) - 1; $i >= 0; $i--) {
|
||||
$ret |= ord($res[$i]);
|
||||
}
|
||||
return !$ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
plugins/Btcpay/config.json
Normal file
8
plugins/Btcpay/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "BTCPay",
|
||||
"code": "btcpay",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "BTCPay payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
112
plugins/CoinPayments/Plugin.php
Normal file
112
plugins/CoinPayments/Plugin.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\CoinPayments;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['CoinPayments'] = [
|
||||
'name' => $this->getConfig('display_name', 'CoinPayments'),
|
||||
'icon' => $this->getConfig('icon', '💰'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'coinpayments_merchant_id' => [
|
||||
'label' => 'Merchant ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '商户 ID,填写您在 Account Settings 中得到的 ID'
|
||||
],
|
||||
'coinpayments_ipn_secret' => [
|
||||
'label' => 'IPN Secret',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '通知密钥,填写您在 Merchant Settings 中自行设置的值'
|
||||
],
|
||||
'coinpayments_currency' => [
|
||||
'label' => '货币代码',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '填写您的货币代码(大写),建议与 Merchant Settings 中的值相同'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$parseUrl = parse_url($order['return_url']);
|
||||
$port = isset($parseUrl['port']) ? ":{$parseUrl['port']}" : '';
|
||||
$successUrl = "{$parseUrl['scheme']}://{$parseUrl['host']}{$port}";
|
||||
|
||||
$params = [
|
||||
'cmd' => '_pay_simple',
|
||||
'reset' => 1,
|
||||
'merchant' => $this->getConfig('coinpayments_merchant_id'),
|
||||
'item_name' => $order['trade_no'],
|
||||
'item_number' => $order['trade_no'],
|
||||
'want_shipping' => 0,
|
||||
'currency' => $this->getConfig('coinpayments_currency'),
|
||||
'amountf' => sprintf('%.2f', $order['total_amount'] / 100),
|
||||
'success_url' => $successUrl,
|
||||
'cancel_url' => $order['return_url'],
|
||||
'ipn_url' => $order['notify_url']
|
||||
];
|
||||
|
||||
$params_string = http_build_query($params);
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => 'https://www.coinpayments.net/index.php?' . $params_string
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|string
|
||||
{
|
||||
if (!isset($params['merchant']) || $params['merchant'] != trim($this->getConfig('coinpayments_merchant_id'))) {
|
||||
throw new ApiException('No or incorrect Merchant ID passed');
|
||||
}
|
||||
|
||||
$headers = getallheaders();
|
||||
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$request = stripslashes(http_build_query($params));
|
||||
|
||||
$headerName = 'Hmac';
|
||||
$signHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
|
||||
$hmac = hash_hmac("sha512", $request, trim($this->getConfig('coinpayments_ipn_secret')));
|
||||
|
||||
if (!hash_equals($hmac, $signHeader)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
}
|
||||
|
||||
$status = $params['status'];
|
||||
if ($status >= 100 || $status == 2) {
|
||||
return [
|
||||
'trade_no' => $params['item_number'],
|
||||
'callback_no' => $params['txn_id'],
|
||||
'custom_result' => 'IPN OK'
|
||||
];
|
||||
} else if ($status < 0) {
|
||||
throw new ApiException('Payment Timed Out or Error');
|
||||
} else {
|
||||
return 'IPN OK: pending';
|
||||
}
|
||||
}
|
||||
}
|
||||
8
plugins/CoinPayments/config.json
Normal file
8
plugins/CoinPayments/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "CoinPayments",
|
||||
"code": "coin_payments",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "CoinPayments payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
138
plugins/Coinbase/Plugin.php
Normal file
138
plugins/Coinbase/Plugin.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Coinbase;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['Coinbase'] = [
|
||||
'name' => $this->getConfig('display_name', 'Coinbase'),
|
||||
'icon' => $this->getConfig('icon', '🪙'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'coinbase_url' => [
|
||||
'label' => '接口地址',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Coinbase Commerce API地址'
|
||||
],
|
||||
'coinbase_api_key' => [
|
||||
'label' => 'API KEY',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Coinbase Commerce API密钥'
|
||||
],
|
||||
'coinbase_webhook_key' => [
|
||||
'label' => 'WEBHOOK KEY',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Webhook签名验证密钥'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'name' => '订阅套餐',
|
||||
'description' => '订单号 ' . $order['trade_no'],
|
||||
'pricing_type' => 'fixed_price',
|
||||
'local_price' => [
|
||||
'amount' => sprintf('%.2f', $order['total_amount'] / 100),
|
||||
'currency' => 'CNY'
|
||||
],
|
||||
'metadata' => [
|
||||
"outTradeNo" => $order['trade_no'],
|
||||
],
|
||||
];
|
||||
|
||||
$params_string = http_build_query($params);
|
||||
$ret_raw = $this->curlPost($this->getConfig('coinbase_url'), $params_string);
|
||||
$ret = @json_decode($ret_raw, true);
|
||||
|
||||
if (empty($ret['data']['hosted_url'])) {
|
||||
throw new ApiException("error!");
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => $ret['data']['hosted_url'],
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array
|
||||
{
|
||||
$payload = trim(request()->getContent());
|
||||
$json_param = json_decode($payload, true);
|
||||
|
||||
$headerName = 'X-Cc-Webhook-Signature';
|
||||
$headers = getallheaders();
|
||||
$signatureHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
$computedSignature = \hash_hmac('sha256', $payload, $this->getConfig('coinbase_webhook_key'));
|
||||
|
||||
if (!$this->hashEqual($signatureHeader, $computedSignature)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
}
|
||||
|
||||
$out_trade_no = $json_param['event']['data']['metadata']['outTradeNo'];
|
||||
$pay_trade_no = $json_param['event']['id'];
|
||||
|
||||
return [
|
||||
'trade_no' => $out_trade_no,
|
||||
'callback_no' => $pay_trade_no
|
||||
];
|
||||
}
|
||||
|
||||
private function curlPost($url, $params = false)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||||
curl_setopt(
|
||||
$ch,
|
||||
CURLOPT_HTTPHEADER,
|
||||
array('X-CC-Api-Key:' . $this->getConfig('coinbase_api_key'), 'X-CC-Version: 2018-03-22')
|
||||
);
|
||||
$result = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function hashEqual($str1, $str2)
|
||||
{
|
||||
if (function_exists('hash_equals')) {
|
||||
return \hash_equals($str1, $str2);
|
||||
}
|
||||
|
||||
if (strlen($str1) != strlen($str2)) {
|
||||
return false;
|
||||
} else {
|
||||
$res = $str1 ^ $str2;
|
||||
$ret = 0;
|
||||
|
||||
for ($i = strlen($res) - 1; $i >= 0; $i--) {
|
||||
$ret |= ord($res[$i]);
|
||||
}
|
||||
return !$ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
plugins/Coinbase/config.json
Normal file
8
plugins/Coinbase/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Coinbase",
|
||||
"code": "coinbase",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "Coinbase payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
100
plugins/Epay/Plugin.php
Normal file
100
plugins/Epay/Plugin.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Epay;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function ($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['EPay'] = [
|
||||
'name' => $this->getConfig('display_name', '易支付'),
|
||||
'icon' => $this->getConfig('icon', '💳'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'url' => [
|
||||
'label' => '支付网关地址',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '请填写完整的支付网关地址,包括协议(http或https)'
|
||||
],
|
||||
'pid' => [
|
||||
'label' => '商户ID',
|
||||
'type' => 'string',
|
||||
'description' => '请填写商户ID',
|
||||
'required' => true
|
||||
],
|
||||
'key' => [
|
||||
'label' => '通信密钥',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '请填写通信密钥'
|
||||
],
|
||||
'type' => [
|
||||
'label' => '支付类型',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
['value' => 'alipay', 'label' => '支付宝'],
|
||||
['value' => 'wxpay', 'label' => '微信支付'],
|
||||
['value' => 'qqpay', 'label' => 'QQ钱包']
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'money' => $order['total_amount'] / 100,
|
||||
'name' => $order['trade_no'],
|
||||
'notify_url' => $order['notify_url'],
|
||||
'return_url' => $order['return_url'],
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'pid' => $this->getConfig('pid')
|
||||
];
|
||||
|
||||
if ($paymentType = $this->getConfig('type')) {
|
||||
$params['type'] = $paymentType;
|
||||
}
|
||||
|
||||
ksort($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->getConfig('key');
|
||||
$params['sign'] = md5($str);
|
||||
$params['sign_type'] = 'MD5';
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => $this->getConfig('url') . '/submit.php?' . http_build_query($params)
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$sign = $params['sign'];
|
||||
unset($params['sign'], $params['sign_type']);
|
||||
ksort($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->getConfig('key');
|
||||
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
}
|
||||
8
plugins/Epay/config.json
Normal file
8
plugins/Epay/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "EPay",
|
||||
"code": "epay",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "EPay payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
124
plugins/Mgate/Plugin.php
Normal file
124
plugins/Mgate/Plugin.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Mgate;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
use Curl\Curl;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function ($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['MGate'] = [
|
||||
'name' => $this->getConfig('display_name', 'MGate'),
|
||||
'icon' => $this->getConfig('icon', '🏛️'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'mgate_url' => [
|
||||
'label' => 'API地址',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'MGate支付网关API地址'
|
||||
],
|
||||
'mgate_app_id' => [
|
||||
'label' => 'APP ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'MGate应用标识符'
|
||||
],
|
||||
'mgate_app_secret' => [
|
||||
'label' => 'App Secret',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'MGate应用密钥'
|
||||
],
|
||||
'mgate_source_currency' => [
|
||||
'label' => '源货币',
|
||||
'type' => 'string',
|
||||
'description' => '默认CNY,源货币类型'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'],
|
||||
'notify_url' => $order['notify_url'],
|
||||
'return_url' => $order['return_url']
|
||||
];
|
||||
|
||||
if ($this->getConfig('mgate_source_currency')) {
|
||||
$params['source_currency'] = $this->getConfig('mgate_source_currency');
|
||||
}
|
||||
|
||||
$params['app_id'] = $this->getConfig('mgate_app_id');
|
||||
ksort($params);
|
||||
$str = http_build_query($params) . $this->getConfig('mgate_app_secret');
|
||||
$params['sign'] = md5($str);
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent('MGate');
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, 0);
|
||||
$curl->post($this->getConfig('mgate_url') . '/v1/gateway/fetch', http_build_query($params));
|
||||
$result = $curl->response;
|
||||
|
||||
if (!$result) {
|
||||
throw new ApiException('网络异常');
|
||||
}
|
||||
|
||||
if ($curl->error) {
|
||||
if (isset($result->errors)) {
|
||||
$errors = (array) $result->errors;
|
||||
throw new ApiException($errors[array_keys($errors)[0]][0]);
|
||||
}
|
||||
if (isset($result->message)) {
|
||||
throw new ApiException($result->message);
|
||||
}
|
||||
throw new ApiException('未知错误');
|
||||
}
|
||||
|
||||
$curl->close();
|
||||
|
||||
if (!isset($result->data->trade_no)) {
|
||||
throw new ApiException('接口请求失败');
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => $result->data->pay_url
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$sign = $params['sign'];
|
||||
unset($params['sign']);
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = http_build_query($params) . $this->getConfig('mgate_app_secret');
|
||||
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
}
|
||||
8
plugins/Mgate/config.json
Normal file
8
plugins/Mgate/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "MGate",
|
||||
"code": "mgate",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "MGate payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
128
plugins/Smogate/Plugin.php
Normal file
128
plugins/Smogate/Plugin.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Smogate;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use Curl\Curl;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['Smogate'] = [
|
||||
'name' => $this->getConfig('display_name', 'Smogate'),
|
||||
'icon' => $this->getConfig('icon', '🔥'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'smogate_app_id' => [
|
||||
'label' => 'APP ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Smogate -> 接入文档和密钥 -> 查看APPID和密钥'
|
||||
],
|
||||
'smogate_app_secret' => [
|
||||
'label' => 'APP Secret',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Smogate -> 接入文档和密钥 -> 查看APPID和密钥'
|
||||
],
|
||||
'smogate_source_currency' => [
|
||||
'label' => '源货币',
|
||||
'type' => 'string',
|
||||
'description' => '默认CNY,源货币类型'
|
||||
],
|
||||
'smogate_method' => [
|
||||
'label' => '支付方式',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Smogate支付方式标识'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'],
|
||||
'notify_url' => $order['notify_url'],
|
||||
'method' => $this->getConfig('smogate_method')
|
||||
];
|
||||
|
||||
if ($this->getConfig('smogate_source_currency')) {
|
||||
$params['source_currency'] = strtolower($this->getConfig('smogate_source_currency'));
|
||||
}
|
||||
|
||||
$params['app_id'] = $this->getConfig('smogate_app_id');
|
||||
ksort($params);
|
||||
$str = http_build_query($params) . $this->getConfig('smogate_app_secret');
|
||||
$params['sign'] = md5($str);
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent("Smogate {$this->getConfig('smogate_app_id')}");
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, 0);
|
||||
$curl->post("https://{$this->getConfig('smogate_app_id')}.vless.org/v1/gateway/pay", http_build_query($params));
|
||||
$result = $curl->response;
|
||||
|
||||
if (!$result) {
|
||||
abort(500, '网络异常');
|
||||
}
|
||||
|
||||
if ($curl->error) {
|
||||
if (isset($result->errors)) {
|
||||
$errors = (array)$result->errors;
|
||||
abort(500, $errors[array_keys($errors)[0]][0]);
|
||||
}
|
||||
if (isset($result->message)) {
|
||||
abort(500, $result->message);
|
||||
}
|
||||
abort(500, '未知错误');
|
||||
}
|
||||
|
||||
$curl->close();
|
||||
|
||||
if (!isset($result->data)) {
|
||||
abort(500, '请求失败');
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $this->isMobile() ? 1 : 0,
|
||||
'data' => $result->data
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$sign = $params['sign'];
|
||||
unset($params['sign']);
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = http_build_query($params) . $this->getConfig('smogate_app_secret');
|
||||
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
|
||||
private function isMobile(): bool
|
||||
{
|
||||
return strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'mobile') !== false;
|
||||
}
|
||||
}
|
||||
8
plugins/Smogate/config.json
Normal file
8
plugins/Smogate/config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Smogate",
|
||||
"code": "smogate",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "Smogate payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
425
plugins/Telegram/Plugin.php
Normal file
425
plugins/Telegram/Plugin.php
Normal file
@@ -0,0 +1,425 @@
|
||||
<?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,
|
||||
$payment->payment,
|
||||
$payment->name,
|
||||
$order->trade_no
|
||||
);
|
||||
$this->telegramService->sendMessageWithAdmin($message, true);
|
||||
}
|
||||
|
||||
public function sendTicketNotify(array $data): void
|
||||
{
|
||||
if (!$this->getConfig('enable_ticket_notify', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$ticket, $message] = $data;
|
||||
$user = User::find($ticket->user_id);
|
||||
if (!$user)
|
||||
return;
|
||||
$user->load('plan');
|
||||
$transfer_enable = Helper::transferToGB($user->transfer_enable);
|
||||
$remaining_traffic = Helper::transferToGB($user->transfer_enable - $user->u - $user->d);
|
||||
$u = Helper::transferToGB($user->u);
|
||||
$d = Helper::transferToGB($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———————————————\n";
|
||||
$TGmessage .= "邮箱: `{$user->email}`\n";
|
||||
$TGmessage .= "用户位置: \n`{$region}`\n";
|
||||
if ($plan) {
|
||||
$TGmessage .= "套餐与流量: \n`{$plan->name} {$transfer_enable}/{$remaining_traffic}`\n";
|
||||
$TGmessage .= "上传/下载: \n`{$u}/{$d}`\n";
|
||||
$TGmessage .= "到期时间: \n`{$expired_at}`\n";
|
||||
} else {
|
||||
$TGmessage .= "套餐与流量: \n`未订购任何套餐`\n";
|
||||
}
|
||||
$TGmessage .= "余额/佣金余额: \n`{$money}/{$affmoney}`\n";
|
||||
$TGmessage .= "主题:\n`{$ticket->subject}`\n内容:\n`{$message->message}`\n";
|
||||
$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已用流量:%s\n总流量:%s\n剩余流量:%s\n使用率:%.2f%%",
|
||||
Helper::transferToGB($transferUsed),
|
||||
Helper::transferToGB($transferTotal),
|
||||
Helper::transferToGB($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;
|
||||
}
|
||||
|
||||
}
|
||||
84
plugins/Telegram/README.md
Normal file
84
plugins/Telegram/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Telegram 插件
|
||||
|
||||
XBoard 的 Telegram Bot 插件,提供用户账号绑定、流量查询、订阅链接获取等功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 工单通知功能(可配置开关)
|
||||
- ✅ 支付通知功能(可配置开关)
|
||||
- ✅ 用户账号绑定/解绑
|
||||
- ✅ 流量使用情况查询
|
||||
- ✅ 订阅链接获取
|
||||
- ✅ 工单回复支持
|
||||
|
||||
## 可用命令
|
||||
|
||||
### `/start` - 开始使用
|
||||
|
||||
欢迎新用户并显示帮助信息,支持动态配置。
|
||||
|
||||
### `/bind` - 绑定账号
|
||||
|
||||
绑定用户的 XBoard 账号到 Telegram。
|
||||
|
||||
```
|
||||
/bind [订阅链接]
|
||||
```
|
||||
|
||||
### `/traffic` - 查看流量
|
||||
|
||||
查看当前绑定账号的流量使用情况。
|
||||
|
||||
### `/getlatesturl` - 获取订阅链接
|
||||
|
||||
获取最新的订阅链接。
|
||||
|
||||
### `/unbind` - 解绑账号
|
||||
|
||||
解绑当前 Telegram 账号与 XBoard 账号的关联。
|
||||
|
||||
## 配置选项
|
||||
|
||||
### 基础配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------------------------ | -------------------- |
|
||||
| `auto_reply` | boolean | true | 是否自动回复未知命令 |
|
||||
| `help_text` | text | '请使用以下命令:\\n/bind - 绑定账号\\n/traffic - 查看流量\\n/getlatesturl - 获取最新链接' | 未知命令的回复文本 |
|
||||
|
||||
### `/start` 命令动态配置
|
||||
|
||||
| 配置项 | 类型 | 说明 |
|
||||
| ----------------------- | ---- | ------------------------ |
|
||||
| `start_welcome_title` | text | 欢迎标题 |
|
||||
| `start_bot_description` | text | 机器人功能介绍 |
|
||||
| `start_bind_guide` | text | 未绑定用户的绑定指导 |
|
||||
| `start_unbind_guide` | text | 已绑定用户显示的命令列表 |
|
||||
| `start_bind_commands` | text | 未绑定用户显示的命令列表 |
|
||||
| `start_footer` | text | 底部提示信息 |
|
||||
|
||||
### 工单通知配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ---------------------- | ------- | ------ | -------------------- |
|
||||
| `enable_ticket_notify` | boolean | true | 是否开启工单通知功能 |
|
||||
|
||||
### 支付通知配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ----------------------- | ------- | ------ | -------------------- |
|
||||
| `enable_payment_notify` | boolean | true | 是否开启支付通知功能 |
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 新用户使用流程
|
||||
|
||||
1. 用户首次使用 Bot,发送 `/start`
|
||||
2. 根据提示绑定账号:`/bind [订阅链接]`
|
||||
3. 绑定成功后即可使用其他功能
|
||||
|
||||
### 日常使用流程
|
||||
|
||||
1. 查看流量:`/traffic`
|
||||
2. 获取订阅链接:`/getlatesturl`
|
||||
3. 管理绑定:`/unbind`
|
||||
66
plugins/Telegram/config.json
Normal file
66
plugins/Telegram/config.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "Telegram Bot 集成",
|
||||
"code": "telegram",
|
||||
"version": "1.0.0",
|
||||
"description": "Telegram Bot 消息处理和命令系统",
|
||||
"author": "XBoard Team",
|
||||
"require": {
|
||||
"xboard": ">=1.0.0"
|
||||
},
|
||||
"config": {
|
||||
"enable_ticket_notify": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "开启工单通知",
|
||||
"description": "是否开启工单创建和回复的 Telegram 通知功能"
|
||||
},
|
||||
"enable_payment_notify": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "开启支付通知",
|
||||
"description": "是否开启支付成功的 Telegram 通知功能"
|
||||
},
|
||||
"start_welcome_title": {
|
||||
"type": "string",
|
||||
"default": "🎉 欢迎使用 XBoard Telegram Bot!",
|
||||
"label": "欢迎标题",
|
||||
"description": "/start 命令显示的欢迎标题"
|
||||
},
|
||||
"start_bot_description": {
|
||||
"type": "text",
|
||||
"default": "🤖 我是您的专属助手,可以帮助您:\\n• 绑定您的 XBoard 账号\\n• 查看流量使用情况\\n• 获取最新订阅链接\\n• 管理账号绑定状态",
|
||||
"label": "机器人描述",
|
||||
"description": "/start 命令显示的机器人功能介绍"
|
||||
},
|
||||
"start_bind_guide": {
|
||||
"type": "text",
|
||||
"default": "🔗 请先绑定您的 XBoard 账号:\\n1. 登录您的 XBoard 账户\\n2. 复制您的订阅链接\\n3. 发送 /bind + 订阅链接",
|
||||
"label": "绑定指导",
|
||||
"description": "未绑定用户显示的绑定指导文本"
|
||||
},
|
||||
"start_unbind_guide": {
|
||||
"type": "text",
|
||||
"default": "📋 可用命令:\\n/traffic - 查看流量使用情况\\n/getlatesturl - 获取订阅链接\\n/unbind - 解绑账号",
|
||||
"label": "已绑定用户命令列表",
|
||||
"description": "已绑定用户显示的命令列表"
|
||||
},
|
||||
"start_bind_commands": {
|
||||
"type": "text",
|
||||
"default": "📋 可用命令:\\n/bind [订阅链接] - 绑定账号",
|
||||
"label": "未绑定用户命令列表",
|
||||
"description": "未绑定用户显示的命令列表"
|
||||
},
|
||||
"start_footer": {
|
||||
"type": "text",
|
||||
"default": "💡 提示:所有命令都需要在私聊中使用",
|
||||
"label": "底部提示",
|
||||
"description": "/start 命令底部的提示信息"
|
||||
},
|
||||
"help_text": {
|
||||
"type": "text",
|
||||
"default": "请使用以下命令:\\n/bind - 绑定账号\\n/traffic - 查看流量\\n/getlatesturl - 获取最新链接",
|
||||
"label": "帮助文本",
|
||||
"description": "未知命令时显示的帮助文本"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user