mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0fe48124c9
commit
960bd3c832
3 changed files with 91 additions and 112 deletions
|
|
@ -1,18 +1,15 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"strings"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
ldaputil "github.com/mhsanaei/3x-ui/v3/util/ldap"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/service"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||
|
|
@ -20,6 +17,7 @@ var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
|||
type LdapSyncJob struct {
|
||||
settingService service.SettingService
|
||||
inboundService service.InboundService
|
||||
clientService service.ClientService
|
||||
xrayService service.XrayService
|
||||
}
|
||||
|
||||
|
|
@ -135,20 +133,31 @@ func (j *LdapSyncJob) Run() {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Execute batch create ---
|
||||
for tag, newClients := range clientsToCreate {
|
||||
if len(newClients) == 0 {
|
||||
continue
|
||||
}
|
||||
payload := &model.Inbound{Id: inboundMap[tag].Id}
|
||||
payload.Settings = j.clientsToJSON(newClients)
|
||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||
logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
|
||||
} else {
|
||||
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
|
||||
ib := inboundMap[tag]
|
||||
created := 0
|
||||
restartNeeded := false
|
||||
for _, c := range newClients {
|
||||
nr, err := j.clientService.CreateOne(&j.inboundService, ib.Id, c)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to add client %s for tag %s: %v", c.Email, tag, err)
|
||||
continue
|
||||
}
|
||||
created++
|
||||
if nr {
|
||||
restartNeeded = true
|
||||
}
|
||||
}
|
||||
if created > 0 {
|
||||
logger.Infof("LDAP auto-create: %d clients for %s", created, tag)
|
||||
if restartNeeded {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Execute enable/disable batch ---
|
||||
for tag, emails := range clientsToEnable {
|
||||
|
|
@ -206,35 +215,32 @@ func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExp
|
|||
return c
|
||||
}
|
||||
|
||||
// batchSetEnable enables/disables clients in batch through a single call
|
||||
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
||||
if len(emails) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare JSON for mass update
|
||||
clients := make([]model.Client, 0, len(emails))
|
||||
restartNeeded := false
|
||||
changed := 0
|
||||
for _, email := range emails {
|
||||
clients = append(clients, model.Client{
|
||||
Email: email,
|
||||
Enable: enable,
|
||||
})
|
||||
ok, needRestart, err := j.inboundService.SetClientEnableByEmail(email, enable)
|
||||
if err != nil {
|
||||
logger.Warningf("Batch set enable failed for %s in inbound %s: %v", email, ib.Tag, err)
|
||||
continue
|
||||
}
|
||||
|
||||
payload := &model.Inbound{
|
||||
Id: ib.Id,
|
||||
Settings: j.clientsToJSON(clients),
|
||||
if ok {
|
||||
changed++
|
||||
}
|
||||
|
||||
// Use a single AddInboundClient call to update enable
|
||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
|
||||
return
|
||||
if needRestart {
|
||||
restartNeeded = true
|
||||
}
|
||||
|
||||
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
|
||||
}
|
||||
if changed > 0 {
|
||||
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, changed, ib.Tag)
|
||||
}
|
||||
if restartNeeded {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
||||
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
||||
|
|
@ -269,90 +275,28 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s
|
|||
continue
|
||||
}
|
||||
|
||||
// Delete in batches
|
||||
for i := 0; i < len(toDelete); i += batchSize {
|
||||
end := min(i+batchSize, len(toDelete))
|
||||
batch := toDelete[i:end]
|
||||
|
||||
for _, c := range batch {
|
||||
var clientKey string
|
||||
switch ib.Protocol {
|
||||
case model.Trojan:
|
||||
clientKey = c.Password
|
||||
case model.Shadowsocks:
|
||||
clientKey = c.Email
|
||||
default: // vless/vmess
|
||||
clientKey = c.ID
|
||||
}
|
||||
|
||||
if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {
|
||||
nr, err := j.clientService.DetachByEmail(&j.inboundService, ib.Id, c.Email)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
|
||||
c.Email, ib.Id, ib.Tag, err)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
||||
c.Email, ib.Id, ib.Tag)
|
||||
// do not restart here
|
||||
if nr {
|
||||
restartNeeded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One time after all batches
|
||||
if restartNeeded {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
logger.Info("Xray restart scheduled after batch deletion")
|
||||
}
|
||||
}
|
||||
|
||||
// clientsToJSON serializes an array of clients to JSON
|
||||
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
|
||||
b := strings.Builder{}
|
||||
b.WriteString("{\"clients\":[")
|
||||
for i, c := range clients {
|
||||
if i > 0 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString(j.clientToJSON(c))
|
||||
}
|
||||
b.WriteString("]}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// clientToJSON serializes minimal client fields to JSON object string without extra deps
|
||||
func (j *LdapSyncJob) clientToJSON(c model.Client) string {
|
||||
// construct minimal JSON manually to avoid importing json for simple case
|
||||
b := strings.Builder{}
|
||||
b.WriteString("{")
|
||||
if c.ID != "" {
|
||||
b.WriteString("\"id\":\"")
|
||||
b.WriteString(c.ID)
|
||||
b.WriteString("\",")
|
||||
}
|
||||
if c.Password != "" {
|
||||
b.WriteString("\"password\":\"")
|
||||
b.WriteString(c.Password)
|
||||
b.WriteString("\",")
|
||||
}
|
||||
b.WriteString("\"email\":\"")
|
||||
b.WriteString(c.Email)
|
||||
b.WriteString("\",")
|
||||
b.WriteString("\"enable\":")
|
||||
if c.Enable {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
b.WriteString(",")
|
||||
b.WriteString("\"limitIp\":")
|
||||
b.WriteString(strconv.Itoa(c.LimitIP))
|
||||
b.WriteString(",")
|
||||
b.WriteString("\"totalGB\":")
|
||||
b.WriteString(strconv.FormatInt(c.TotalGB, 10))
|
||||
if c.ExpiryTime > 0 {
|
||||
b.WriteString(",\"expiryTime\":")
|
||||
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -475,6 +475,24 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []
|
|||
return needRestart, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) CreateOne(inboundSvc *InboundService, inboundId int, client model.Client) (bool, error) {
|
||||
return s.Create(inboundSvc, &ClientCreatePayload{
|
||||
Client: client,
|
||||
InboundIds: []int{inboundId},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ClientService) DetachByEmail(inboundSvc *InboundService, inboundId int, email string) (bool, error) {
|
||||
if email == "" {
|
||||
return false, common.NewError("client email is required")
|
||||
}
|
||||
rec, err := s.GetRecordByEmail(nil, email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.Detach(inboundSvc, rec.Id, []int{inboundId})
|
||||
}
|
||||
|
||||
func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email string) (bool, error) {
|
||||
if email == "" {
|
||||
return false, common.NewError("client email is required")
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ type LoginAttempt struct {
|
|||
// It handles bot commands, user interactions, and status reporting via Telegram.
|
||||
type Tgbot struct {
|
||||
inboundService InboundService
|
||||
clientService ClientService
|
||||
settingService SettingService
|
||||
serverService ServerService
|
||||
xrayService XrayService
|
||||
|
|
@ -2209,27 +2210,43 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
|||
return jsonString, nil
|
||||
}
|
||||
|
||||
// SubmitAddClient submits the client addition request to the inbound service.
|
||||
// SubmitAddClient submits the client addition request to the client service.
|
||||
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
||||
|
||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
if err != nil {
|
||||
logger.Warning("getIboundClients run failed:", err)
|
||||
return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||
}
|
||||
|
||||
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
|
||||
if err != nil {
|
||||
logger.Warning("BuildJSONForProtocol run failed:", err)
|
||||
return false, errors.New("failed to build JSON for protocol")
|
||||
tgIDInt, _ := strconv.ParseInt(client_TgID, 10, 64)
|
||||
client := model.Client{
|
||||
Email: client_Email,
|
||||
Enable: client_Enable,
|
||||
LimitIP: client_LimitIP,
|
||||
TotalGB: client_TotalGB,
|
||||
ExpiryTime: client_ExpiryTime,
|
||||
SubID: client_SubID,
|
||||
Comment: client_Comment,
|
||||
Reset: client_Reset,
|
||||
TgID: tgIDInt,
|
||||
}
|
||||
|
||||
newInbound := &model.Inbound{
|
||||
Id: receiver_inbound_ID,
|
||||
Settings: jsonString,
|
||||
switch inbound.Protocol {
|
||||
case model.VMESS:
|
||||
client.ID = client_Id
|
||||
client.Security = client_Security
|
||||
case model.VLESS:
|
||||
client.ID = client_Id
|
||||
client.Flow = client_Flow
|
||||
case model.Trojan:
|
||||
client.Password = client_TrPassword
|
||||
case model.Shadowsocks:
|
||||
client.Password = client_ShPassword
|
||||
default:
|
||||
return false, errors.New("unknown protocol")
|
||||
}
|
||||
|
||||
return t.inboundService.AddInboundClient(newInbound)
|
||||
return t.clientService.CreateOne(&t.inboundService, receiver_inbound_ID, client)
|
||||
}
|
||||
|
||||
// checkAdmin checks if the given Telegram ID is an admin.
|
||||
|
|
|
|||
Loading…
Reference in a new issue