Browse Source

auto renew feature #1286

Alireza Ahmadi 1 year ago
parent
commit
a8b7063647

+ 1 - 0
database/model/model.go

@@ -83,4 +83,5 @@ type Client struct {
 	Enable     bool   `json:"enable" form:"enable"`
 	TgID       string `json:"tgId" form:"tgId"`
 	SubID      string `json:"subId" form:"subId"`
+	Reset      int    `json:"reset" form:"reset"`
 }

+ 14 - 4
web/assets/js/model/xray.js

@@ -1783,7 +1783,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
     }
 };
 Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
-    constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
+    constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
         super();
         this.id = id;
         this.email = email;
@@ -1793,6 +1793,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
         this.enable = enable;
         this.tgId = tgId;
         this.subId = subId;
+        this.reset = reset;
     }
 
     static fromJson(json={}) {
@@ -1805,6 +1806,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
             json.enable,
             json.tgId,
             json.subId,
+            json.reset,
         );
     }
     get _expiryTime() {
@@ -1873,7 +1875,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
 
 };
 Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
-    constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
+    constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
         super();
         this.id = id;
         this.flow = flow;
@@ -1884,6 +1886,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
         this.enable = enable;
         this.tgId = tgId;
         this.subId = subId;
+        this.reset = reset;
     }
 
     static fromJson(json={}) {
@@ -1897,6 +1900,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
             json.enable,
             json.tgId,
             json.subId,
+            json.reset,
         );
       }
 
@@ -1996,7 +2000,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
     }
 };
 Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
-    constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
+    constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
         super();
         this.password = password;
         this.flow = flow;
@@ -2007,6 +2011,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
         this.enable = enable;
         this.tgId = tgId;
         this.subId = subId;
+        this.reset = reset;
     }
 
     toJson() {
@@ -2020,6 +2025,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             enable: this.enable,
             tgId: this.tgId,
             subId: this.subId,
+            reset: this.reset,
         };
     }
 
@@ -2034,6 +2040,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             json.enable,
             json.tgId,
             json.subId,
+            json.reset,
         );
     }
 
@@ -2138,7 +2145,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
 };
 
 Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
-    constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
+    constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
         super();
         this.method = method;
         this.password = password;
@@ -2149,6 +2156,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
         this.enable = enable;
         this.tgId = tgId;
         this.subId = subId;
+        this.reset = reset;
     }
 
     toJson() {
@@ -2162,6 +2170,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
             enable: this.enable,
             tgId: this.tgId,
             subId: this.subId,
+            reset: this.reset,
         };
     }
 
@@ -2176,6 +2185,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
             json.enable,
             json.tgId,
             json.subId,
+            json.reset,
         );
     }
 

+ 15 - 0
web/html/xui/client_bulk_modal.html

@@ -117,6 +117,18 @@
                            :dropdown-class-name="themeSwitcher.darkCardClass"
                            v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
         </a-form-item>
+        <a-form-item v-if="clientsBulkModal.expiryTime != 0">
+            <span slot="label">
+                <span>{{ i18n "pages.client.renew" }}</span>
+                <a-tooltip>
+                    <template slot="title">
+                        <span>{{ i18n "pages.client.renewDesc" }}</span>
+                    </template>
+                    <a-icon type="question-circle" theme="filled"></a-icon>
+                </a-tooltip>
+            </span>
+            <a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
+        </a-form-item>
     </a-form>
 </a-modal>
 <script>
@@ -142,6 +154,7 @@
         tgId: "",
         flow: "",
         delayedStart: false,
+        reset: 0,
         ok() {
             clients = [];
             method = clientsBulkModal.emailMethod;
@@ -170,6 +183,7 @@
                 if (clientsBulkModal.inbound.xtls) {
                     newClient.flow = clientsBulkModal.flow;
                 }
+                newClient.reset = clientsBulkModal.reset;
                 clients.push(newClient);
             }
             ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
@@ -199,6 +213,7 @@
             this.dbInbound = new DBInbound(dbInbound);
             this.inbound = dbInbound.toInbound();
             this.delayedStart = false;
+            this.reset = 0;
         },
         newClient(protocol) {
             switch (protocol) {

+ 6 - 0
web/html/xui/client_modal.html

@@ -115,6 +115,12 @@
             get statsColor() {
                 return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
             },
+            get delayedStart() {
+                return this.clientModal.delayedStart;
+            },
+            set delayedStart(value) {
+                this.clientModal.delayedStart = value;
+            },
             get delayedExpireDays() {
                 return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
             },

+ 125 - 23
web/service/inbound.go

@@ -263,7 +263,18 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 
 	tag := oldInbound.Tag
 
-	err = s.updateClientTraffics(oldInbound, inbound)
+	db := database.GetDB()
+	tx := db.Begin()
+
+	defer func() {
+		if err != nil {
+			tx.Rollback()
+		} else {
+			tx.Commit()
+		}
+	}()
+
+	err = s.updateClientTraffics(tx, oldInbound, inbound)
 	if err != nil {
 		return inbound, false, err
 	}
@@ -304,11 +315,10 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
 	}
 	s.xrayApi.Close()
 
-	db := database.GetDB()
-	return inbound, needRestart, db.Save(oldInbound).Error
+	return inbound, needRestart, tx.Save(oldInbound).Error
 }
 
-func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbound *model.Inbound) error {
+func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error {
 	oldClients, err := s.GetClients(oldInbound)
 	if err != nil {
 		return err
@@ -318,17 +328,6 @@ func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbo
 		return err
 	}
 
-	db := database.GetDB()
-	tx := db.Begin()
-
-	defer func() {
-		if err != nil {
-			tx.Rollback()
-		} else {
-			tx.Commit()
-		}
-	}()
-
 	var emailExists bool
 
 	for _, oldClient := range oldClients {
@@ -601,7 +600,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 
 	if len(clients[0].Email) > 0 {
 		if len(oldEmail) > 0 {
-			err = s.UpdateClientStat(oldEmail, &clients[0])
+			err = s.UpdateClientStat(tx, oldEmail, &clients[0])
 			if err != nil {
 				return false, err
 			}
@@ -676,6 +675,13 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
 		return err, false
 	}
 
+	needRestart0, count, err := s.autoRenewClients(tx)
+	if err != nil {
+		logger.Warning("Error in renew clients:", err)
+	} else if count > 0 {
+		logger.Debugf("%v clients renewed", count)
+	}
+
 	needRestart1, count, err := s.disableInvalidClients(tx)
 	if err != nil {
 		logger.Warning("Error in disabling invalid clients:", err)
@@ -689,7 +695,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
 	} else if count > 0 {
 		logger.Debugf("%v inbounds disabled", count)
 	}
-	return nil, (needRestart1 || needRestart2)
+	return nil, (needRestart0 || needRestart1 || needRestart2)
 }
 
 func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@@ -823,6 +829,102 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
 	return dbClientTraffics, nil
 }
 
+func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
+	// check for time expired
+	var traffics []*xray.ClientTraffic
+	now := time.Now().Unix() * 1000
+	var err, err1 error
+
+	err = tx.Model(xray.ClientTraffic{}).Where("reset > 0 and expiry_time > 0 and expiry_time <= ?", now).Find(&traffics).Error
+	if err != nil {
+		return false, 0, err
+	}
+	// return if there is no client to renew
+	if len(traffics) == 0 {
+		return false, 0, nil
+	}
+
+	var inbound_ids []int
+	var inbounds []*model.Inbound
+	needRestart := false
+	var clientsToAdd []struct {
+		protocol string
+		tag      string
+		client   map[string]interface{}
+	}
+
+	for _, traffic := range traffics {
+		inbound_ids = append(inbound_ids, traffic.InboundId)
+	}
+	err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error
+	if err != nil {
+		return false, 0, err
+	}
+	for inbound_index := range inbounds {
+		settings := map[string]interface{}{}
+		json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
+		clients := settings["clients"].([]interface{})
+		for client_index := range clients {
+			c := clients[client_index].(map[string]interface{})
+			for traffic_index, traffic := range traffics {
+				if traffic.Email == c["email"].(string) {
+					newExpiryTime := traffic.ExpiryTime
+					for newExpiryTime < now {
+						newExpiryTime += (int64(traffic.Reset) * 86400000)
+					}
+					c["expiryTime"] = newExpiryTime
+					traffics[traffic_index].ExpiryTime = newExpiryTime
+					traffics[traffic_index].Down = 0
+					traffics[traffic_index].Up = 0
+					if !traffic.Enable {
+						traffics[traffic_index].Enable = true
+						clientsToAdd = append(clientsToAdd,
+							struct {
+								protocol string
+								tag      string
+								client   map[string]interface{}
+							}{
+								protocol: string(inbounds[inbound_index].Protocol),
+								tag:      inbounds[inbound_index].Tag,
+								client:   c,
+							})
+					}
+					clients[client_index] = interface{}(c)
+					break
+				}
+			}
+		}
+		settings["clients"] = clients
+		newSettings, err := json.MarshalIndent(settings, "", "  ")
+		if err != nil {
+			return false, 0, err
+		}
+		inbounds[inbound_index].Settings = string(newSettings)
+	}
+	err = tx.Save(inbounds).Error
+	if err != nil {
+		return false, 0, err
+	}
+	err = tx.Save(traffics).Error
+	if err != nil {
+		return false, 0, err
+	}
+	if p != nil {
+		err1 = s.xrayApi.Init(p.GetAPIPort())
+		if err1 != nil {
+			return true, int64(len(traffics)), nil
+		}
+		for _, clientToAdd := range clientsToAdd {
+			err1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client)
+			if err1 != nil {
+				needRestart = true
+			}
+		}
+		s.xrayApi.Close()
+	}
+	return needRestart, int64(len(traffics)), nil
+}
+
 func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {
 	now := time.Now().Unix() * 1000
 	needRestart := false
@@ -916,6 +1018,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
 	clientTraffic.Enable = true
 	clientTraffic.Up = 0
 	clientTraffic.Down = 0
+	clientTraffic.Reset = client.Reset
 	result := tx.Create(&clientTraffic)
 	err := result.Error
 	if err != nil {
@@ -924,16 +1027,15 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
 	return nil
 }
 
-func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
-	db := database.GetDB()
-
-	result := db.Model(xray.ClientTraffic{}).
+func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
+	result := tx.Model(xray.ClientTraffic{}).
 		Where("email = ?", email).
 		Updates(map[string]interface{}{
 			"enable":      true,
 			"email":       client.Email,
 			"total":       client.TotalGB,
-			"expiry_time": client.ExpiryTime})
+			"expiry_time": client.ExpiryTime,
+			"reset":       client.Reset})
 	err := result.Error
 	if err != nil {
 		return err
@@ -1429,7 +1531,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
 		}
 	}()
 
-	whereText := "inbound_id "
+	whereText := "reset = 0 and inbound_id "
 	if id < 0 {
 		whereText += "> ?"
 	} else {

+ 1 - 0
xray/client_traffic.go

@@ -9,4 +9,5 @@ type ClientTraffic struct {
 	Down       int64  `json:"down" form:"down"`
 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`
 	Total      int64  `json:"total" form:"total"`
+	Reset      int    `json:"reset" form:"reset" gorm:"default:0"`
 }