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
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"strings"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||||
ldaputil "github.com/mhsanaei/3x-ui/v3/util/ldap"
|
ldaputil "github.com/mhsanaei/3x-ui/v3/util/ldap"
|
||||||
"github.com/mhsanaei/3x-ui/v3/web/service"
|
"github.com/mhsanaei/3x-ui/v3/web/service"
|
||||||
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||||
|
|
@ -20,6 +17,7 @@ var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||||
type LdapSyncJob struct {
|
type LdapSyncJob struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
|
clientService service.ClientService
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,20 +133,31 @@ func (j *LdapSyncJob) Run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Execute batch create ---
|
|
||||||
for tag, newClients := range clientsToCreate {
|
for tag, newClients := range clientsToCreate {
|
||||||
if len(newClients) == 0 {
|
if len(newClients) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
payload := &model.Inbound{Id: inboundMap[tag].Id}
|
ib := inboundMap[tag]
|
||||||
payload.Settings = j.clientsToJSON(newClients)
|
created := 0
|
||||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
restartNeeded := false
|
||||||
logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
|
for _, c := range newClients {
|
||||||
} else {
|
nr, err := j.clientService.CreateOne(&j.inboundService, ib.Id, c)
|
||||||
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
|
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()
|
j.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Execute enable/disable batch ---
|
// --- Execute enable/disable batch ---
|
||||||
for tag, emails := range clientsToEnable {
|
for tag, emails := range clientsToEnable {
|
||||||
|
|
@ -206,35 +215,32 @@ func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExp
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// batchSetEnable enables/disables clients in batch through a single call
|
|
||||||
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
||||||
if len(emails) == 0 {
|
if len(emails) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
restartNeeded := false
|
||||||
// Prepare JSON for mass update
|
changed := 0
|
||||||
clients := make([]model.Client, 0, len(emails))
|
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
clients = append(clients, model.Client{
|
ok, needRestart, err := j.inboundService.SetClientEnableByEmail(email, enable)
|
||||||
Email: email,
|
if err != nil {
|
||||||
Enable: enable,
|
logger.Warningf("Batch set enable failed for %s in inbound %s: %v", email, ib.Tag, err)
|
||||||
})
|
continue
|
||||||
}
|
}
|
||||||
|
if ok {
|
||||||
payload := &model.Inbound{
|
changed++
|
||||||
Id: ib.Id,
|
|
||||||
Settings: j.clientsToJSON(clients),
|
|
||||||
}
|
}
|
||||||
|
if needRestart {
|
||||||
// Use a single AddInboundClient call to update enable
|
restartNeeded = true
|
||||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
|
||||||
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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()
|
j.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
||||||
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
||||||
|
|
@ -269,90 +275,28 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete in batches
|
|
||||||
for i := 0; i < len(toDelete); i += batchSize {
|
for i := 0; i < len(toDelete); i += batchSize {
|
||||||
end := min(i+batchSize, len(toDelete))
|
end := min(i+batchSize, len(toDelete))
|
||||||
batch := toDelete[i:end]
|
batch := toDelete[i:end]
|
||||||
|
|
||||||
for _, c := range batch {
|
for _, c := range batch {
|
||||||
var clientKey string
|
nr, err := j.clientService.DetachByEmail(&j.inboundService, ib.Id, c.Email)
|
||||||
switch ib.Protocol {
|
if err != nil {
|
||||||
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 {
|
|
||||||
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
|
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
|
||||||
c.Email, ib.Id, ib.Tag, err)
|
c.Email, ib.Id, ib.Tag, err)
|
||||||
} else {
|
continue
|
||||||
|
}
|
||||||
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
||||||
c.Email, ib.Id, ib.Tag)
|
c.Email, ib.Id, ib.Tag)
|
||||||
// do not restart here
|
if nr {
|
||||||
restartNeeded = true
|
restartNeeded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One time after all batches
|
|
||||||
if restartNeeded {
|
if restartNeeded {
|
||||||
j.xrayService.SetToNeedRestart()
|
j.xrayService.SetToNeedRestart()
|
||||||
logger.Info("Xray restart scheduled after batch deletion")
|
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
|
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) {
|
func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email string) (bool, error) {
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return false, common.NewError("client email is required")
|
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.
|
// It handles bot commands, user interactions, and status reporting via Telegram.
|
||||||
type Tgbot struct {
|
type Tgbot struct {
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
|
clientService ClientService
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
serverService ServerService
|
serverService ServerService
|
||||||
xrayService XrayService
|
xrayService XrayService
|
||||||
|
|
@ -2209,27 +2210,43 @@ func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
||||||
return jsonString, nil
|
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) {
|
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
||||||
|
|
||||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("getIboundClients run failed:", err)
|
logger.Warning("getIboundClients run failed:", err)
|
||||||
return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
|
tgIDInt, _ := strconv.ParseInt(client_TgID, 10, 64)
|
||||||
if err != nil {
|
client := model.Client{
|
||||||
logger.Warning("BuildJSONForProtocol run failed:", err)
|
Email: client_Email,
|
||||||
return false, errors.New("failed to build JSON for protocol")
|
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{
|
switch inbound.Protocol {
|
||||||
Id: receiver_inbound_ID,
|
case model.VMESS:
|
||||||
Settings: jsonString,
|
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.
|
// checkAdmin checks if the given Telegram ID is an admin.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue