Merge branch 'cedar2025:dev' into dev
This commit is contained in:
@@ -26,9 +26,10 @@ class BackupDatabase extends Command
|
||||
}
|
||||
|
||||
// 数据库备份逻辑
|
||||
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
|
||||
$compressedBackupPath = $databaseBackupPath . '.gz';
|
||||
try{
|
||||
if (config('database.default') === 'mysql'){
|
||||
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
|
||||
$this->info("1️⃣:开始备份Mysql");
|
||||
\Spatie\DbDumper\Databases\MySql::create()
|
||||
->setHost(config('database.connections.mysql.host'))
|
||||
@@ -83,7 +84,7 @@ class BackupDatabase extends Command
|
||||
$bucket->upload(fopen($compressedBackupPath, 'r'), [
|
||||
'name' => $objectName,
|
||||
]);
|
||||
|
||||
|
||||
// 输出文件链接
|
||||
\Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
$this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
|
||||
|
||||
@@ -51,15 +51,16 @@ class MigrateFromV2b extends Command
|
||||
],
|
||||
'1.7.3' => [
|
||||
'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;',
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_amount order_total INT COMMENT '订单合计';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_amount paid_total INT COMMENT '订单合计';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
|
||||
"ALTER TABLE `v2_stat`
|
||||
ADD COLUMN paid_count INT NULL,
|
||||
ADD COLUMN paid_total INT NULL,
|
||||
ADD COLUMN order_count INT NULL,
|
||||
ADD COLUMN order_total INT NULL,
|
||||
ADD COLUMN register_count INT NULL,
|
||||
ADD COLUMN invite_count INT NULL,
|
||||
ADD COLUMN transfer_used_total VARCHAR(32) NULL;
|
||||
",
|
||||
",
|
||||
"CREATE TABLE `v2_log` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`title` TEXT NOT NULL,
|
||||
|
||||
@@ -46,6 +46,9 @@ class XboardInstall extends Command
|
||||
{
|
||||
try {
|
||||
$isDocker = env('docker', false);
|
||||
$enableSqlite = env('enable_sqlite', false);
|
||||
$enableRedis = env('enable_redis', false);
|
||||
$adminAccount = env('admin_account', '');
|
||||
$this->info("__ __ ____ _ ");
|
||||
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
|
||||
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
|
||||
@@ -67,7 +70,7 @@ class XboardInstall extends Command
|
||||
return;
|
||||
}
|
||||
// 选择是否使用Sqlite
|
||||
if (confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) {
|
||||
if ($enableSqlite || confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) {
|
||||
$sqliteFile = '.docker/.data/database.sqlite';
|
||||
if (!file_exists(base_path($sqliteFile))) {
|
||||
// 创建空文件
|
||||
@@ -142,7 +145,7 @@ class XboardInstall extends Command
|
||||
$isReidsValid = false;
|
||||
while (!$isReidsValid) {
|
||||
// 判断是否为Docker环境
|
||||
if ($isDocker == 'true' && (confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
|
||||
if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
|
||||
$envConfig['REDIS_HOST'] = '/run/redis-socket/redis.sock';
|
||||
$envConfig['REDIS_PORT'] = 0;
|
||||
$envConfig['REDIS_PASSWORD'] = null;
|
||||
@@ -175,7 +178,7 @@ class XboardInstall extends Command
|
||||
abort(500, '复制环境文件失败,请检查目录权限');
|
||||
}
|
||||
;
|
||||
$email = text(
|
||||
$email = !empty($adminAccount) ? $adminAccount : text(
|
||||
label: '请输入管理员账号',
|
||||
default: 'admin@demo.com',
|
||||
required: true,
|
||||
|
||||
@@ -73,7 +73,7 @@ class UserController extends Controller
|
||||
$res[$i]['plan_name'] = $plan[$k]['name'];
|
||||
}
|
||||
}
|
||||
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']);
|
||||
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl( $res[$i]['token']);
|
||||
}
|
||||
return response([
|
||||
'data' => $res,
|
||||
@@ -162,7 +162,7 @@ class UserController extends Controller
|
||||
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
|
||||
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
|
||||
$planName = $user['plan_name'] ?? '无订阅';
|
||||
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
|
||||
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
|
||||
}
|
||||
echo "\xEF\xBB\xBF" . $data;
|
||||
@@ -240,7 +240,7 @@ class UserController extends Controller
|
||||
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
|
||||
$createDate = date('Y-m-d H:i:s', $user['created_at']);
|
||||
$password = $request->input('password') ?? $user['email'];
|
||||
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
|
||||
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
|
||||
}
|
||||
echo $data;
|
||||
|
||||
@@ -24,6 +24,7 @@ class ClientController extends Controller
|
||||
'ClashX Meta' => '1.3.5',
|
||||
'Hiddify' => '0.1.0',
|
||||
'loon' => '637',
|
||||
'v2rayng' => '1.9.5',
|
||||
'v2rayN' => '6.31',
|
||||
'surge' => '2398'
|
||||
];
|
||||
|
||||
@@ -57,7 +57,7 @@ class PaymentController extends Controller
|
||||
$payment->name,
|
||||
$order->trade_no
|
||||
);
|
||||
|
||||
|
||||
$telegramService->sendMessageWithAdmin($message);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class KnowledgeController extends Controller
|
||||
if (!$userService->isAvailable($user)) {
|
||||
$this->formatAccessData($knowledge['body']);
|
||||
}
|
||||
$subscribeUrl = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
|
||||
$knowledge['body'] = str_replace('{{siteName}}', admin_setting('app_name', 'XBoard'), $knowledge['body']);
|
||||
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
|
||||
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
|
||||
|
||||
@@ -13,7 +13,7 @@ class StatController extends Controller
|
||||
{
|
||||
public function getTrafficLog(Request $request)
|
||||
{
|
||||
$startDate = now()->startOfMonth();
|
||||
$startDate = now()->startOfMonth()->timestamp;
|
||||
$records = StatUser::query()
|
||||
->where('user_id', $request->user['id'])
|
||||
->where('record_at', '>=', $startDate)
|
||||
|
||||
@@ -140,7 +140,7 @@ class UserController extends Controller
|
||||
return $this->fail([400, __('Subscription plan does not exist')]);
|
||||
}
|
||||
}
|
||||
$user['subscribe_url'] = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
|
||||
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
|
||||
$userService = new UserService();
|
||||
$user['reset_day'] = $userService->getResetDay($user);
|
||||
return $this->success($user);
|
||||
@@ -157,7 +157,7 @@ class UserController extends Controller
|
||||
if (!$user->save()) {
|
||||
return $this->fail([400, __('Reset failed')]);
|
||||
}
|
||||
return $this->success(Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user->token));
|
||||
return $this->success(Helper::getSubscribeUrl($user->token));
|
||||
}
|
||||
|
||||
public function update(UserUpdate $request)
|
||||
|
||||
@@ -31,6 +31,7 @@ class Server
|
||||
$request->validate([
|
||||
'token' => [
|
||||
"string",
|
||||
"required",
|
||||
function ($attribute, $value, $fail) {
|
||||
if ($value !== admin_setting('server_token')) {
|
||||
$fail('The ' . $attribute . ' is invalid.');
|
||||
@@ -39,10 +40,11 @@ class Server
|
||||
],
|
||||
'node_id' => 'required',
|
||||
'node_type' => [
|
||||
'required',
|
||||
'nullable',
|
||||
'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/',
|
||||
function ($attribute, $value, $fail) use ($aliasTypes, $request) {
|
||||
$request->merge([$attribute => strtolower(isset ($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]);
|
||||
$request->merge([$attribute => strtolower(isset($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]);
|
||||
},
|
||||
]
|
||||
], [
|
||||
|
||||
@@ -12,7 +12,7 @@ class ClientRoute
|
||||
'middleware' => 'client'
|
||||
], function ($router) {
|
||||
// Client
|
||||
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe');
|
||||
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe');
|
||||
// App
|
||||
$router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig');
|
||||
$router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -17,7 +18,7 @@ class BatchTrafficFetchJob implements ShouldQueue
|
||||
protected $protocol;
|
||||
protected $timestamp;
|
||||
public $tries = 1;
|
||||
public $timeout = 10;
|
||||
public $timeout = 20;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
@@ -36,34 +37,16 @@ class BatchTrafficFetchJob implements ShouldQueue
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// 获取子节点
|
||||
$targetServer = $this->childServer ?? $this->server;
|
||||
foreach ($this->data as $uid => $v) {
|
||||
$u = $v[0];
|
||||
$d = $v[1];
|
||||
$result = \DB::transaction(function () use ($uid, $u, $d, $targetServer) {
|
||||
$user = \DB::table('v2_user')->lockForUpdate()->where('id', $uid)->first();
|
||||
if (!$user) {
|
||||
return true;
|
||||
}
|
||||
$newTime = time();
|
||||
$newU = $user->u + ($u * $targetServer['rate']);
|
||||
$newD = $user->d + ($d * $targetServer['rate']);
|
||||
$rows = \DB::table('v2_user')
|
||||
->where('id', $uid)
|
||||
->update([
|
||||
't' => $newTime,
|
||||
'u' => $newU,
|
||||
'd' => $newD,
|
||||
]);
|
||||
if ($rows === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, 3);
|
||||
if (!$result) {
|
||||
TrafficFetchJob::dispatch($u, $d, $uid, $targetServer, $this->protocol);
|
||||
}
|
||||
User::where('id', $uid)
|
||||
->incrementEach(
|
||||
[
|
||||
'u' => $v[0] * $targetServer['rate'],
|
||||
'd' => $v[1] * $targetServer['rate'],
|
||||
],
|
||||
['t' => time()]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +76,13 @@ class BTCPay
|
||||
//NOT BTCPay-Sig
|
||||
//API doc is WRONG!
|
||||
$headerName = 'Btcpay-Sig';
|
||||
$signraturHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
$signatureHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
$json_param = json_decode($payload, true);
|
||||
|
||||
$computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->config['btcpay_webhook_key']);
|
||||
|
||||
if (!self::hashEqual($signraturHeader, $computedSignature)) {
|
||||
if (!self::hashEqual($signatureHeader, $computedSignature)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
return false;
|
||||
}
|
||||
|
||||
//get order id store in metadata
|
||||
|
||||
@@ -27,7 +27,12 @@ class EPay
|
||||
'label' => 'KEY',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
]
|
||||
],
|
||||
'type' => [
|
||||
'label' => 'TYPE',
|
||||
'description' => 'alipay / qqpay / wxpay',
|
||||
'type' => 'input',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -41,6 +46,9 @@ class EPay
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'pid' => $this->config['pid']
|
||||
];
|
||||
if(optional($this->config)['type']){
|
||||
$params['type'] = $this->config['type'];
|
||||
}
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 自己写别抄,抄NMB抄
|
||||
*/
|
||||
namespace App\Payments;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class StripeALLInOne {
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function form()
|
||||
{
|
||||
return [
|
||||
'currency' => [
|
||||
'label' => '货币单位',
|
||||
'description' => '请使用符合ISO 4217标准的三位字母,例如GBP',
|
||||
'type' => 'input',
|
||||
],
|
||||
'stripe_sk_live' => [
|
||||
'label' => 'SK_LIVE',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'stripe_webhook_key' => [
|
||||
'label' => 'WebHook密钥签名',
|
||||
'description' => 'whsec_....',
|
||||
'type' => 'input',
|
||||
],
|
||||
'description' => [
|
||||
'label' => '自定义商品介绍',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'payment_method' => [
|
||||
'label' => '支付方式',
|
||||
'description' => '请输入alipay, wechat_pay, cards',
|
||||
'type' => 'input',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order)
|
||||
{
|
||||
$currency = $this->config['currency'];
|
||||
$exchange = $this->exchange('CNY', strtoupper($currency));
|
||||
if (!$exchange) {
|
||||
throw new ApiException('Currency conversion has timed out, please try again later', 500);
|
||||
}
|
||||
//jump url
|
||||
$jumpUrl = null;
|
||||
$actionType = 0;
|
||||
$stripe = new \Stripe\StripeClient($this->config['stripe_sk_live']);
|
||||
|
||||
if ($this->config['payment_method'] != "cards"){
|
||||
$stripePaymentMethod = $stripe->paymentMethods->create([
|
||||
'type' => $this->config['payment_method'],
|
||||
]);
|
||||
// 准备支付意图的基础参数
|
||||
$params = [
|
||||
'amount' => floor($order['total_amount'] * $exchange),
|
||||
'currency' => $currency,
|
||||
'confirm' => true,
|
||||
'payment_method' => $stripePaymentMethod->id,
|
||||
'automatic_payment_methods' => ['enabled' => true],
|
||||
'statement_descriptor_suffix' => 'sub-' . $order['user_id'] . '-' . substr($order['trade_no'], -8),
|
||||
'description' => $this->config['description'],
|
||||
'metadata' => [
|
||||
'user_id' => $order['user_id'],
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'identifier' => ''
|
||||
],
|
||||
'return_url' => $order['return_url']
|
||||
];
|
||||
|
||||
// 如果支付方式为 wechat_pay,添加相应的支付方式选项
|
||||
if ($this->config['payment_method'] === 'wechat_pay') {
|
||||
$params['payment_method_options'] = [
|
||||
'wechat_pay' => [
|
||||
'client' => 'web'
|
||||
],
|
||||
];
|
||||
}
|
||||
//更新支持最新的paymentIntents方法,Sources API将在今年被彻底替
|
||||
$stripeIntents = $stripe->paymentIntents->create($params);
|
||||
|
||||
$nextAction = null;
|
||||
|
||||
if (!$stripeIntents['next_action']) {
|
||||
throw new ApiException(__('Payment gateway request failed'));
|
||||
}else {
|
||||
$nextAction = $stripeIntents['next_action'];
|
||||
}
|
||||
|
||||
switch ($this->config['payment_method']){
|
||||
case "alipay":
|
||||
if (isset($nextAction['alipay_handle_redirect'])){
|
||||
$jumpUrl = $nextAction['alipay_handle_redirect']['url'];
|
||||
$actionType = 1;
|
||||
}else {
|
||||
throw new ApiException('unable get Alipay redirect url', 500);
|
||||
}
|
||||
break;
|
||||
case "wechat_pay":
|
||||
if (isset($nextAction['wechat_pay_display_qr_code'])){
|
||||
$jumpUrl = $nextAction['wechat_pay_display_qr_code']['data'];
|
||||
}else {
|
||||
throw new ApiException('unable get WeChat Pay redirect url', 500);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$creditCheckOut = $stripe->checkout->sessions->create([
|
||||
'success_url' => $order['return_url'],
|
||||
'client_reference_id' => $order['trade_no'],
|
||||
'payment_method_types' => ['card'],
|
||||
'line_items' => [
|
||||
[
|
||||
'price_data' => [
|
||||
'currency' => $currency,
|
||||
'unit_amount' => floor($order['total_amount'] * $exchange),
|
||||
'product_data' => [
|
||||
'name' => 'sub-' . $order['user_id'] . '-' . substr($order['trade_no'], -8),
|
||||
'description' => $this->config['description'],
|
||||
]
|
||||
],
|
||||
'quantity' => 1,
|
||||
],
|
||||
],
|
||||
'mode' => 'payment',
|
||||
]);
|
||||
$jumpUrl = $creditCheckOut['url'];
|
||||
$actionType = 1;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $actionType,
|
||||
'data' => $jumpUrl
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params)
|
||||
{
|
||||
try {
|
||||
\Stripe\Stripe::setApiKey($this->config['stripe_sk_live']);
|
||||
//Workerman不支持使用php://input, stripe同时要求验证签名的payload不能经过修改,所以使用这个方法
|
||||
$payload = $GLOBALS['HTTP_RAW_POST_DATA'];
|
||||
$headers = getallheaders();
|
||||
$headerName = 'Stripe-Signature';
|
||||
$signatureHeader = $headers[$headerName] ?? '';
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$payload,
|
||||
$signatureHeader,
|
||||
$this->config['stripe_webhook_key']
|
||||
);
|
||||
|
||||
} catch (\UnexpectedValueException $e){
|
||||
throw new ApiException('Error parsing payload', 400);
|
||||
}
|
||||
catch (\Stripe\Exception\SignatureVerificationException $e) {
|
||||
throw new ApiException('signature not match', 400);
|
||||
}
|
||||
switch ($event->type) {
|
||||
case 'payment_intent.succeeded':
|
||||
$object = $event->data->object;
|
||||
if ($object->status === 'succeeded') {
|
||||
if (!isset($object->metadata->out_trade_no)) {
|
||||
return('order error');
|
||||
}
|
||||
$metaData = $object->metadata;
|
||||
$tradeNo = $metaData->out_trade_no;
|
||||
return [
|
||||
'trade_no' => $tradeNo,
|
||||
'callback_no' => $object->id
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'checkout.session.completed':
|
||||
$object = $event->data->object;
|
||||
if ($object->payment_status === 'paid') {
|
||||
return [
|
||||
'trade_no' => $object->client_reference_id,
|
||||
'callback_no' => $object->payment_intent
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'checkout.session.async_payment_succeeded':
|
||||
$object = $event->data->object;
|
||||
return [
|
||||
'trade_no' => $object->client_reference_id,
|
||||
'callback_no' => $object->payment_intent
|
||||
];
|
||||
break;
|
||||
default:
|
||||
throw new ApiException('event is not support');
|
||||
}
|
||||
return('success');
|
||||
}
|
||||
|
||||
private function exchange($from, $to)
|
||||
{
|
||||
$from = strtolower($from);
|
||||
$to = strtolower($to);
|
||||
$result = file_get_contents("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/" . $from . ".min.json");
|
||||
$result = json_decode($result, true);
|
||||
return $result[$from][$to];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use App\Plugins\Telegram\Telegram;
|
||||
|
||||
class GetLatestUrl extends Telegram {
|
||||
public $command = '/getlatesturl';
|
||||
public $description = '将Telegram账号绑定到网站';
|
||||
public $description = '获取网站最新网址';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
$telegramService = $this->telegramService;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram\Commands;
|
||||
|
||||
use App\Plugins\Telegram\Telegram;
|
||||
|
||||
class Start extends Telegram {
|
||||
public $command = '/start';
|
||||
public $description = 'telegram机器人初始化';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
if (!$message->is_private) return;
|
||||
$telegramService = $this->telegramService;
|
||||
$text = "/start 显示所有可用指令\n /bind+空格+订阅链接,将telegram绑定至账户\n /traffic 获取当前使用流量 \n /getlatesturl 获取网站最新网址 \n /unbind 解绑telegram账户";
|
||||
$telegramService->sendMessage($message->chat_id, $text, 'markdown');
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,17 @@ class UnBind extends Telegram {
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
if (!$message->is_private) return;
|
||||
$user = User::where('telegram_id', $message->chat_id)->first();
|
||||
$telegramService = $this->telegramService;
|
||||
if (!$user) {
|
||||
$telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
|
||||
return;
|
||||
if (!isset($message->args[0])) {
|
||||
$user = User::where('telegram_id', $message->chat_id)->first();
|
||||
} else {
|
||||
$chat = User::where('telegram_id', $message->chat_id)->first();
|
||||
if (!$chat) return;
|
||||
if (!($chat->is_admin || $chat->is_staff)) return;
|
||||
if (strpos($message->args[0], '@') !== true) {
|
||||
$user = User::where('email', $message->args[0])->first();
|
||||
} else {
|
||||
$user = User::where('telegram_id', $message->args[0])->first();
|
||||
}
|
||||
}
|
||||
$user->telegram_id = NULL;
|
||||
if (!$user->save()) {
|
||||
|
||||
@@ -88,7 +88,8 @@ class ClashMeta
|
||||
return response($yaml, 200)
|
||||
->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
|
||||
->header('profile-update-interval', '24')
|
||||
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
|
||||
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName))
|
||||
->header('profile-web-page-url', admin_setting('app_url'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,8 +195,6 @@ class ClashMeta
|
||||
$array['server'] = $server['host'];
|
||||
$array['port'] = $server['port'];
|
||||
$array['uuid'] = $password;
|
||||
$array['alterId'] = 0;
|
||||
$array['cipher'] = 'auto';
|
||||
$array['udp'] = true;
|
||||
|
||||
// XTLS流控算法
|
||||
@@ -207,8 +206,8 @@ class ClashMeta
|
||||
$array['tls'] = true;
|
||||
if ($server['tls_settings']) {
|
||||
$tlsSettings = $server['tls_settings'];
|
||||
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
|
||||
$array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false);
|
||||
if (isset($tlsSettings['allow_insecure']) && !empty($tlsSettings['allow_insecure']))
|
||||
$array['skip-cert-verify'] = ($tlsSettings['allow_insecure'] ? true : false);
|
||||
if (isset($tlsSettings['server_name']) && !empty($tlsSettings['server_name']))
|
||||
$array['servername'] = $tlsSettings['server_name'];
|
||||
}
|
||||
@@ -242,10 +241,8 @@ class ClashMeta
|
||||
$array['ws-opts']['path'] = $wsSettings['path'];
|
||||
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
|
||||
$array['ws-opts']['headers'] = ['Host' => $wsSettings['headers']['Host']];
|
||||
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
|
||||
$array['ws-path'] = $wsSettings['path'];
|
||||
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
|
||||
$array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']];
|
||||
$array['ws-opts']['max-early-data'] = 2560;
|
||||
$array['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol';
|
||||
}
|
||||
}
|
||||
if ($server['network'] === 'grpc') {
|
||||
|
||||
@@ -36,6 +36,9 @@ class General
|
||||
if ($item['type'] === 'trojan') {
|
||||
$uri .= self::buildTrojan($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'hysteria') {
|
||||
$uri .= self::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
}
|
||||
return base64_encode($uri);
|
||||
}
|
||||
@@ -170,4 +173,33 @@ class General
|
||||
return $uri;
|
||||
}
|
||||
|
||||
public static function buildHysteria($password, $server)
|
||||
{
|
||||
$params = [];
|
||||
// Return empty if version is not 2
|
||||
if ($server['version'] !== 2) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($server['server_name']) {
|
||||
$params['sni'] = $server['server_name'];
|
||||
$params['security'] = 'tls';
|
||||
}
|
||||
|
||||
if ($server['is_obfs']) {
|
||||
$params['obfs'] = 'salamander';
|
||||
$params['obfs-password'] = $server['server_key'];
|
||||
}
|
||||
|
||||
$params['insecure'] = $server['insecure'] ? 1 : 0;
|
||||
|
||||
$query = http_build_query($params);
|
||||
$name = rawurlencode($server['name']);
|
||||
|
||||
$uri = "hysteria2://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
|
||||
$uri .= "\r\n";
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ class Loon
|
||||
$server['host'],
|
||||
$server['port'],
|
||||
$password,
|
||||
$server['server_name'] ? "tls={$server['server_name']}" : "(null)"
|
||||
$server['server_name'] ? "sni={$server['server_name']}" : "(null)"
|
||||
];
|
||||
if ($server['insecure']) $config[] = "skip-cert-verify=true";
|
||||
$config[] = "download-bandwidth=" . ($user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps']);
|
||||
|
||||
@@ -34,6 +34,9 @@ class Passwall
|
||||
if ($item['type'] === 'trojan') {
|
||||
$uri .= self::buildTrojan($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'hysteria') {
|
||||
$uri .= General::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
}
|
||||
return base64_encode($uri);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ class SSRPlus
|
||||
if ($item['type'] === 'trojan') {
|
||||
$uri .= self::buildTrojan($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'hysteria') {
|
||||
$uri .= General::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
}
|
||||
return base64_encode($uri);
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ class Shadowrocket
|
||||
if ($server['network_settings']) {
|
||||
$wsSettings = $server['network_settings'];
|
||||
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
|
||||
$config['path'] = $wsSettings['path'];
|
||||
$config['path'] = $wsSettings['path'] . '?ed=2560';
|
||||
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
|
||||
$config['obfsParam'] = $wsSettings['headers']['Host'];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class SingBox
|
||||
|
||||
return response()
|
||||
->json($this->config)
|
||||
->header('profile-title', 'base64:'. base64_encode($appName))
|
||||
->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
|
||||
->header('profile-update-interval', '24');
|
||||
}
|
||||
@@ -148,7 +149,7 @@ class SingBox
|
||||
$array['transport']['path'] = $wsSettings['path'];
|
||||
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
|
||||
$array['transport']['headers'] = ['Host' => array($wsSettings['headers']['Host'])];
|
||||
$array['transport']['max_early_data'] = 2048;
|
||||
$array['transport']['max_early_data'] = 2560;
|
||||
$array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol';
|
||||
}
|
||||
}
|
||||
@@ -216,7 +217,7 @@ class SingBox
|
||||
$array['transport']['path'] = $wsSettings['path'];
|
||||
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
|
||||
$array['transport']['headers'] = ['Host' => array($wsSettings['headers']['Host'])];
|
||||
$array['transport']['max_early_data'] = 2048;
|
||||
$array['transport']['max_early_data'] = 2560;
|
||||
$array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol';
|
||||
}
|
||||
}
|
||||
@@ -271,7 +272,7 @@ class SingBox
|
||||
if (isset($server['network_settings']['headers']['Host'])) {
|
||||
$array['transport']['headers'] = ['Host' => array($server['network_settings']['headers']['Host'])];
|
||||
}
|
||||
$array['transport']['max_early_data'] = 2048;
|
||||
$array['transport']['max_early_data'] = 2560;
|
||||
$array['transport']['early_data_header_name'] = 'Sec-WebSocket-Protocol';
|
||||
}
|
||||
}
|
||||
@@ -309,6 +310,8 @@ class SingBox
|
||||
$array['tag'] = $server['name'];
|
||||
$array['type'] = 'hysteria2';
|
||||
$array['password'] = $password;
|
||||
$array['up_mbps'] = $user->speed_limit ? min($server['down_mbps'], $user->speed_limit) : $server['down_mbps'];
|
||||
$array['down_mbps'] = $user->speed_limit ? min($server['up_mbps'], $user->speed_limit) : $server['up_mbps'];
|
||||
|
||||
if ($server['is_obfs']) {
|
||||
$array['obfs']['type'] = 'salamander';
|
||||
@@ -318,4 +321,4 @@ class SingBox
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class Surfboard
|
||||
}
|
||||
|
||||
// Subscription link
|
||||
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
|
||||
$subsURL = Helper::getSubscribeUrl($user['token']);
|
||||
$subsDomain = request()->header('Host');
|
||||
|
||||
$config = str_replace('$subs_link', $subsURL, $config);
|
||||
|
||||
@@ -32,7 +32,9 @@ class Surge
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305'
|
||||
'chacha20-ietf-poly1305',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
])
|
||||
) {
|
||||
// [Proxy]
|
||||
@@ -69,9 +71,8 @@ class Surge
|
||||
}
|
||||
|
||||
// Subscription link
|
||||
$subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}");
|
||||
$subsDomain = request()->header('Host');
|
||||
$subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];
|
||||
$subsURL = Helper::getSubscribeUrl($user['token'], $subsDomain ? 'https://' . $subsDomain : null);
|
||||
|
||||
$config = str_replace('$subs_link', $subsURL, $config);
|
||||
$config = str_replace('$subs_domain', $subsDomain, $config);
|
||||
|
||||
@@ -37,7 +37,7 @@ class V2rayN
|
||||
$uri .= self::buildTrojan($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'hysteria') {
|
||||
$uri .= self::buildHysteria($user['uuid'], $item);
|
||||
$uri .= General::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -192,25 +192,5 @@ class V2rayN
|
||||
return $uri;
|
||||
}
|
||||
|
||||
public static function buildHysteria($password, $server)
|
||||
{
|
||||
$name = rawurlencode($server['name']);
|
||||
$params = [];
|
||||
if ($server['server_name']) $params['sni'] = $server['server_name'];
|
||||
$params['insecure'] = $server['insecure'] ? 1 : 0;
|
||||
if($server['is_obfs']) {
|
||||
$params['obfs'] = 'salamander';
|
||||
$params['obfs-password'] = $server['server_key'];
|
||||
}
|
||||
$query = http_build_query($params);
|
||||
if ($server['version'] == 2) {
|
||||
$uri = "hysteria2://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
|
||||
$uri .= "\r\n";
|
||||
} else {
|
||||
// V2rayN似乎不支持v1, 返回空
|
||||
$uri = "";
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ class V2rayNG
|
||||
if ($item['type'] === 'vless') {
|
||||
$uri .= self::buildVless($user['uuid'], $item);
|
||||
}
|
||||
if ($item['type'] === 'hysteria') {
|
||||
$uri .= General::buildHysteria($user['uuid'], $item);
|
||||
}
|
||||
}
|
||||
return base64_encode($uri);
|
||||
}
|
||||
@@ -190,5 +193,4 @@ class V2rayNG
|
||||
return $uri;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ class ServerService
|
||||
// 获取可用的用户列表
|
||||
public static function getAvailableUsers($groupId): Collection
|
||||
{
|
||||
return \DB::table('v2_user')
|
||||
return User::toBase()
|
||||
->whereIn('group_id', $groupId)
|
||||
->whereRaw('u + d < transfer_enable')
|
||||
->where(function ($query) {
|
||||
@@ -314,9 +314,11 @@ class ServerService
|
||||
$servers[$k]['online'] = Cache::get(CacheKey::get("SERVER_{$serverType}_ONLINE_USER", $v['parent_id'] ?? $v['id'])) ?? 0;
|
||||
// 如果是子节点,先尝试从缓存中获取
|
||||
if($pid = $v['parent_id']){
|
||||
// 获取缓存
|
||||
$onlineUsers = Cache::get(CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid)) ?? [];
|
||||
$servers[$k]['online'] = (collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user')) . "|{$servers[$k]['online']}";
|
||||
$cacheKey = CacheKey::get('MULTI_SERVER_' . $serverType . '_ONLINE_USER', $pid);
|
||||
$onlineUsers = Cache::get($cacheKey) ?? [];
|
||||
$onlineUserSum = collect($onlineUsers)->whereIn('ip', $v['ips'])->sum('online_user');
|
||||
$online = ($onlineUserSum > 0 ? $onlineUserSum . "|" : "") . $servers[$k]['online'];
|
||||
$servers[$k]['online'] = $online;
|
||||
}
|
||||
$servers[$k]['last_check_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_CHECK_AT", $v['parent_id'] ?? $v['id']));
|
||||
$servers[$k]['last_push_at'] = Cache::get(CacheKey::get("SERVER_{$serverType}_LAST_PUSH_AT", $v['parent_id'] ?? $v['id']));
|
||||
|
||||
@@ -108,14 +108,14 @@ class StatisticalService
|
||||
/**
|
||||
* 获取指定用户的流量使用情况
|
||||
*/
|
||||
public function getStatUserByUserID($userId): array
|
||||
public function getStatUserByUserID(int|string $userId): array
|
||||
{
|
||||
|
||||
$stats = [];
|
||||
$statsUser = $this->redis->zrange($this->statUserKey, 0, -1, true);
|
||||
foreach ($statsUser as $member => $value) {
|
||||
list($rate, $uid, $type) = explode('_', $member);
|
||||
if ($uid !== $userId)
|
||||
if (intval($uid) !== intval($userId))
|
||||
continue;
|
||||
$key = "{$rate}_{$uid}";
|
||||
$stats[$key] = $stats[$key] ?? [
|
||||
|
||||
+17
-6
@@ -108,13 +108,24 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribeUrl($path)
|
||||
public static function getSubscribeUrl(string $token, $subscribeUrl = null)
|
||||
{
|
||||
$subscribeUrls = explode(',', admin_setting('subscribe_url'));
|
||||
$subscribeUrl = $subscribeUrls[array_rand($subscribeUrls)];
|
||||
$subscribeUrl = self::replaceByPattern($subscribeUrl);
|
||||
if ($subscribeUrl) return $subscribeUrl . $path;
|
||||
return url($path);
|
||||
$strs = 'QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm';
|
||||
$randstr = substr(str_shuffle($strs), 0, rand(4,8));
|
||||
|
||||
$path = route('client.subscribe', ['token' => $token], false);
|
||||
if(!$subscribeUrl){
|
||||
$subscribeUrls = explode(',', admin_setting('subscribe_url'));
|
||||
$subscribeUrl = \Arr::random($subscribeUrls);
|
||||
$subscribeUrl = self::replaceByPattern($subscribeUrl);
|
||||
if (strpos($subscribeUrl, "*") !== false) {
|
||||
$subscribeUrl = str_replace("*", $randstr, $subscribeUrl);
|
||||
} elseif (strpos($subscribeUrl, '{uuid}') !== false) {
|
||||
$user = User::where('token', $token)->first();
|
||||
$subscribeUrl = str_replace('{uuid}', $user->uuid, $subscribeUrl);
|
||||
}
|
||||
}
|
||||
return $subscribeUrl ? rtrim($subscribeUrl, '/') . $path : url($path);
|
||||
}
|
||||
|
||||
public static function randomPort($range) {
|
||||
|
||||
Reference in New Issue
Block a user