Compare commits

...

16 Commits
1.0.0 ... 1.0.2

Author SHA1 Message Date
Alireza Ahmadi
f7aee9fe18 v1.0.2 2023-04-24 17:52:33 +02:00
Alireza Ahmadi
a19b58675b Add migration to install script 2023-04-24 11:16:28 +02:00
Alireza Ahmadi
7a229b27f5 Better client delete + api #218 2023-04-24 11:10:59 +02:00
Alireza Ahmadi
abf68446e5 Optimize database #258 2023-04-24 10:40:37 +02:00
Alireza Ahmadi
b4b7ec565e simplify API: del client #218 2023-04-22 11:21:44 +02:00
Alireza Ahmadi
4b1b920bf4 Add database migration 2023-04-22 11:19:22 +02:00
Alireza Ahmadi
11bff57f23 [feature] add getClientTraffics api 2023-04-20 19:16:06 +02:00
Alireza Ahmadi
4838b0f6f0 v1.0.1 2023-04-20 14:30:58 +02:00
Alireza Ahmadi
bb8807129a [client] add reset traffic in edit #244 2023-04-20 13:59:32 +02:00
Alireza Ahmadi
cb050bfe34 [sub] fix userinfo header #217 2023-04-20 12:53:50 +02:00
Alireza Ahmadi
76a996cc2d main.go enhancements 2023-04-20 10:55:58 +02:00
Alireza Ahmadi
7fec1dd5cf [reality] fix fingerprint issue #236 2023-04-20 10:50:20 +02:00
Alireza Ahmadi
a24e9089b6 small visual enhancements #237 2023-04-20 10:48:57 +02:00
Alireza Ahmadi
b6474eaef6 fix setting numbers #237 2023-04-20 10:47:24 +02:00
Alireza Ahmadi
76f90b0a46 fix enabletgbot cli 2023-04-20 10:45:21 +02:00
Alireza Ahmadi
42abffdaef [docker] fix build for amd64 2023-04-20 10:36:46 +02:00
21 changed files with 286 additions and 200 deletions

View File

@@ -10,8 +10,11 @@ else
FNAME="amd64"; FNAME="amd64";
fi fi
mkdir -p build/bin mkdir -p build/bin
cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip" wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -o build/bin/geoip.dat unzip "Xray-linux-${ARCH}.zip"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -o build/bin/geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "build/bin/xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"
mv main build/x-ui wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
cd ../../

View File

@@ -1,9 +1,9 @@
FROM golang:1.20-alpine AS builder FROM golang:1.20-alpine AS builder
WORKDIR /app WORKDIR /app
ARG TARGETARCH ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget RUN apk --no-cache --update add build-base gcc wget unzip
COPY . . COPY . .
RUN env CGO_ENABLED=1 go build main.go RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInitFiles.sh "$TARGETARCH" RUN ./DockerInitFiles.sh "$TARGETARCH"
FROM alpine FROM alpine

View File

@@ -54,7 +54,7 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
| `POST` | `"/del/:id"` | Delete Inbound | | `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound | | `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/addClient/"` | Add Client to inbound | | `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/delClient/:email"` | Delete Client | | `POST` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId |
| `POST` | `"/updateClient/:index"` | Update Client | | `POST` | `"/updateClient/:index"` | Update Client |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic | | `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds | | `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |

View File

@@ -1 +1 @@
1.0.0 1.0.2

View File

@@ -79,6 +79,7 @@ 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() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [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

24
main.go
View File

@@ -51,8 +51,8 @@ func runWebServer() {
} }
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
//信号量捕获处理 // Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
for { for {
sig := <-sigCh sig := <-sigCh
@@ -133,7 +133,6 @@ func updateTgbotEnableSts(status bool) {
logger.Infof("SetTgbotenabled[%v] success", status) logger.Infof("SetTgbotenabled[%v] success", status)
} }
} }
return
} }
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
@@ -204,6 +203,19 @@ func updateSetting(port int, username string, password string) {
} }
} }
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrationRequirements()
inboundService.RemoveOrphanedTraffics()
fmt.Println("Migration done!")
}
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
runWebServer() runWebServer()
@@ -246,6 +258,7 @@ func main() {
fmt.Println("Commands:") fmt.Println("Commands:")
fmt.Println(" run run web panel") fmt.Println(" run run web panel")
fmt.Println(" v2-ui migrate form v2-ui") fmt.Println(" v2-ui migrate form v2-ui")
fmt.Println(" migrate migrate form other/old x-ui")
fmt.Println(" setting set settings") fmt.Println(" setting set settings")
} }
@@ -263,6 +276,8 @@ func main() {
return return
} }
runWebServer() runWebServer()
case "migrate":
migrateDb()
case "v2-ui": case "v2-ui":
err := v2uiCmd.Parse(os.Args[2:]) err := v2uiCmd.Parse(os.Args[2:])
if err != nil { if err != nil {
@@ -290,6 +305,9 @@ func main() {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") { if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
} }
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
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()

View File

@@ -647,7 +647,7 @@ class RealityStreamSettings extends XrayCommonClass {
} }
RealityStreamSettings.Settings = class extends XrayCommonClass { RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = '', serverName = '', spiderX= '/') { constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
super(); super();
this.publicKey = publicKey; this.publicKey = publicKey;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
@@ -1243,15 +1243,13 @@ class Inbound extends XrayCommonClass {
if (this.reality) { if (this.reality) {
params.set("security", "reality"); params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey); params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) { if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]); params.set("sni", this.stream.reality.serverNames.split(",")[0]);
} }
if (this.stream.reality.shortIds.length > 0) { if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]); params.set("sid", this.stream.reality.shortIds.split(",")[0]);
} }
if (!ObjectUtil.isEmpty(this.stream.reality.fingerprint)) {
params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName; address = this.stream.reality.settings.serverName;
} }
@@ -1355,15 +1353,13 @@ class Inbound extends XrayCommonClass {
if (this.reality) { if (this.reality) {
params.set("security", "reality"); params.set("security", "reality");
params.set("pbk", this.stream.reality.settings.publicKey); params.set("pbk", this.stream.reality.settings.publicKey);
params.set("fp", this.stream.reality.settings.fingerprint);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) { if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]); params.set("sni", this.stream.reality.serverNames.split(",")[0]);
} }
if (this.stream.reality.shortIds.length > 0) { if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]); params.set("sid", this.stream.reality.shortIds.split(",")[0]);
} }
if (!ObjectUtil.isEmpty(this.stream.reality.fingerprint)) {
params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName; address = this.stream.reality.settings.serverName;
} }

View File

@@ -19,11 +19,12 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.inbounds) g.GET("/", a.inbounds)
g.GET("/get/:id", a.inbound) g.GET("/get/:id", a.inbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.POST("/add", a.addInbound) g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound) g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/addClient/", a.addInboundClient) g.POST("/addClient/", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient) g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient) g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllTraffics", a.resetAllTraffics)
@@ -38,6 +39,9 @@ func (a *APIController) inbounds(c *gin.Context) {
func (a *APIController) inbound(c *gin.Context) { func (a *APIController) inbound(c *gin.Context) {
a.inboundController.getInbound(c) a.inboundController.getInbound(c)
} }
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) { func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c) a.inboundController.addInbound(c)
} }

View File

@@ -32,7 +32,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/del/:id", a.delInbound) g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/addClient", a.addInboundClient) g.POST("/addClient", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient) g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient) g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllTraffics", a.resetAllTraffics)
@@ -75,6 +75,15 @@ func (a *InboundController) getInbound(c *gin.Context) {
} }
jsonObj(c, inbound, nil) jsonObj(c, inbound, nil)
} }
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, clientTraffics, nil)
}
func (a *InboundController) addInbound(c *gin.Context) { func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
@@ -148,15 +157,14 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
} }
func (a *InboundController) delInboundClient(c *gin.Context) { func (a *InboundController) delInboundClient(c *gin.Context) {
email := c.Param("email") id, err := strconv.Atoi(c.Param("id"))
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return return
} }
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(inbound, email) err = a.inboundService.DelInboundClient(id, clientId)
if err != nil { if err != nil {
jsonMsg(c, "something worng!", err) jsonMsg(c, "something worng!", err)
return return

View File

@@ -39,7 +39,7 @@ func (a *SUBController) subs(c *gin.Context) {
} }
// Add subscription-userinfo // Add subscription-userinfo
c.Writer.Header().Set("subscription-userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", header)
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} }

View File

@@ -122,6 +122,24 @@
} }
client.email = string; client.email = string;
}, },
resetClientTraffic(email,dbInboundId,iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}
iconElement.disabled = false;
},
})
},
}, },
}); });
</script> </script>

View File

@@ -9,7 +9,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input> <a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template> </template>
<template v-else-if="type === 'number'"> <template v-else-if="type === 'number'">
<a-input-number :value="value" @input="$emit('input', $event.target.value)"></a-input-number> <a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template> </template>
<template v-else-if="type === 'textarea'"> <template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea> <a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>

View File

@@ -103,6 +103,10 @@
[[ sizeFormat(clientStats.down) ]] [[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]]) ([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag> </a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -1,24 +1,17 @@
{{define "form/streamSettings"}} {{define "form/streamSettings"}}
<!-- select stream network --> <!-- select stream network -->
<a-form layout="inline"> <a-form layout="inline">
<table width="100%" class="ant-table-tbody"> <a-form-item label="{{ i18n "transmission" }}">
<tr> <a-select v-model="inbound.stream.network" @change="streamNetworkChange"
<td>{{ i18n "transmission" }}</td> :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<td> <a-select-option value="tcp">tcp</a-select-option>
<a-form-item> <a-select-option value="kcp">kcp</a-select-option>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" <a-select-option value="ws">ws</a-select-option>
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select-option value="http">http</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="quic">quic</a-select-option>
<a-select-option value="kcp">kcp</a-select-option> <a-select-option value="grpc">grpc</a-select-option>
<a-select-option value="ws">ws</a-select-option> </a-select>
<a-select-option value="http">http</a-select-option> </a-form-item>
<a-select-option value="quic">quic</a-select-option>
<a-select-option value="grpc">grpc</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
</table>
</a-form> </a-form>
<!-- tcp --> <!-- tcp -->

View File

@@ -176,7 +176,6 @@
<td> <td>
<a-form-item > <a-form-item >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 250px"> <a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 250px">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

View File

@@ -51,7 +51,7 @@
</td> </td>
<td v-else-if="inbound.reality"> <td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td> </td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag> <td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>

View File

@@ -612,22 +612,14 @@
}, },
delClient(dbInboundId,client) { delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = new DBInbound(dbInbound); clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
inbound = newDbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients.splice(index, 1);
const data = {
id: dbInboundId,
settings: inbound.settings.toString(),
};
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data), onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
}); });
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {

View File

@@ -300,22 +300,50 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
return db.Save(oldInbound).Error return db.Save(oldInbound).Error
} }
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error { func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
db := database.GetDB() oldInbound, err := s.GetInbound(inboundId)
err := s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil { if err != nil {
logger.Error("Load Old Data Error") logger.Error("Load Old Data Error")
return err return err
} }
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings email := ""
client_key := "id"
if oldInbound.Protocol == "trojan" {
client_key = "password"
}
inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
for _, client := range inerfaceClients {
c := client.(map[string]interface{})
c_id := c[client_key].(string)
if c_id == clientId {
email = c["email"].(string)
} else {
newClients = append(newClients, client)
}
}
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
err = s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
return db.Save(oldInbound).Error return db.Save(oldInbound).Error
} }
@@ -390,45 +418,35 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
return db.Save(oldInbound).Error return db.Save(oldInbound).Error
} }
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) { func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
if len(traffics) == 0 { if len(traffics) == 0 {
return nil return nil
} }
db := database.GetDB() // Update traffics in a single transaction
db = db.Model(model.Inbound{}) err := database.GetDB().Transaction(func(tx *gorm.DB) error {
tx := db.Begin() for _, traffic := range traffics {
defer func() { if traffic.IsInbound {
if err != nil { update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
tx.Rollback() Updates(map[string]interface{}{
} else { "up": gorm.Expr("up + ?", traffic.Up),
tx.Commit() "down": gorm.Expr("down + ?", traffic.Down),
} })
}() if update.Error != nil {
for _, traffic := range traffics { return update.Error
if traffic.IsInbound { }
err = tx.Where("tag = ?", traffic.Tag).
UpdateColumns(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down)}).Error
if err != nil {
return
} }
} }
} return nil
return })
return err
} }
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) { func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 { if len(traffics) == 0 {
return nil return nil
} }
traffics, err = s.adjustTraffics(traffics)
if err != nil {
return err
}
db := database.GetDB() db := database.GetDB()
db = db.Model(xray.ClientTraffic{})
tx := db.Begin() tx := db.Begin()
defer func() { defer func() {
@@ -439,7 +457,32 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
} }
}() }()
err = tx.Save(traffics).Error emails := make([]string, 0, len(traffics))
for _, traffic := range traffics {
emails = append(emails, traffic.Email)
}
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
if err != nil {
return err
}
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
if err != nil {
return err
}
for dbTraffic_index := range dbClientTraffics {
for traffic_index := range traffics {
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
break
}
}
}
err = tx.Save(dbClientTraffics).Error
if err != nil { if err != nil {
logger.Warning("AddClientTraffic update data ", err) logger.Warning("AddClientTraffic update data ", err)
} }
@@ -447,81 +490,56 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
return nil return nil
} }
func (s *InboundService) adjustTraffics(traffics []*xray.ClientTraffic) (full_traffics []*xray.ClientTraffic, err error) { func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) {
db := database.GetDB() inboundIds := make([]int, 0, len(dbClientTraffics))
dbInbound := db.Model(model.Inbound{}) for _, dbClientTraffic := range dbClientTraffics {
txInbound := dbInbound.Begin() if dbClientTraffic.ExpiryTime < 0 {
inboundIds = append(inboundIds, dbClientTraffic.InboundId)
defer func() {
if err != nil {
txInbound.Rollback()
} else {
txInbound.Commit()
} }
}()
for _, traffic := range traffics {
inbound := &model.Inbound{}
client_traffic := &xray.ClientTraffic{}
err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
client_traffic.Up += traffic.Up
client_traffic.Down += traffic.Down
err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
// get clients
clients, err := s.getClients(inbound)
needUpdate := false
if err == nil {
for client_index, client := range clients {
if traffic.Email == client.Email {
if client.ExpiryTime < 0 {
clients[client_index].ExpiryTime = (time.Now().Unix() * 1000) - client.ExpiryTime
needUpdate = true
}
client_traffic.ExpiryTime = client.ExpiryTime
client_traffic.Total = client.TotalGB
break
}
}
}
if needUpdate {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbound.Settings), &settings)
// Convert clients to []interface to update clients in settings
var clientsInterface []interface{}
for _, c := range clients {
clientsInterface = append(clientsInterface, interface{}(c))
}
settings["clients"] = clientsInterface
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
if err != nil {
return nil, err
}
}
full_traffics = append(full_traffics, client_traffic)
} }
return full_traffics, nil
if len(inboundIds) > 0 {
var inbounds []*model.Inbound
err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error
if err != nil {
return nil, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index := range dbClientTraffics {
if c["email"] == dbClientTraffics[traffic_index].Email {
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
}
err = tx.Save(inbounds).Error
if err != nil {
logger.Warning("AddClientTraffic update inbounds ", err)
logger.Error(inbounds)
}
}
return dbClientTraffics, nil
} }
func (s *InboundService) DisableInvalidInbounds() (int64, error) { func (s *InboundService) DisableInvalidInbounds() (int64, error) {
@@ -668,18 +686,18 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
return traffics, err return traffics, err
} }
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) { func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB() db := database.GetDB()
var traffics []*xray.ClientTraffic var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%"+email+"%").Find(&traffics).Error err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
logger.Warning(err) logger.Warning(err)
return nil, err return nil, err
} }
} }
return traffics, err return traffics[0], err
} }
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) { func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
@@ -730,3 +748,44 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
} }
return inbounds, nil return inbounds, nil
} }
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
// Add email='' if it is not exists
if _, ok := c["email"]; !ok {
c["email"] = ""
}
// Remove "flow": "xtls-rprx-direct"
if _, ok := c["flow"]; ok {
if c["flow"] == "xtls-rprx-direct" {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
}
db.Save(inbounds)
}

View File

@@ -66,13 +66,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
} }
} }
} }
header = fmt.Sprintf("upload=%d;download=%d", traffic.Up, traffic.Down) header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
if traffic.Total > 0 {
header = header + fmt.Sprintf(";total=%d", traffic.Total)
}
if traffic.ExpiryTime > 0 {
header = header + fmt.Sprintf(";expire=%d", traffic.ExpiryTime)
}
return result, header, nil return result, header, nil
} }

View File

@@ -404,38 +404,36 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
} }
func (t *Tgbot) searchClient(chatId int64, email string) { func (t *Tgbot) searchClient(chatId int64, email string) {
traffics, err := t.inboundService.GetClientTrafficByEmail(email) traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
msg := "❌ Something went wrong!" msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
if len(traffics) == 0 { if traffic == nil {
msg := "No result!" msg := "No result!"
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
for _, traffic := range traffics { expiryTime := ""
expiryTime := "" if traffic.ExpiryTime == 0 {
if traffic.ExpiryTime == 0 { expiryTime = "♾Unlimited"
expiryTime = "♾Unlimited" } else if traffic.ExpiryTime < 0 {
} else if traffic.ExpiryTime < 0 { expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) } else {
} else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
} }
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
} }
func (t *Tgbot) searchInbound(chatId int64, remark string) { func (t *Tgbot) searchInbound(chatId int64, remark string) {

View File

@@ -69,7 +69,6 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
} }
s.inboundService.DisableInvalidClients() s.inboundService.DisableInvalidClients()
s.inboundService.RemoveOrphanedTraffics()
inbounds, err := s.inboundService.GetAllInbounds() inbounds, err := s.inboundService.GetAllInbounds()
if err != nil { if err != nil {
@@ -124,7 +123,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
} }
settings["clients"] = final_clients settings["clients"] = final_clients
modifiedSettings, err := json.Marshal(settings) modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil { if err != nil {
return nil, err return nil, err
} }