瀏覽代碼

feat: 添加Telegram机器人消息通知和用户绑定(订单通知,工单通知,工单回复,节点掉线通知,昨日节点流量使用通知) (#182)

BobCoderS9 4 年之前
父節點
當前提交
0fd76ba5b4

+ 8 - 0
app/Http/Controllers/Admin/SystemController.php

@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
 use App\Http\Requests\Admin\SystemRequest;
 use App\Models\Config;
 use App\Notifications\Custom;
+use App\Services\TelegramService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\RedirectResponse;
 use Notification;
@@ -133,6 +134,13 @@ class SystemController extends Controller
             $value /= 100;
         }
 
+        // 设置TG机器人
+        if ($name === 'telegram_token' && $value) {
+            $telegramService = new TelegramService($value);
+            $telegramService->getMe();
+            $telegramService->setWebhook(rtrim(sysConfig('website_url'), '/').'/api/telegram/webhook?access_token='.md5($value));
+        }
+
         // 更新配置
         if (Config::findOrFail($name)->update(['value' => $value])) {
             return Response::json(['status' => 'success', 'message' => trans('common.update_action', ['action' => trans('common.success')])]);

+ 4 - 6
app/Http/Controllers/Admin/TicketController.php

@@ -42,9 +42,7 @@ class TicketController extends Controller
         }
 
         if ($ticket = Ticket::create(['user_id' => $user->id, 'admin_id' => auth()->id(), 'title' => $data['title'], 'content' => clean($data['content'])])) {
-            if (in_array('mail', sysConfig('ticket_created_notification') ?? [], true)) {
-                $user->notify(new TicketCreated($data['title'], $data['content'], route('replyTicket', $ticket), true));
-            }
+            $user->notify(new TicketCreated($ticket, route('replyTicket', $ticket)));
 
             return Response::json(['status' => 'success', 'message' => '工单创建成功']);
         }
@@ -71,8 +69,8 @@ class TicketController extends Controller
             $ticket->update(['status' => 1]);
 
             // 通知用户
-            if (in_array('mail', sysConfig('ticket_replied_notification') ?? [], true)) {
-                $ticket->user->notify(new TicketReplied($ticket->title, $content, route('replyTicket', $ticket), true));
+            if (sysConfig('ticket_replied_notification')) {
+                $ticket->user->notify(new TicketReplied($ticket, route('replyTicket', $ticket), true));
             }
 
             return Response::json(['status' => 'success', 'message' => '回复成功']);
@@ -88,7 +86,7 @@ class TicketController extends Controller
             return Response::json(['status' => 'fail', 'message' => '关闭失败']);
         }
         // 通知用户
-        if (in_array('mail', sysConfig('ticket_closed_notification') ?? [], true)) {
+        if (sysConfig('ticket_closed_notification')) {
             $ticket->user->notify(new TicketClosed($ticket->id, $ticket->title, route('replyTicket', $ticket), \request('reason'), true));
         }
 

+ 215 - 0
app/Http/Controllers/TelegramController.php

@@ -0,0 +1,215 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Ticket;
+use App\Models\User;
+use App\Services\TelegramService;
+use Illuminate\Http\Request;
+
+class TelegramController extends Controller
+{
+    protected $msg;
+
+    public function __construct(Request $request)
+    {
+        if ($request->input('access_token') !== md5(sysConfig('telegram_token'))) {
+            abort(500, 'authentication failed');
+        }
+    }
+
+    public function webhook(Request $request)
+    {
+        $this->msg = $this->getMessage($request->input());
+        if (! $this->msg) {
+            return;
+        }
+        try {
+            switch ($this->msg->message_type) {
+                case 'send':
+                    $this->fromSend();
+                    break;
+                case 'reply':
+                    $this->fromReply();
+                    break;
+            }
+        } catch (\Exception $e) {
+            $telegramService = new TelegramService();
+            $telegramService->sendMessage($this->msg->chat_id, $e->getMessage());
+        }
+    }
+
+    private function fromSend()
+    {
+        switch ($this->msg->command) {
+            case '/bind':
+                $this->bind();
+                break;
+            case '/traffic':
+                $this->traffic();
+                break;
+            case '/getlatesturl':
+                $this->getLatestUrl();
+                break;
+            case '/unbind':
+                $this->unbind();
+                break;
+            default:
+                $this->help();
+        }
+    }
+
+    private function fromReply()
+    {
+        // ticket
+        if (preg_match('/[#](.*)/', $this->msg->reply_text, $match)) {
+            $this->replayTicket($match[1]);
+        }
+    }
+
+    private function getMessage(array $data)
+    {
+        if (! isset($data['message'])) {
+            return false;
+        }
+        $obj = new \StdClass();
+        $obj->is_private = $data['message']['chat']['type'] === 'private' ? true : false;
+        if (! isset($data['message']['text'])) {
+            return false;
+        }
+        $text = explode(' ', $data['message']['text']);
+        $obj->command = $text[0];
+        $obj->args = array_slice($text, 1);
+        $obj->chat_id = $data['message']['chat']['id'];
+        $obj->message_id = $data['message']['message_id'];
+        $obj->message_type = ! isset($data['message']['reply_to_message']['text']) ? 'send' : 'reply';
+        $obj->text = $data['message']['text'];
+        if ($obj->message_type === 'reply') {
+            $obj->reply_text = $data['message']['reply_to_message']['text'];
+        }
+
+        return $obj;
+    }
+
+    private function bind()
+    {
+        $msg = $this->msg;
+        if (! $msg->is_private) {
+            return;
+        }
+        if (! isset($msg->args[0])) {
+            abort(500, '参数有误,请携带邮箱地址发送');
+        }
+        $user = User::where('email', $msg->args[0])->first();
+        if (! $user) {
+            abort(500, '用户不存在');
+        }
+        if ($user->telegram_id) {
+            abort(500, '该账号已经绑定了Telegram账号');
+        }
+        $user->telegram_id = $msg->chat_id;
+        if (! $user->save()) {
+            abort(500, '设置失败');
+        }
+        $telegramService = new TelegramService();
+        $telegramService->sendMessage($msg->chat_id, '绑定成功');
+    }
+
+    private function unbind()
+    {
+        $msg = $this->msg;
+        if (! $msg->is_private) {
+            return;
+        }
+        $user = User::where('telegram_id', $msg->chat_id)->first();
+        $telegramService = new TelegramService();
+        if (! $user) {
+            $this->help();
+            $telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
+
+            return;
+        }
+        $user->telegram_id = null;
+        if (! $user->save()) {
+            abort(500, '解绑失败');
+        }
+        $telegramService->sendMessage($msg->chat_id, '解绑成功', 'markdown');
+    }
+
+    private function help()
+    {
+        $msg = $this->msg;
+        if (! $msg->is_private) {
+            return;
+        }
+        $telegramService = new TelegramService();
+        $commands = [
+            '/bind 订阅地址 - 绑定你的'.sysConfig('website_name').'账号',
+            '/traffic - 查询流量信息',
+            '/getlatesturl - 获取最新的'.sysConfig('website_name').'网址',
+            '/unbind - 解除绑定',
+        ];
+        $text = implode(PHP_EOL, $commands);
+        $telegramService->sendMessage($msg->chat_id, "你可以使用以下命令进行操作:\n\n$text", 'markdown');
+    }
+
+    private function traffic()
+    {
+        $msg = $this->msg;
+        if (! $msg->is_private) {
+            return;
+        }
+        $user = User::where('telegram_id', $msg->chat_id)->first();
+        $telegramService = new TelegramService();
+        if (! $user) {
+            $this->help();
+            $telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
+
+            return;
+        }
+        $transferEnable = flowAutoShow($user->transfer_enable);
+        $up = flowAutoShow($user->u);
+        $down = flowAutoShow($user->d);
+        $remaining = flowAutoShow($user->transfer_enable - ($user->u + $user->d));
+        $text = "🚥流量查询\n———————————————\n计划流量:`{$transferEnable}`\n已用上行:`{$up}`\n已用下行:`{$down}`\n剩余流量:`{$remaining}`";
+        $telegramService->sendMessage($msg->chat_id, $text, 'markdown');
+    }
+
+    private function getLatestUrl()
+    {
+        $msg = $this->msg;
+        $telegramService = new TelegramService();
+        $text = sprintf(
+            '%s的最新网址是:%s',
+            sysConfig('website_name'),
+            sysConfig('website_url')
+        );
+        $telegramService->sendMessage($msg->chat_id, $text, 'markdown');
+    }
+
+    private function replayTicket($ticketId)
+    {
+        $msg = $this->msg;
+        if (! $msg->is_private) {
+            return;
+        }
+        $user = User::where('telegram_id', $msg->chat_id)->first();
+        if (! $user) {
+            abort(500, '用户不存在');
+        }
+        $admin = User::role('Super Admin')->where('user.id', $user->id)->first();
+        if ($admin) {
+            $ticket = Ticket::where('id', $ticketId)
+                ->first();
+            if (! $ticket) {
+                abort(500, '工单不存在');
+            }
+            if ($ticket->status) {
+                abort(500, '工单已关闭,无法回复');
+            }
+            $ticket->reply()->create(['admin_id' => $admin->id, 'content' => $msg->text]);
+        }
+        $telegramService = new TelegramService();
+        $telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
+    }
+}

+ 2 - 2
app/Http/Controllers/UserController.php

@@ -284,7 +284,7 @@ class UserController extends Controller
 
         if ($ticket = $user->tickets()->create(compact('title', 'content'))) {
             // 通知相关管理员
-            Notification::send(User::find(1), new TicketCreated($ticket->title, $ticket->content, route('admin.ticket.edit', $ticket)));
+            Notification::send(User::find(1), new TicketCreated($ticket, route('admin.ticket.edit', $ticket)));
 
             return Response::json(['status' => 'success', 'message' => trans('common.submit_item', ['attribute' => trans('common.success')])]);
         }
@@ -318,7 +318,7 @@ class UserController extends Controller
                 $ticket->save();
 
                 // 通知相关管理员
-                Notification::send(User::find(1), new TicketReplied($ticket->title, $content, route('admin.ticket.edit', $ticket)));
+                Notification::send(User::find(1), new TicketReplied($ticket, route('admin.ticket.edit', $ticket)));
 
                 return Response::json(['status' => 'success', 'message' => trans('user.ticket.reply').trans('common.success')]);
             }

+ 1 - 0
app/Http/Middleware/VerifyCsrfToken.php

@@ -13,5 +13,6 @@ class VerifyCsrfToken extends Middleware
      */
     protected $except = [
         'callback/notify',
+        'api/telegram/webhook',
     ];
 }

+ 10 - 0
app/Models/User.php

@@ -313,4 +313,14 @@ class User extends Authenticatable implements JWTSubject
     {
         return [];
     }
+
+    /**
+     * Route notifications for the Telegram channel.
+     *
+     * @return int
+     */
+    public function routeNotificationForTelegram()
+    {
+        return $this->telegram_id;
+    }
 }

+ 12 - 0
app/Notifications/DataAnomaly.php

@@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class DataAnomaly extends Notification implements ShouldQueue
 {
@@ -43,4 +44,15 @@ class DataAnomaly extends Notification implements ShouldQueue
             'content' => trans('notification.data_anomaly_content', ['id' => $this->userId, 'upload' => $this->upload, 'download' => $this->download, 'total' => $this->total]),
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content(trans('notification.data_anomaly_content', ['id' => $this->userId, 'upload' => $this->upload, 'download' => $this->download, 'total' => $this->total]));
+    }
 }

+ 12 - 0
app/Notifications/NodeBlocked.php

@@ -7,6 +7,7 @@ use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class NodeBlocked extends Notification implements ShouldQueue
 {
@@ -56,4 +57,15 @@ class NodeBlocked extends Notification implements ShouldQueue
             'content' => $this->markdownMessage(),
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content($this->markdownMessage());
+    }
 }

+ 12 - 0
app/Notifications/NodeDailyReport.php

@@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class NodeDailyReport extends Notification implements ShouldQueue
 {
@@ -47,4 +48,15 @@ class NodeDailyReport extends Notification implements ShouldQueue
             'content' => $this->markdownMessage(),
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content($this->markdownMessage());
+    }
 }

+ 12 - 0
app/Notifications/NodeOffline.php

@@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class NodeOffline extends Notification implements ShouldQueue
 {
@@ -65,4 +66,15 @@ class NodeOffline extends Notification implements ShouldQueue
 
         return $content;
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content($this->markdownMessage());
+    }
 }

+ 22 - 0
app/Notifications/PaymentReceived.php

@@ -2,10 +2,12 @@
 
 namespace App\Notifications;
 
+use App\Models\User;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class PaymentReceived extends Notification implements ShouldQueue
 {
@@ -40,4 +42,24 @@ class PaymentReceived extends Notification implements ShouldQueue
             'amount' => $this->amount,
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        foreach (User::role('Super Admin')->get() as $admin) {
+            $message = sprintf(
+                "💰成功收款%s元\n———————————————\n订单号:%s",
+                $this->amount,
+                $this->sn
+            );
+
+            return TelegramMessage::create()
+                ->to($admin->telegram_id)
+                ->token(sysConfig('telegram_token'))
+                ->content($message);
+        }
+    }
 }

+ 13 - 1
app/Notifications/TicketClosed.php

@@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class TicketClosed extends Notification implements ShouldQueue
 {
@@ -17,7 +18,7 @@ class TicketClosed extends Notification implements ShouldQueue
     private $reason;
     private $is_user;
 
-    public function __construct($ticketId, $title, $url, $reason, $is_user = null)
+    public function __construct($ticketId, $title, $url, $reason, $is_user = false)
     {
         $this->ticketId = $ticketId;
         $this->title = $title;
@@ -47,4 +48,15 @@ class TicketClosed extends Notification implements ShouldQueue
             'content' => $this->reason,
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content($this->reason);
+    }
 }

+ 26 - 9
app/Notifications/TicketCreated.php

@@ -2,24 +2,24 @@
 
 namespace App\Notifications;
 
+use App\Models\Ticket;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class TicketCreated extends Notification implements ShouldQueue
 {
     use Queueable;
 
-    private $title;
-    private $content;
+    private $ticket;
     private $url;
     private $is_user;
 
-    public function __construct($title, $content, $url, $is_user = null)
+    public function __construct(Ticket $ticket, $url, $is_user = false)
     {
-        $this->title = $title;
-        $this->content = $content;
+        $this->ticket = $ticket;
         $this->url = $url;
         $this->is_user = $is_user;
     }
@@ -32,17 +32,34 @@ class TicketCreated extends Notification implements ShouldQueue
     public function toMail($notifiable)
     {
         return (new MailMessage)
-            ->subject(trans('notification.new_ticket', ['title' => $this->title]))
+            ->subject(trans('notification.new_ticket', ['title' => $this->ticket->title]))
             ->line(trans('notification.ticket_content'))
-            ->line($this->content)
+            ->line($this->ticket->content)
             ->action(trans('notification.view_ticket'), $this->url);
     }
 
     public function toCustom($notifiable)
     {
         return [
-            'title'   => trans('notification.new_ticket', ['title' => $this->title]),
-            'content' => trans('notification.ticket_content').strip_tags($this->content),
+            'title'   => trans('notification.new_ticket', ['title' => $this->ticket->title]),
+            'content' => trans('notification.ticket_content').strip_tags($this->ticket->content),
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content($this->markdownMessage($this->ticket))
+            ->button(trans('notification.view_ticket'), $this->url);
+    }
+
+    private function markdownMessage($ticket)
+    {
+        return "📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->title}`\n内容:\n`{$ticket->content}`";
+    }
 }

+ 26 - 9
app/Notifications/TicketReplied.php

@@ -2,24 +2,24 @@
 
 namespace App\Notifications;
 
+use App\Models\Ticket;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
 use Illuminate\Notifications\Notification;
+use NotificationChannels\Telegram\TelegramMessage;
 
 class TicketReplied extends Notification implements ShouldQueue
 {
     use Queueable;
 
-    private $title;
-    private $content;
+    private $ticket;
     private $url;
     private $is_user;
 
-    public function __construct($title, $content, $url, $is_user = null)
+    public function __construct(Ticket $ticket, $url, $is_user = false)
     {
-        $this->title = $title;
-        $this->content = $content;
+        $this->ticket = $ticket;
         $this->url = $url;
         $this->is_user = $is_user;
     }
@@ -32,17 +32,34 @@ class TicketReplied extends Notification implements ShouldQueue
     public function toMail($notifiable)
     {
         return (new MailMessage)
-            ->subject(trans('notification.reply_ticket', ['title' => $this->title]))
+            ->subject(trans('notification.reply_ticket', ['title' => $this->ticket->title]))
             ->line(trans('notification.ticket_content'))
-            ->line(strip_tags($this->content))
+            ->line(strip_tags($this->ticket->content))
             ->action(trans('notification.view_ticket'), $this->url);
     }
 
     public function toCustom($notifiable)
     {
         return [
-            'title'   => trans('notification.reply_ticket', ['title' => $this->title]),
-            'content' => trans('notification.ticket_content').strip_tags($this->content),
+            'title' => trans('notification.reply_ticket', ['title' => $this->ticket->title]),
+            'content' => trans('notification.ticket_content').strip_tags($this->ticket->content),
         ];
     }
+
+    /**
+     * @param $notifiable
+     * @return TelegramMessage|\NotificationChannels\Telegram\Traits\HasSharedLogic
+     */
+    public function toTelegram($notifiable)
+    {
+        return TelegramMessage::create()
+            ->token(sysConfig('telegram_token'))
+            ->content($this->markdownMessage($this->ticket))
+            ->button(trans('notification.view_ticket'), $this->url);
+    }
+
+    private function markdownMessage($ticket)
+    {
+        return "📮工单回复提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->title}`\n内容:\n`{$ticket->content}`";
+    }
 }

+ 47 - 0
app/Services/TelegramService.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Http;
+
+class TelegramService
+{
+    protected $api;
+
+    public function __construct($token = '')
+    {
+        $this->api = 'https://api.telegram.org/bot'.($token ? $token : sysConfig('telegram_token')).'/';
+    }
+
+    public function sendMessage(int $chatId, string $text, string $parseMode = '')
+    {
+        $this->request('sendMessage', [
+            'chat_id' => $chatId,
+            'text' => $text,
+            'parse_mode' => $parseMode,
+        ]);
+    }
+
+    public function getMe()
+    {
+        return $this->request('getMe');
+    }
+
+    public function setWebhook(string $url)
+    {
+        return $this->request('setWebhook', [
+            'url' => $url,
+        ]);
+    }
+
+    private function request(string $method, array $params = [])
+    {
+        $curl = new Http();
+        $response = Http::get($this->api.$method.'?'.http_build_query($params));
+        if (! $response->ok()) {
+            abort(500, '来自TG的错误:'.$response->json());
+        }
+
+        return $response->json();
+    }
+}

+ 34 - 0
database/migrations/2021_06_23_103914_append_telegram_id_to_user_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use App\Models\Config;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AppendTelegramIdToUserTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('user', function (Blueprint $table) {
+            $table->string('telegram_id')->nullable()->comment('用户绑定的Telegram_ID');
+        });
+        Config::insert(['name' => 'telegram_token']);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('user', function (Blueprint $table) {
+            $table->dropColumn(['telegram_id']);
+        });
+    }
+}

+ 1 - 0
routes/web.php

@@ -9,6 +9,7 @@ if (env('APP_KEY') && config('settings')) {
 }
 
 Route::get('callback/checkout', 'Gateway\PayPal@getCheckout')->name('paypal.checkout'); // 支付回调相关
+Route::post('api/telegram/webhook', 'TelegramController@webhook'); // Telegram fallback
 
 // 登录相关
 Route::middleware(['isForbidden', 'affiliate', 'isMaintenance'])->group(function () {