Merge pull request #161 from FranzKafkaYu/develop

Update Tg bot related function
This commit is contained in:
vaxilu
2022-04-19 11:33:56 +08:00
committed by GitHub
27 changed files with 533 additions and 56 deletions

View File

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

View File

@@ -9,8 +9,10 @@
- 流量统计,限制流量,限制到期时间 - 流量统计,限制流量,限制到期时间
- 可自定义 xray 配置模板 - 可自定义 xray 配置模板
- 支持 https 访问面板(自备域名 + ssl 证书) - 支持 https 访问面板(自备域名 + ssl 证书)
- 支持一键SSL证书申请且自动续签
- 更多高级配置项,详见面板 - 更多高级配置项,详见面板
# 安装&升级 # 安装&升级
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
@@ -56,6 +58,44 @@ docker run -itd --network=host \
```shell ```shell
docker build -t x-ui . 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+ - CentOS 7+

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/gin-contrib/sessions v0.0.3 github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.7.1 github.com/gin-gonic/gin v1.7.1
github.com/go-ole/go-ole v1.2.5 // indirect 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/nicksnyder/go-i18n/v2 v2.1.2
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1

2
go.sum
View File

@@ -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/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 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 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/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/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= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=

View File

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

107
main.go
View File

@@ -3,7 +3,6 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/op/go-logging"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@@ -16,6 +15,8 @@ import (
"x-ui/web" "x-ui/web"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
"github.com/op/go-logging"
) )
func runWebServer() { func runWebServer() {
@@ -50,6 +51,7 @@ func runWebServer() {
} }
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
//信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
for { for {
sig := <-sigCh 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) { func updateSetting(port int, username string, password string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
@@ -137,11 +223,21 @@ func main() {
var port int var port int
var username string var username string
var password string var password string
var tgbottoken string
var tgbotchatid int
var enabletgbot bool
var tgbotRuntime string
var reset bool 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.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&username, "username", "", "set login username") settingCmd.StringVar(&username, "username", "", "set login username")
settingCmd.StringVar(&password, "password", "", "set login password") 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 oldUsage := flag.Usage
flag.Usage = func() { flag.Usage = func() {
@@ -188,6 +284,13 @@ func main() {
} else { } else {
updateSetting(port, username, password) updateSetting(port, username, password)
} }
if show {
showSetting(show)
}
updateTgbotEnableSts(enabletgbot)
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
default: default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands") fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println() fmt.Println()

BIN
media/2022-04-04_141259.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
media/2022-04-17_110907.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
media/2022-04-17_111321.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
media/2022-04-17_111705.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
media/2022-04-17_111910.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

21
util/common/format.go Normal file
View File

@@ -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))
}
}

View File

@@ -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
}

View File

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

View File

@@ -1,11 +1,14 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"net/http" "net/http"
"time"
"x-ui/logger" "x-ui/logger"
"x-ui/web/job"
"x-ui/web/service" "x-ui/web/service"
"x-ui/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin"
) )
type LoginForm struct { 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) logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, "用户名或密码错误") pureJsonMsg(c, false, "用户名或密码错误")
return 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) err = session.SetLoginUser(c, user)

View File

@@ -27,12 +27,14 @@ type Pager struct {
} }
type AllSetting struct { type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"` WebListen string `json:"webListen" form:"webListen"`
WebPort int `json:"webPort" form:"webPort"` WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"` WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"` 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"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"` TimeLocation string `json:"timeLocation" form:"timeLocation"`

View File

@@ -71,7 +71,14 @@
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item> <setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list> </a-list>
</a-tab-pane> </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"> <a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item> <setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
</a-list> </a-list>

121
web/job/stats_notify_job.go Normal file
View File

@@ -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)
}

View File

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

View File

@@ -29,6 +29,10 @@ var defaultValueMap = map[string]string{
"secret": random.Seq(32), "secret": random.Seq(32),
"webBasePath": "/", "webBasePath": "/",
"timeLocation": "Asia/Shanghai", "timeLocation": "Asia/Shanghai",
"tgBotEnable": "false",
"tgBotToken": "",
"tgBotChatId": "0",
"tgRunTime": "",
} }
type SettingService struct { type SettingService struct {
@@ -156,6 +160,18 @@ func (s *SettingService) setString(key string, value string) error {
return s.saveSetting(key, value) 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) { func (s *SettingService) getInt(key string) (int, error) {
str, err := s.getString(key) str, err := s.getString(key)
if err != nil { if err != nil {
@@ -176,6 +192,38 @@ func (s *SettingService) GetListen() (string, error) {
return s.getString("webListen") 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) { func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort") return s.getInt("webPort")
} }

View File

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

View File

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

View File

@@ -4,13 +4,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"embed" "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" "html/template"
"io" "io"
"io/fs" "io/fs"
@@ -27,6 +20,14 @@ import (
"x-ui/web/job" "x-ui/web/job"
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "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/* //go:embed assets/*
@@ -294,9 +295,28 @@ func (s *Server) startTask() {
// 每 30 秒检查一次 inbound 流量超出和到期的情况 // 每 30 秒检查一次 inbound 流量超出和到期的情况
s.cron.AddJob("@every 30s", job.NewCheckInboundJob()) 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) { func (s *Server) Start() (err error) {
//这是一个匿名函数,没没有函数名
defer func() { defer func() {
if err != nil { if err != nil {
s.Stop() s.Stop()
@@ -348,6 +368,7 @@ func (s *Server) Start() (err error) {
listener = network.NewAutoHttpsListener(listener) listener = network.NewAutoHttpsListener(listener)
listener = tls.NewListener(listener, c) listener = tls.NewListener(listener, c)
} }
if certFile != "" || keyFile != "" { if certFile != "" || keyFile != "" {
logger.Info("web server run https on", listener.Addr()) logger.Info("web server run https on", listener.Addr())
} else { } else {

135
x-ui.sh
View File

@@ -18,7 +18,7 @@ function LOGI() {
echo -e "${green}[INF] $* ${plain}" echo -e "${green}[INF] $* ${plain}"
} }
# check root # check root
[[ $EUID -ne 0 ]] && LOGE "错误: 必须使用root用户运行此脚本\n" && exit 1 [[ $EUID -ne 0 ]] && LOGE "错误: 必须使用root用户运行此脚本!\n" && exit 1
# check os # check os
if [[ -f /etc/redhat-release ]]; then if [[ -f /etc/redhat-release ]]; then
@@ -121,7 +121,7 @@ update() {
} }
uninstall() { uninstall() {
confirm "确定要卸载面板吗xray 也会卸载?" "n" confirm "确定要卸载面板吗,xray 也会卸载?" "n"
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
show_menu show_menu
@@ -171,6 +171,15 @@ reset_config() {
confirm_restart 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() { set_port() {
echo && echo -n -e "输入端口号[1-65535]: " && read port echo && echo -n -e "输入端口号[1-65535]: " && read port
if [[ -z "${port}" ]]; then if [[ -z "${port}" ]]; then
@@ -399,6 +408,70 @@ show_xray_status() {
fi 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() { ssl_cert_issue() {
echo -E "" echo -E ""
LOGD "******使用说明******" LOGD "******使用说明******"
@@ -450,8 +523,8 @@ ssl_cert_issue() {
LOGI "证书签发成功,安装中..." LOGI "证书签发成功,安装中..."
fi fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \ ~/.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 \ --cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
--fullchain-file /root/cert/fullchain.cer --fullchain-file /root/cert/fullchain.cer
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
LOGE "证书安装失败,脚本退出" LOGE "证书安装失败,脚本退出"
exit 1 exit 1
@@ -504,21 +577,25 @@ show_menu() {
${green}4.${plain} 重置用户名密码 ${green}4.${plain} 重置用户名密码
${green}5.${plain} 重置面板设置 ${green}5.${plain} 重置面板设置
${green}6.${plain} 设置面板端口 ${green}6.${plain} 设置面板端口
${green}7.${plain} 当前面板设置
———————————————— ————————————————
${green}7.${plain} 启动 x-ui ${green}8.${plain} 启动 x-ui
${green}8.${plain} 停止 x-ui ${green}9.${plain} 停止 x-ui
${green}9.${plain} 重启 x-ui ${green}10.${plain} 重启 x-ui
${green}10.${plain} 查看 x-ui 状态 ${green}11.${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} 一键安装 bbr (最新内核)
${green}15.${plain} 一键申请SSL证书(acme申请) ${green}16.${plain} 一键申请SSL证书(acme申请)
${green}17.${plain} 开启Telegram通知(TgBot)
${green}18.${plain} 关闭Telegram通知(TgBot)
${green}19.${plain} 设置TelegramBot
" "
show_status show_status
echo && read -p "请输入选择 [0-14]: " num echo && read -p "请输入选择 [0-19]: " num
case "${num}" in case "${num}" in
0) 0)
@@ -543,34 +620,46 @@ show_menu() {
check_install && set_port check_install && set_port
;; ;;
7) 7)
check_install && start check_install && check_config
;; ;;
8) 8)
check_install && stop check_install && start
;; ;;
9) 9)
check_install && restart check_install && stop
;; ;;
10) 10)
check_install && status check_install && restart
;; ;;
11) 11)
check_install && show_log check_install && status
;; ;;
12) 12)
check_install && enable check_install && show_log
;; ;;
13) 13)
check_install && disable check_install && enable
;; ;;
14) 14)
install_bbr check_install && disable
;; ;;
15) 15)
install_bbr
;;
16)
ssl_cert_issue ssl_cert_issue
;; ;;
17)
enable_telegram_bot
;;
18)
disable_telegram_bot
;;
19)
set_telegram_bot
;;
*) *)
LOGE "请输入正确的数字 [0-14]" LOGE "请输入正确的数字 [0-19]"
;; ;;
esac esac
} }

View File

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