فهرست منبع

Merge pull request #161 from FranzKafkaYu/develop

Update Tg bot related function
vaxilu 3 سال پیش
والد
کامیت
04ce724801

+ 1 - 1
.github/workflows/release.yml

@@ -144,4 +144,4 @@ jobs:
           upload_url: ${{ needs.release.outputs.upload_url }}
           asset_path: x-ui-linux-s390x.tar.gz
           asset_name: x-ui-linux-s390x.tar.gz
-          asset_content_type: application/gzip
+          asset_content_type: application/gzip

+ 40 - 0
README.md

@@ -9,8 +9,10 @@
 - 流量统计,限制流量,限制到期时间
 - 可自定义 xray 配置模板
 - 支持 https 访问面板(自备域名 + ssl 证书)
+- 支持一键SSL证书申请且自动续签
 - 更多高级配置项,详见面板
 
+
 # 安装&升级
 ```
 bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
@@ -56,6 +58,44 @@ docker run -itd --network=host \
 ```shell
 docker build -t x-ui .
 ```
+## SSL证书申请
+>此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供 
+
+脚本内置SSL证书申请功能,使用该脚本申请证书,需满足以下条件:  
+- 知晓Cloudflare 注册邮箱
+- 知晓Cloudflare Global API Key
+- 域名已通过cloudflare进行解析到当前服务器  
+
+获取Cloudflare Global API Key的方法:
+    ![](media/bda84fbc2ede834deaba1c173a932223.png)
+    ![](media/d13ffd6a73f938d1037d0708e31433bf.png)  
+
+使用时只需输入`域名`, `邮箱`, `API KEY`即可,示意图如下:
+        ![](media/2022-04-04_141259.png)  
+
+注意事项:
+- 该脚本使用DNS API进行证书申请 
+- 默认使用Let'sEncrypt作为CA方
+- 证书安装目录为/root/cert目录 
+- 本脚本申请证书均为泛域名证书
+
+## Tg机器人使用
+>此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供  
+
+X-UI支持通过Tg机器人实现每日流量通知,面板登录提醒等功能,使用Tg机器人,需要自行申请  
+具体申请教程可以参考[博客链接](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)  
+使用说明:在面板后台或通过脚本设置机器人相关参数,具体包括  
+- Tg机器人Token
+- Tg机器人ChatId
+- Tg机器人周期运行时间,采用crontab语法
+
+参考示例:  
+  每小时定时通知
+ ![](media/2022-04-17_110907.png)  
+  每分钟的第30s通知
+ ![](media/2022-04-17_111321.png)  
+ 效果示意图:  
+ ![](media/2022-04-17_111705.png)   
 
 ## 建议系统
 - CentOS 7+

+ 1 - 0
go.mod

@@ -9,6 +9,7 @@ require (
 	github.com/gin-contrib/sessions v0.0.3
 	github.com/gin-gonic/gin v1.7.1
 	github.com/go-ole/go-ole v1.2.5 // indirect
+	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
 	github.com/nicksnyder/go-i18n/v2 v2.1.2
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
 	github.com/robfig/cron/v3 v3.0.1

+ 2 - 0
go.sum

@@ -70,6 +70,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=

+ 8 - 8
install.sh

@@ -83,15 +83,15 @@ install_base() {
 
 #This function will be called when user installed x-ui out of sercurity
 config_after_install() {
-    echo -e "${yellow}出于安全考虑,安装完成后需要强制修改端口与账户密码${plain}"
-    read -p "请设置您的账户名:" config_account
-    echo -e "${yellow}您的账户名将设定为:${config_account}${plain}"
-    read -p "请设置您的账户密码:" config_password
-    echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}"
-    read -p "请设置面板访问端口:" config_port
-    echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}"
-    read -p "确认设定完成?[y/n]": config_confirm
+    echo -e "${yellow}出于安全考虑,安装/更新完成后需要强制修改端口与账户密码${plain}"
+    read -p "确认是否继续?[y/n]": config_confirm
     if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
+        read -p "请设置您的账户名:" config_account
+        echo -e "${yellow}您的账户名将设定为:${config_account}${plain}"
+        read -p "请设置您的账户密码:" config_password
+        echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}"
+        read -p "请设置面板访问端口:" config_port
+        echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}"
         echo -e "${yellow}确认设定,设定中${plain}"
         /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
         echo -e "${yellow}账户密码设定完成${plain}"

+ 105 - 2
main.go

@@ -3,7 +3,6 @@ package main
 import (
 	"flag"
 	"fmt"
-	"github.com/op/go-logging"
 	"log"
 	"os"
 	"os/signal"
@@ -16,6 +15,8 @@ import (
 	"x-ui/web"
 	"x-ui/web/global"
 	"x-ui/web/service"
+
+	"github.com/op/go-logging"
 )
 
 func runWebServer() {
@@ -50,6 +51,7 @@ func runWebServer() {
 	}
 
 	sigCh := make(chan os.Signal, 1)
+	//信号量捕获处理
 	signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
 	for {
 		sig := <-sigCh
@@ -90,6 +92,90 @@ func resetSetting() {
 	}
 }
 
+func showSetting(show bool) {
+	if show {
+		settingService := service.SettingService{}
+		port, err := settingService.GetPort()
+		if err != nil {
+			fmt.Println("get current port fialed,error info:", err)
+		}
+		userService := service.UserService{}
+		userModel, err := userService.GetFirstUser()
+		if err != nil {
+			fmt.Println("get current user info failed,error info:", err)
+		}
+		username := userModel.Username
+		userpasswd := userModel.Password
+		if (username == "") || (userpasswd == "") {
+			fmt.Println("current username or password is empty")
+		}
+		fmt.Println("current pannel settings as follows:")
+		fmt.Println("username:", username)
+		fmt.Println("userpasswd:", userpasswd)
+		fmt.Println("port:", port)
+	}
+}
+
+func updateTgbotEnableSts(status bool) {
+	settingService := service.SettingService{}
+	currentTgSts, err := settingService.GetTgbotenabled()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status)
+	if currentTgSts != status {
+		err := settingService.SetTgbotenabled(status)
+		if err != nil {
+			fmt.Println(err)
+			return
+		} else {
+			logger.Infof("SetTgbotenabled[%v] success", status)
+		}
+	}
+	return
+}
+
+func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
+	err := database.InitDB(config.GetDBPath())
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	settingService := service.SettingService{}
+
+	if tgBotToken != "" {
+		err := settingService.SetTgBotToken(tgBotToken)
+		if err != nil {
+			fmt.Println(err)
+			return
+		} else {
+			logger.Info("updateTgbotSetting tgBotToken success")
+		}
+	}
+
+	if tgBotRuntime != "" {
+		err := settingService.SetTgbotRuntime(tgBotRuntime)
+		if err != nil {
+			fmt.Println(err)
+			return
+		} else {
+			logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime)
+		}
+	}
+
+	if tgBotChatid != 0 {
+		err := settingService.SetTgBotChatId(tgBotChatid)
+		if err != nil {
+			fmt.Println(err)
+			return
+		} else {
+			logger.Info("updateTgbotSetting tgBotChatid success")
+		}
+	}
+}
+
 func updateSetting(port int, username string, password string) {
 	err := database.InitDB(config.GetDBPath())
 	if err != nil {
@@ -137,11 +223,21 @@ func main() {
 	var port int
 	var username string
 	var password string
+	var tgbottoken string
+	var tgbotchatid int
+	var enabletgbot bool
+	var tgbotRuntime string
 	var reset bool
-	settingCmd.BoolVar(&reset, "reset", false, "reset all setting")
+	var show bool
+	settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
+	settingCmd.BoolVar(&show, "show", false, "show current settings")
 	settingCmd.IntVar(&port, "port", 0, "set panel port")
 	settingCmd.StringVar(&username, "username", "", "set login username")
 	settingCmd.StringVar(&password, "password", "", "set login password")
+	settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
+	settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
+	settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
+	settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
 
 	oldUsage := flag.Usage
 	flag.Usage = func() {
@@ -188,6 +284,13 @@ func main() {
 		} else {
 			updateSetting(port, username, password)
 		}
+		if show {
+			showSetting(show)
+		}
+		updateTgbotEnableSts(enabletgbot)
+		if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
+			updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
+		}
 	default:
 		fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
 		fmt.Println()

BIN
media/2022-04-04_141259.png


BIN
media/2022-04-17_110907.png


BIN
media/2022-04-17_111321.png


BIN
media/2022-04-17_111705.png


BIN
media/2022-04-17_111910.png


BIN
media/bda84fbc2ede834deaba1c173a932223.png


BIN
media/d13ffd6a73f938d1037d0708e31433bf.png


+ 21 - 0
util/common/format.go

@@ -0,0 +1,21 @@
+package common
+
+import (
+   "fmt"
+)
+
+func FormatTraffic(trafficBytes int64) (size string) {
+   if trafficBytes < 1024 {
+      return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
+   } else if trafficBytes < (1024 * 1024) {
+      return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
+   } else if trafficBytes < (1024 * 1024 * 1024) {
+      return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
+   } else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
+      return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
+   } else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
+      return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
+   } else {
+      return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
+   }
+}

+ 9 - 0
util/common/stringUtil.go

@@ -0,0 +1,9 @@
+package common
+
+import "sort"
+
+func IsSubString(target string, str_array []string) bool {
+	sort.Strings(str_array)
+	index := sort.SearchStrings(str_array, target)
+	return index < len(str_array) && str_array[index] == target
+}

+ 3 - 1
web/assets/js/model/models.js

@@ -163,7 +163,9 @@ class AllSetting {
         this.webCertFile = "";
         this.webKeyFile = "";
         this.webBasePath = "/";
-
+        this.tgBotToken = "";
+        this.tgBotChatId = 0;
+        this.tgRunTime = "";
         this.xrayTemplateConfig = "";
 
         this.timeLocation = "Asia/Shanghai";

+ 8 - 1
web/controller/index.go

@@ -1,11 +1,14 @@
 package controller
 
 import (
-	"github.com/gin-gonic/gin"
 	"net/http"
+	"time"
 	"x-ui/logger"
+	"x-ui/web/job"
 	"x-ui/web/service"
 	"x-ui/web/session"
+
+	"github.com/gin-gonic/gin"
 )
 
 type LoginForm struct {
@@ -59,6 +62,10 @@ func (a *IndexController) login(c *gin.Context) {
 		logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
 		pureJsonMsg(c, false, "用户名或密码错误")
 		return
+	} else {
+		timeStr := time.Now().Format("2006-01-02 15:04:05")
+		logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
+		job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr)
 	}
 
 	err = session.SetLoginUser(c, user)

+ 8 - 6
web/entity/entity.go

@@ -27,12 +27,14 @@ type Pager struct {
 }
 
 type AllSetting struct {
-	WebListen   string `json:"webListen" form:"webListen"`
-	WebPort     int    `json:"webPort" form:"webPort"`
-	WebCertFile string `json:"webCertFile" form:"webCertFile"`
-	WebKeyFile  string `json:"webKeyFile" form:"webKeyFile"`
-	WebBasePath string `json:"webBasePath" form:"webBasePath"`
-
+	WebListen          string `json:"webListen" form:"webListen"`
+	WebPort            int    `json:"webPort" form:"webPort"`
+	WebCertFile        string `json:"webCertFile" form:"webCertFile"`
+	WebKeyFile         string `json:"webKeyFile" form:"webKeyFile"`
+	WebBasePath        string `json:"webBasePath" form:"webBasePath"`
+	TgBotToken         string `json:"tgBotToken" form:"tgBotToken"`
+	TgBotChatId        int    `json:"tgBotChatId" form:"tgBotChatId"`
+	TgRunTime          string `json:"tgRunTime" form:"tgRunTime"`
 	XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
 
 	TimeLocation string `json:"timeLocation" form:"timeLocation"`

+ 8 - 1
web/html/xui/setting.html

@@ -71,7 +71,14 @@
                                 <setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
                             </a-list>
                         </a-tab-pane>
-                        <a-tab-pane key="4" tab="其他设置">
+                        <a-tab-pane key="4" tab="TG提醒相关设置">
+                            <a-list item-layout="horizontal" style="background: white">
+                                <setting-list-item type="text" title="电报机器人TOKEN" desc="重启面板生效"  v-model="allSetting.tgBotToken"></setting-list-item>
+                                <setting-list-item type="number" title="电报机器人ChatId" desc="重启面板生效"  v-model.number="allSetting.tgBotChatId"></setting-list-item>
+                                <setting-list-item type="text" title="电报机器人通知时间" desc="采用Crontab定时格式"  v-model="allSetting.tgRunTime"></setting-list-item>
+                            </a-list>
+                        </a-tab-pane>
+                        <a-tab-pane key="5" tab="其他设置">
                             <a-list item-layout="horizontal" style="background: white">
                                 <setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
                             </a-list>

+ 121 - 0
web/job/stats_notify_job.go

@@ -0,0 +1,121 @@
+package job
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"x-ui/logger"
+	"x-ui/util/common"
+	"x-ui/web/service"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
+)
+
+type StatsNotifyJob struct {
+	enable         bool
+	xrayService    service.XrayService
+	inboundService service.InboundService
+	settingService service.SettingService
+}
+
+func NewStatsNotifyJob() *StatsNotifyJob {
+	return new(StatsNotifyJob)
+}
+
+func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
+	//Telegram bot basic info
+	tgBottoken, err := j.settingService.GetTgBotToken()
+	if err != nil {
+		logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
+		return
+	}
+	tgBotid, err := j.settingService.GetTgBotChatId()
+	if err != nil {
+		logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
+		return
+	}
+
+	bot, err := tgbotapi.NewBotAPI(tgBottoken)
+	if err != nil {
+		fmt.Println("get tgbot error:", err)
+		return
+	}
+	bot.Debug = true
+	fmt.Printf("Authorized on account %s", bot.Self.UserName)
+	info := tgbotapi.NewMessage(int64(tgBotid), msg)
+	//msg.ReplyToMessageID = int(tgBotid)
+	bot.Send(info)
+}
+
+//Here run is a interface method of Job interface
+func (j *StatsNotifyJob) Run() {
+	if !j.xrayService.IsXrayRunning() {
+		return
+	}
+	var info string
+	//get hostname
+	name, err := os.Hostname()
+	if err != nil {
+		fmt.Println("get hostname error:", err)
+		return
+	}
+	info = fmt.Sprintf("主机名称:%s\r\n", name)
+	//get ip address
+	var ip string
+	netInterfaces, err := net.Interfaces()
+	if err != nil {
+		fmt.Println("net.Interfaces failed, err:", err.Error())
+		return
+	}
+
+	for i := 0; i < len(netInterfaces); i++ {
+		if (netInterfaces[i].Flags & net.FlagUp) != 0 {
+			addrs, _ := netInterfaces[i].Addrs()
+
+			for _, address := range addrs {
+				if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+					if ipnet.IP.To4() != nil {
+						ip = ipnet.IP.String()
+						break
+					} else {
+						ip = ipnet.IP.String()
+						break
+					}
+				}
+			}
+		}
+	}
+	info += fmt.Sprintf("IP地址:%s\r\n \r\n", ip)
+
+	//get traffic
+	inbouds, err := j.inboundService.GetAllInbounds()
+	if err != nil {
+		logger.Warning("StatsNotifyJob run failed:", err)
+		return
+	}
+	//NOTE:If there no any sessions here,need to notify here
+	//TODO:分节点推送,自动转化格式
+	for _, inbound := range inbouds {
+		info += fmt.Sprintf("节点名称:%s\r\n端口:%d\r\n上行流量↑:%s\r\n下行流量↓:%s\r\n总流量:%s\r\n \r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
+	}
+	j.SendMsgToTgbot(info)
+}
+
+func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string) {
+	if username == "" || ip == "" || time == "" {
+		logger.Warning("UserLoginNotify failed,invalid info")
+		return
+	}
+	var msg string
+	//get hostname
+	name, err := os.Hostname()
+	if err != nil {
+		fmt.Println("get hostname error:", err)
+		return
+	}
+	msg = fmt.Sprintf("面板登录提醒\r\n主机名称:%s\r\n", name)
+	msg += fmt.Sprintf("时间:%s\r\n", time)
+	msg += fmt.Sprintf("用户:%s\r\n", username)
+	msg += fmt.Sprintf("IP:%s\r\n", ip)
+	j.SendMsgToTgbot(msg)
+}

+ 2 - 1
web/service/inbound.go

@@ -2,12 +2,13 @@ package service
 
 import (
 	"fmt"
-	"gorm.io/gorm"
 	"time"
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/util/common"
 	"x-ui/xray"
+
+	"gorm.io/gorm"
 )
 
 type InboundService struct {

+ 48 - 0
web/service/setting.go

@@ -29,6 +29,10 @@ var defaultValueMap = map[string]string{
 	"secret":             random.Seq(32),
 	"webBasePath":        "/",
 	"timeLocation":       "Asia/Shanghai",
+	"tgBotEnable":        "false",
+	"tgBotToken":         "",
+	"tgBotChatId":        "0",
+	"tgRunTime":          "",
 }
 
 type SettingService struct {
@@ -156,6 +160,18 @@ func (s *SettingService) setString(key string, value string) error {
 	return s.saveSetting(key, value)
 }
 
+func (s *SettingService) getBool(key string) (bool, error) {
+	str, err := s.getString(key)
+	if err != nil {
+		return false, err
+	}
+	return strconv.ParseBool(str)
+}
+
+func (s *SettingService) setBool(key string, value bool) error {
+	return s.setString(key, strconv.FormatBool(value))
+}
+
 func (s *SettingService) getInt(key string) (int, error) {
 	str, err := s.getString(key)
 	if err != nil {
@@ -176,6 +192,38 @@ func (s *SettingService) GetListen() (string, error) {
 	return s.getString("webListen")
 }
 
+func (s *SettingService) GetTgBotToken() (string, error) {
+	return s.getString("tgBotToken")
+}
+
+func (s *SettingService) SetTgBotToken(token string) error {
+	return s.setString("tgBotToken", token)
+}
+
+func (s *SettingService) GetTgBotChatId() (int, error) {
+	return s.getInt("tgBotChatId")
+}
+
+func (s *SettingService) SetTgBotChatId(chatId int) error {
+	return s.setInt("tgBotChatId", chatId)
+}
+
+func (s *SettingService) SetTgbotenabled(value bool) error {
+	return s.setBool("tgBotEnable", value)
+}
+
+func (s *SettingService) GetTgbotenabled() (bool, error) {
+	return s.getBool("tgBotEnable")
+}
+
+func (s *SettingService) SetTgbotRuntime(time string) error {
+	return s.setString("tgRunTime", time)
+}
+
+func (s *SettingService) GetTgbotRuntime() (string, error) {
+	return s.getString("tgRunTime")
+}
+
 func (s *SettingService) GetPort() (int, error) {
 	return s.getInt("webPort")
 }

+ 2 - 1
web/service/user.go

@@ -2,10 +2,11 @@ package service
 
 import (
 	"errors"
-	"gorm.io/gorm"
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
+
+	"gorm.io/gorm"
 )
 
 type UserService struct {

+ 2 - 1
web/service/xray.go

@@ -3,10 +3,11 @@ package service
 import (
 	"encoding/json"
 	"errors"
-	"go.uber.org/atomic"
 	"sync"
 	"x-ui/logger"
 	"x-ui/xray"
+
+	"go.uber.org/atomic"
 )
 
 var p *xray.Process

+ 28 - 7
web/web.go

@@ -4,13 +4,6 @@ import (
 	"context"
 	"crypto/tls"
 	"embed"
-	"github.com/BurntSushi/toml"
-	"github.com/gin-contrib/sessions"
-	"github.com/gin-contrib/sessions/cookie"
-	"github.com/gin-gonic/gin"
-	"github.com/nicksnyder/go-i18n/v2/i18n"
-	"github.com/robfig/cron/v3"
-	"golang.org/x/text/language"
 	"html/template"
 	"io"
 	"io/fs"
@@ -27,6 +20,14 @@ import (
 	"x-ui/web/job"
 	"x-ui/web/network"
 	"x-ui/web/service"
+
+	"github.com/BurntSushi/toml"
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-contrib/sessions/cookie"
+	"github.com/gin-gonic/gin"
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"github.com/robfig/cron/v3"
+	"golang.org/x/text/language"
 )
 
 //go:embed assets/*
@@ -294,9 +295,28 @@ func (s *Server) startTask() {
 
 	// 每 30 秒检查一次 inbound 流量超出和到期的情况
 	s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
+	// 每一天提示一次流量情况,上海时间8点30
+	var entry cron.EntryID
+	isTgbotenabled, err := s.settingService.GetTgbotenabled()
+	if (err == nil) && (isTgbotenabled) {
+		runtime, err := s.settingService.GetTgbotRuntime()
+		if err != nil || runtime == "" {
+			logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime)
+			runtime = "@daily"
+		}
+		logger.Infof("Tg notify enabled,run at %s", runtime)
+		entry, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
+		if err != nil {
+			logger.Warning("Add NewStatsNotifyJob error", err)
+			return
+		}
+	} else {
+		s.cron.Remove(entry)
+	}
 }
 
 func (s *Server) Start() (err error) {
+	//这是一个匿名函数,没没有函数名
 	defer func() {
 		if err != nil {
 			s.Stop()
@@ -348,6 +368,7 @@ func (s *Server) Start() (err error) {
 		listener = network.NewAutoHttpsListener(listener)
 		listener = tls.NewListener(listener, c)
 	}
+
 	if certFile != "" || keyFile != "" {
 		logger.Info("web server run https on", listener.Addr())
 	} else {

+ 112 - 23
x-ui.sh

@@ -18,7 +18,7 @@ function LOGI() {
     echo -e "${green}[INF] $* ${plain}"
 }
 # check root
-[[ $EUID -ne 0 ]] && LOGE "错误:  必须使用root用户运行此脚本\n" && exit 1
+[[ $EUID -ne 0 ]] && LOGE "错误:  必须使用root用户运行此脚本!\n" && exit 1
 
 # check os
 if [[ -f /etc/redhat-release ]]; then
@@ -121,7 +121,7 @@ update() {
 }
 
 uninstall() {
-    confirm "确定要卸载面板吗xray 也会卸载?" "n"
+    confirm "确定要卸载面板吗,xray 也会卸载?" "n"
     if [[ $? != 0 ]]; then
         if [[ $# == 0 ]]; then
             show_menu
@@ -171,6 +171,15 @@ reset_config() {
     confirm_restart
 }
 
+check_config() {
+    info=$(/usr/local/x-ui/x-ui setting -show true)
+    if [[ $? != 0 ]]; then
+        LOGE "get current settings error,please check logs"
+        show_menu
+    fi
+    LOGI "${info}"
+}
+
 set_port() {
     echo && echo -n -e "输入端口号[1-65535]: " && read port
     if [[ -z "${port}" ]]; then
@@ -399,6 +408,70 @@ show_xray_status() {
     fi
 }
 
+set_telegram_bot() {
+    echo -E ""
+    LOGI "设置Telegram Bot需要知晓Bot的Token与ChatId"
+    LOGI "使用方法请参考博客https://coderfan.net"
+    confirm "我已确认以上内容[y/n]" "y"
+    if [ $? -ne 0 ]; then
+        show_menu
+    else
+        read -p "please input your tg bot token here:" TG_BOT_TOKEN
+        LOGI "你设置的电报机器人Token:$TG_BOT_TOKEN"
+        read -p "please input your tg chat id here:" TG_BOT_CHATID
+        LOGI "你设置的电报机器人ChatId:$TG_BOT_CHATID"
+        read -p "please input your tg bot runtime here:" TG_BOT_RUNTIME
+        LOGI "你设置的电报机器人运行周期:$TG_BOT_RUNTIME"
+        info=$(/usr/local/x-ui/x-ui setting -tgbottoken ${TG_BOT_TOKEN} -tgbotchatid ${TG_BOT_CHATID} -tgbotRuntime "$TG_BOT_RUNTIME")
+        if [ $? != 0 ]; then
+            LOGE "$info"
+            LOGE "设置TelegramBot失败"
+            exit 1
+        else
+            LOGI "设置TelegramBot成功"
+            show_menu
+        fi
+    fi
+}
+
+enable_telegram_bot() {
+    echo -E ""
+    LOGI "该功能会开启Telegram Bot通知"
+    LOGI "通知内容包括:"
+    LOGI "1.流量使用情况"
+    LOGI "2.节点到期提醒,待实现(规划中)"
+    LOGI "3.面板登录提醒,待完善(规划中)"
+    confirm "我已确认以上内容[y/n]" "y"
+    if [ $? -eq 0 ]; then
+        info=$(/usr/local/x-ui/x-ui setting -enabletgbot=true)
+        if [ $? == 0 ]; then
+            LOGI "开启成功,重启X-UI生效,重启中...."
+            restart
+        else
+            LOGE "开启失败,即将退出..."
+            exit 1
+        fi
+    else
+        show_menu
+    fi
+}
+
+disable_telegram_bot() {
+    confirm "确认是否关闭Tgbot[y/n]" "n"
+    if [ $? -eq 0 ]; then
+        info=$(/usr/local/x-ui/x-ui setting -enabletgbot=false)
+        if [ $? == 0 ]; then
+            LOGI "关闭成功,重启X-UI生效,重启中...."
+            restart
+        else
+            LOGE "关闭失败,请检查日志..."
+            exit 1
+        fi
+    else
+        show_menu
+    fi
+}
+
 ssl_cert_issue() {
     echo -E ""
     LOGD "******使用说明******"
@@ -450,8 +523,8 @@ ssl_cert_issue() {
             LOGI "证书签发成功,安装中..."
         fi
         ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \
-            --cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
-            --fullchain-file /root/cert/fullchain.cer
+        --cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
+        --fullchain-file /root/cert/fullchain.cer
         if [ $? -ne 0 ]; then
             LOGE "证书安装失败,脚本退出"
             exit 1
@@ -504,21 +577,25 @@ show_menu() {
   ${green}4.${plain} 重置用户名密码
   ${green}5.${plain} 重置面板设置
   ${green}6.${plain} 设置面板端口
+  ${green}7.${plain} 当前面板设置
 ————————————————
-  ${green}7.${plain} 启动 x-ui
-  ${green}8.${plain} 停止 x-ui
-  ${green}9.${plain} 重启 x-ui
- ${green}10.${plain} 查看 x-ui 状态
- ${green}11.${plain} 查看 x-ui 日志
+  ${green}8.${plain} 启动 x-ui
+  ${green}9.${plain} 停止 x-ui
+  ${green}10.${plain} 重启 x-ui
+  ${green}11.${plain} 查看 x-ui 状态
+  ${green}12.${plain} 查看 x-ui 日志
 ————————————————
- ${green}12.${plain} 设置 x-ui 开机自启
- ${green}13.${plain} 取消 x-ui 开机自启
+  ${green}13.${plain} 设置 x-ui 开机自启
+  ${green}14.${plain} 取消 x-ui 开机自启
 ————————————————
- ${green}14.${plain} 一键安装 bbr (最新内核)
- ${green}15.${plain} 一键申请SSL证书(acme申请)
+  ${green}15.${plain} 一键安装 bbr (最新内核)
+  ${green}16.${plain} 一键申请SSL证书(acme申请)
+  ${green}17.${plain} 开启Telegram通知(TgBot)
+  ${green}18.${plain} 关闭Telegram通知(TgBot)
+  ${green}19.${plain} 设置TelegramBot
  "
     show_status
-    echo && read -p "请输入选择 [0-14]: " num
+    echo && read -p "请输入选择 [0-19]: " num
 
     case "${num}" in
     0)
@@ -543,34 +620,46 @@ show_menu() {
         check_install && set_port
         ;;
     7)
-        check_install && start
+        check_install && check_config
         ;;
     8)
-        check_install && stop
+        check_install && start
         ;;
     9)
-        check_install && restart
+        check_install && stop
         ;;
     10)
-        check_install && status
+        check_install && restart
         ;;
     11)
-        check_install && show_log
+        check_install && status
         ;;
     12)
-        check_install && enable
+        check_install && show_log
         ;;
     13)
-        check_install && disable
+        check_install && enable
         ;;
     14)
-        install_bbr
+        check_install && disable
         ;;
     15)
+        install_bbr
+        ;;
+    16)
         ssl_cert_issue
         ;;
+    17)
+        enable_telegram_bot
+        ;;
+    18)
+        disable_telegram_bot
+        ;;
+    19)
+        set_telegram_bot
+        ;;
     *)
-        LOGE "请输入正确的数字 [0-14]"
+        LOGE "请输入正确的数字 [0-19]"
         ;;
     esac
 }

+ 4 - 3
xray/process.go

@@ -7,9 +7,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/Workiva/go-datastructures/queue"
-	statsservice "github.com/xtls/xray-core/app/stats/command"
-	"google.golang.org/grpc"
 	"io/fs"
 	"os"
 	"os/exec"
@@ -18,6 +15,10 @@ import (
 	"strings"
 	"time"
 	"x-ui/util/common"
+
+	"github.com/Workiva/go-datastructures/queue"
+	statsservice "github.com/xtls/xray-core/app/stats/command"
+	"google.golang.org/grpc"
 )
 
 var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")