2026-05-10 06:41:44 +00:00
package service
import (
"bytes"
"encoding/json"
"strings"
"time"
2026-05-10 14:56:03 +08:00
"github.com/admin8800/s-ui/database"
"github.com/admin8800/s-ui/database/model"
"github.com/admin8800/s-ui/logger"
"github.com/admin8800/s-ui/util"
"github.com/admin8800/s-ui/util/common"
2026-05-10 06:41:44 +00:00
"gorm.io/gorm"
)
type ClientService struct { }
func ( s * ClientService ) Get ( id string ) ( * [ ] model . Client , error ) {
if id == "" {
return s . GetAll ( )
}
return s . getById ( id )
}
func ( s * ClientService ) getById ( id string ) ( * [ ] model . Client , error ) {
db := database . GetDB ( )
var client [ ] model . Client
err := db . Model ( model . Client { } ) . Where ( "id in ?" , strings . Split ( id , "," ) ) . Scan ( & client ) . Error
if err != nil {
return nil , err
}
return & client , nil
}
func ( s * ClientService ) GetAll ( ) ( * [ ] model . Client , error ) {
db := database . GetDB ( )
var clients [ ] model . Client
err := db . Model ( model . Client { } ) .
Select ( "`id`, `enable`, `name`, `desc`, `group`, `inbounds`, `up`, `down`, `volume`, `expiry`" ) .
Scan ( & clients ) . Error
if err != nil {
return nil , err
}
return & clients , nil
}
func ( s * ClientService ) Save ( tx * gorm . DB , act string , data json . RawMessage , hostname string ) ( [ ] uint , error ) {
var err error
var inboundIds [ ] uint
switch act {
case "new" , "edit" :
var client model . Client
err = json . Unmarshal ( data , & client )
if err != nil {
return nil , err
}
err = s . updateLinksWithFixedInbounds ( tx , [ ] * model . Client { & client } , hostname )
if err != nil {
return nil , err
}
if act == "edit" {
// Find changed inbounds
inboundIds , err = s . findInboundsChanges ( tx , & client , false )
if err != nil {
return nil , err
}
} else {
err = json . Unmarshal ( client . Inbounds , & inboundIds )
if err != nil {
return nil , err
}
}
err = tx . Save ( & client ) . Error
if err != nil {
return nil , err
}
case "addbulk" :
var clients [ ] * model . Client
err = json . Unmarshal ( data , & clients )
if err != nil {
return nil , err
}
err = json . Unmarshal ( clients [ 0 ] . Inbounds , & inboundIds )
if err != nil {
return nil , err
}
err = s . updateLinksWithFixedInbounds ( tx , clients , hostname )
if err != nil {
return nil , err
}
err = tx . Save ( clients ) . Error
if err != nil {
return nil , err
}
case "editbulk" :
var clients [ ] * model . Client
err = json . Unmarshal ( data , & clients )
if err != nil {
return nil , err
}
for _ , client := range clients {
changedInboundIds , err := s . findInboundsChanges ( tx , client , true )
if err != nil {
return nil , err
}
if len ( changedInboundIds ) > 0 {
inboundIds = common . UnionUintArray ( inboundIds , changedInboundIds )
}
}
if len ( inboundIds ) > 0 {
err = s . updateLinksWithFixedInbounds ( tx , clients , hostname )
if err != nil {
return nil , err
}
}
err = tx . Save ( clients ) . Error
if err != nil {
return nil , err
}
case "delbulk" :
var ids [ ] uint
err = json . Unmarshal ( data , & ids )
if err != nil {
return nil , err
}
for _ , id := range ids {
var client model . Client
err = tx . Where ( "id = ?" , id ) . First ( & client ) . Error
if err != nil {
return nil , err
}
var clientInbounds [ ] uint
err = json . Unmarshal ( client . Inbounds , & clientInbounds )
if err != nil {
return nil , err
}
inboundIds = common . UnionUintArray ( inboundIds , clientInbounds )
}
err = tx . Where ( "id in ?" , ids ) . Delete ( model . Client { } ) . Error
if err != nil {
return nil , err
}
case "del" :
var id uint
err = json . Unmarshal ( data , & id )
if err != nil {
return nil , err
}
var client model . Client
err = tx . Where ( "id = ?" , id ) . First ( & client ) . Error
if err != nil {
return nil , err
}
err = json . Unmarshal ( client . Inbounds , & inboundIds )
if err != nil {
return nil , err
}
err = tx . Where ( "id = ?" , id ) . Delete ( model . Client { } ) . Error
if err != nil {
return nil , err
}
default :
return nil , common . NewErrorf ( "unknown action: %s" , act )
}
return inboundIds , nil
}
func ( s * ClientService ) updateLinksWithFixedInbounds ( tx * gorm . DB , clients [ ] * model . Client , hostname string ) error {
var err error
var inbounds [ ] model . Inbound
var inboundIds [ ] uint
err = json . Unmarshal ( clients [ 0 ] . Inbounds , & inboundIds )
if err != nil {
return err
}
// Zero inbounds means removing local links only
if len ( inboundIds ) > 0 {
err = tx . Model ( model . Inbound { } ) . Preload ( "Tls" ) . Where ( "id in ? and type in ?" , inboundIds , util . InboundTypeWithLink ) . Find ( & inbounds ) . Error
if err != nil {
return err
}
}
for index , client := range clients {
var clientLinks [ ] map [ string ] string
err = json . Unmarshal ( client . Links , & clientLinks )
if err != nil {
return err
}
newClientLinks := [ ] map [ string ] string { }
for _ , inbound := range inbounds {
newLinks := util . LinkGenerator ( client . Config , & inbound , hostname )
for _ , newLink := range newLinks {
newClientLinks = append ( newClientLinks , map [ string ] string {
"remark" : inbound . Tag ,
"type" : "local" ,
"uri" : newLink ,
} )
}
}
// Add non local links
for _ , clientLink := range clientLinks {
if clientLink [ "type" ] != "local" {
newClientLinks = append ( newClientLinks , clientLink )
}
}
clients [ index ] . Links , err = json . MarshalIndent ( newClientLinks , "" , " " )
if err != nil {
return err
}
}
return nil
}
func ( s * ClientService ) UpdateClientsOnInboundAdd ( tx * gorm . DB , initIds string , inboundId uint , hostname string ) error {
clientIds := strings . Split ( initIds , "," )
var clients [ ] model . Client
err := tx . Model ( model . Client { } ) . Where ( "id in ?" , clientIds ) . Find ( & clients ) . Error
if err != nil {
return err
}
var inbound model . Inbound
err = tx . Model ( model . Inbound { } ) . Preload ( "Tls" ) . Where ( "id = ?" , inboundId ) . Find ( & inbound ) . Error
if err != nil {
return err
}
for _ , client := range clients {
// Add inbounds
var clientInbounds [ ] uint
json . Unmarshal ( client . Inbounds , & clientInbounds )
clientInbounds = append ( clientInbounds , inboundId )
client . Inbounds , err = json . MarshalIndent ( clientInbounds , "" , " " )
if err != nil {
return err
}
// Add links
var clientLinks , newClientLinks [ ] map [ string ] string
json . Unmarshal ( client . Links , & clientLinks )
newLinks := util . LinkGenerator ( client . Config , & inbound , hostname )
for _ , newLink := range newLinks {
newClientLinks = append ( newClientLinks , map [ string ] string {
"remark" : inbound . Tag ,
"type" : "local" ,
"uri" : newLink ,
} )
}
for _ , clientLink := range clientLinks {
if clientLink [ "remark" ] != inbound . Tag {
newClientLinks = append ( newClientLinks , clientLink )
}
}
client . Links , err = json . MarshalIndent ( newClientLinks , "" , " " )
if err != nil {
return err
}
err = tx . Save ( & client ) . Error
if err != nil {
return err
}
}
return nil
}
func ( s * ClientService ) UpdateClientsOnInboundDelete ( tx * gorm . DB , id uint , tag string ) error {
var clientIds [ ] uint
err := tx . Raw ( "SELECT clients.id FROM clients, json_each(clients.inbounds) AS je WHERE je.value = ?" , id ) . Scan ( & clientIds ) . Error
if err != nil {
return err
}
if len ( clientIds ) == 0 {
return nil
}
var clients [ ] model . Client
err = tx . Model ( model . Client { } ) . Where ( "id IN ?" , clientIds ) . Find ( & clients ) . Error
if err != nil {
return err
}
for _ , client := range clients {
// Delete inbounds
var clientInbounds , newClientInbounds [ ] uint
json . Unmarshal ( client . Inbounds , & clientInbounds )
for _ , clientInbound := range clientInbounds {
if clientInbound != id {
newClientInbounds = append ( newClientInbounds , clientInbound )
}
}
client . Inbounds , err = json . MarshalIndent ( newClientInbounds , "" , " " )
if err != nil {
return err
}
// Delete links
var clientLinks , newClientLinks [ ] map [ string ] string
json . Unmarshal ( client . Links , & clientLinks )
for _ , clientLink := range clientLinks {
if clientLink [ "remark" ] != tag {
newClientLinks = append ( newClientLinks , clientLink )
}
}
client . Links , err = json . MarshalIndent ( newClientLinks , "" , " " )
if err != nil {
return err
}
err = tx . Save ( & client ) . Error
if err != nil {
return err
}
}
return nil
}
func ( s * ClientService ) UpdateLinksByInboundChange ( tx * gorm . DB , inbounds * [ ] model . Inbound , hostname string , oldTag string ) error {
var err error
for _ , inbound := range * inbounds {
var clientIds [ ] uint
err = tx . Raw ( "SELECT clients.id FROM clients, json_each(clients.inbounds) AS je WHERE je.value = ?" , inbound . Id ) . Scan ( & clientIds ) . Error
if err != nil {
return err
}
if len ( clientIds ) == 0 {
continue
}
var clients [ ] model . Client
err = tx . Model ( model . Client { } ) . Where ( "id IN ?" , clientIds ) . Find ( & clients ) . Error
if err != nil {
return err
}
for _ , client := range clients {
var clientLinks , newClientLinks [ ] map [ string ] string
json . Unmarshal ( client . Links , & clientLinks )
newLinks := util . LinkGenerator ( client . Config , & inbound , hostname )
for _ , newLink := range newLinks {
newClientLinks = append ( newClientLinks , map [ string ] string {
"remark" : inbound . Tag ,
"type" : "local" ,
"uri" : newLink ,
} )
}
for _ , clientLink := range clientLinks {
if clientLink [ "type" ] != "local" || ( clientLink [ "remark" ] != inbound . Tag && clientLink [ "remark" ] != oldTag ) {
newClientLinks = append ( newClientLinks , clientLink )
}
}
client . Links , err = json . MarshalIndent ( newClientLinks , "" , " " )
if err != nil {
return err
}
err = tx . Save ( & client ) . Error
if err != nil {
return err
}
}
}
return nil
}
func ( s * ClientService ) DepleteClients ( ) ( [ ] uint , error ) {
var err error
var clients [ ] model . Client
var changes [ ] model . Changes
var users [ ] string
var inboundIds [ ] uint
dt := time . Now ( ) . Unix ( )
db := database . GetDB ( )
tx := db . Begin ( )
defer func ( ) {
if err == nil {
tx . Commit ( )
if err1 := db . Exec ( "PRAGMA wal_checkpoint(FULL)" ) . Error ; err1 != nil {
logger . Error ( "Error checkpointing WAL: " , err1 . Error ( ) )
}
} else {
tx . Rollback ( )
}
} ( )
// Reset clients
inboundIds , err = s . ResetClients ( tx , dt )
if err != nil {
return nil , err
}
// Deplete clients
err = tx . Model ( model . Client { } ) . Where ( "enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))" , dt ) . Scan ( & clients ) . Error
if err != nil {
return nil , err
}
for _ , client := range clients {
logger . Debug ( "Client " , client . Name , " is going to be disabled" )
users = append ( users , client . Name )
var userInbounds [ ] uint
json . Unmarshal ( client . Inbounds , & userInbounds )
// Find changed inbounds
inboundIds = common . UnionUintArray ( inboundIds , userInbounds )
changes = append ( changes , model . Changes {
DateTime : dt ,
Actor : "DepleteJob" ,
Key : "clients" ,
Action : "disable" ,
Obj : json . RawMessage ( "\"" + client . Name + "\"" ) ,
} )
}
// Save changes
if len ( changes ) > 0 {
err = tx . Model ( model . Client { } ) . Where ( "enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))" , dt ) . Update ( "enable" , false ) . Error
if err != nil {
return nil , err
}
err = tx . Model ( model . Changes { } ) . Create ( & changes ) . Error
if err != nil {
return nil , err
}
LastUpdate = dt
}
return inboundIds , nil
}
func ( s * ClientService ) ResetClients ( tx * gorm . DB , dt int64 ) ( [ ] uint , error ) {
var err error
var resetClients , allClients [ ] * model . Client
var changes [ ] model . Changes
var inboundIds [ ] uint
// Set delay start without periodic reset
err = tx . Model ( model . Client { } ) .
Where ( "enable = true AND delay_start = true AND auto_reset = false AND (Up + Down) > 0" ) . Find ( & resetClients ) . Error
if err != nil {
return nil , err
}
for _ , client := range resetClients {
client . Expiry = dt + ( int64 ( client . ResetDays ) * 86400 )
client . DelayStart = false
changes = append ( changes , model . Changes {
DateTime : dt ,
Actor : "ResetJob" ,
Key : "clients" ,
Action : "reset" ,
Obj : json . RawMessage ( "\"" + client . Name + "\"" ) ,
} )
}
allClients = append ( allClients , resetClients ... )
// Set delay start with periodic reset
err = tx . Model ( model . Client { } ) .
Where ( "enable = true AND delay_start = true AND auto_reset = true AND (Up + Down) > 0" ) . Find ( & resetClients ) . Error
if err != nil {
return nil , err
}
for _ , client := range resetClients {
client . NextReset = dt + ( int64 ( client . ResetDays ) * 86400 )
client . DelayStart = false
changes = append ( changes , model . Changes {
DateTime : dt ,
Actor : "ResetJob" ,
Key : "clients" ,
Action : "reset" ,
Obj : json . RawMessage ( "\"" + client . Name + "\"" ) ,
} )
}
allClients = append ( allClients , resetClients ... )
// Set periodic reset
err = tx . Model ( model . Client { } ) .
Where ( "delay_start = false AND auto_reset = true AND next_reset < ?" , dt ) . Find ( & resetClients ) . Error
if err != nil {
return nil , err
}
for _ , client := range resetClients {
client . NextReset = dt + ( int64 ( client . ResetDays ) * 86400 )
client . TotalUp += client . Up
client . TotalDown += client . Down
client . Up = 0
client . Down = 0
if ! client . Enable {
client . Enable = true
var clientInboundIds [ ] uint
json . Unmarshal ( client . Inbounds , & clientInboundIds )
inboundIds = common . UnionUintArray ( inboundIds , clientInboundIds )
}
}
allClients = append ( allClients , resetClients ... )
// Save clients
if len ( allClients ) > 0 {
err = tx . Save ( allClients ) . Error
if err != nil {
return nil , err
}
}
// Save changes
if len ( changes ) > 0 {
err = tx . Model ( model . Changes { } ) . Create ( & changes ) . Error
if err != nil {
return nil , err
}
LastUpdate = dt
}
return inboundIds , nil
}
func ( s * ClientService ) findInboundsChanges ( tx * gorm . DB , client * model . Client , fillOmitted bool ) ( [ ] uint , error ) {
var err error
var oldClient model . Client
var oldInboundIds , newInboundIds [ ] uint
err = tx . Model ( model . Client { } ) . Where ( "id = ?" , client . Id ) . First ( & oldClient ) . Error
if err != nil {
return nil , err
}
if fillOmitted {
client . Links = oldClient . Links
client . Config = oldClient . Config
}
err = json . Unmarshal ( oldClient . Inbounds , & oldInboundIds )
if err != nil {
return nil , err
}
err = json . Unmarshal ( client . Inbounds , & newInboundIds )
if err != nil {
return nil , err
}
// Check client.Config changes
if ! bytes . Equal ( oldClient . Config , client . Config ) ||
oldClient . Name != client . Name ||
oldClient . Enable != client . Enable {
return common . UnionUintArray ( oldInboundIds , newInboundIds ) , nil
}
// Check client.Inbounds changes
diffInbounds := common . DiffUintArray ( oldInboundIds , newInboundIds )
return diffInbounds , nil
}