package api import ( "fmt" "log" "net/http" "sort" "strings" "time" "github.com/pulse-lets-go/internal/config" "github.com/pulse-lets-go/internal/models" ) // handleGetTrunks — GET /api/trunks — список всех транков с опциональным фильтром. func (a *API) handleGetTrunks(w http.ResponseWriter, r *http.Request) { trunks, err := a.configManager.ReadTrunks() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения транков") return } // Фильтр по типу typeFilter := r.URL.Query().Get("type") if typeFilter != "" { filtered := make([]models.Trunk, 0) for _, t := range trunks { if string(t.Type) == typeFilter { filtered = append(filtered, t) } } trunks = filtered } // Сортировка по created_at sort.Slice(trunks, func(i, j int) bool { return trunks[i].CreatedAt.Before(trunks[j].CreatedAt) }) writeJSON(w, http.StatusOK, trunks) } // handleCreateTrunk — POST /api/trunks — создание нового транка. func (a *API) handleCreateTrunk(w http.ResponseWriter, r *http.Request) { var req models.CreateTrunkRequest if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON") return } if err := validateTrunk(req.Name, req.Type, req.Gateway); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } // Проверяем: fallback может быть только один trunks, err := a.configManager.ReadTrunks() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения транков") return } if req.Type == models.TrunkFallback { for _, t := range trunks { if t.Type == models.TrunkFallback { writeError(w, http.StatusConflict, "fallback-транк уже существует (должен быть ровно 1)") return } } } now := time.Now().UTC() trunk := models.Trunk{ ID: generateID("trk"), Name: req.Name, Type: req.Type, NodeID: req.NodeID, Gateway: req.Gateway, Codecs: req.Codecs, Context: req.Context, Enabled: req.Enabled, Description: req.Description, CreatedAt: now, UpdatedAt: now, } trunks = append(trunks, trunk) if err := a.configManager.SaveTrunks(trunks); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения транка") return } // ESL push: создаём gateway на FS для ingress-транка a.eslPushGatewayAdd(trunk) writeJSON(w, http.StatusCreated, trunk) } // handleUpdateTrunk — PUT /api/trunks/{id} — обновление транка. func (a *API) handleUpdateTrunk(w http.ResponseWriter, r *http.Request) { trunkID := r.PathValue("id") if trunkID == "" { writeError(w, http.StatusBadRequest, "не указан id транка") return } var req models.UpdateTrunkRequest if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON") return } trunks, err := a.configManager.ReadTrunks() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения транков") return } idx := findTrunkIndex(trunks, trunkID) if idx < 0 { writeError(w, http.StatusNotFound, "транк не найден") return } // Применяем частичные обновления t := &trunks[idx] if req.Name != nil { t.Name = *req.Name } if req.Type != nil { // Проверка fallback-уникальности if *req.Type == models.TrunkFallback && t.Type != models.TrunkFallback { for _, other := range trunks { if other.Type == models.TrunkFallback && other.ID != trunkID { writeError(w, http.StatusConflict, "fallback-транк уже существует") return } } } t.Type = *req.Type } if req.NodeID != nil { t.NodeID = *req.NodeID } if req.Gateway != nil { t.Gateway = *req.Gateway } if req.Codecs != nil { t.Codecs = req.Codecs } if req.Context != nil { t.Context = *req.Context } if req.Enabled != nil { t.Enabled = *req.Enabled } if req.Description != nil { t.Description = *req.Description } t.UpdatedAt = time.Now().UTC() if err := a.configManager.SaveTrunks(trunks); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения транка") return } // ESL push: обновляем gateway на FS a.eslPushGatewayUpdate(*t) writeJSON(w, http.StatusOK, t) } // handleDeleteTrunk — DELETE /api/trunks/{id} — удаление транка. func (a *API) handleDeleteTrunk(w http.ResponseWriter, r *http.Request) { trunkID := r.PathValue("id") if trunkID == "" { writeError(w, http.StatusBadRequest, "не указан id транка") return } trunks, err := a.configManager.ReadTrunks() if err != nil { writeError(w, http.StatusInternalServerError, "ошибка чтения транков") return } idx := findTrunkIndex(trunks, trunkID) if idx < 0 { writeError(w, http.StatusNotFound, "транк не найден") return } deletedTrunk := trunks[idx] trunks = append(trunks[:idx], trunks[idx+1:]...) // ESL push: удаляем gateway на FS a.eslPushGatewayDelete(deletedTrunk) if err := a.configManager.SaveTrunks(trunks); err != nil { writeError(w, http.StatusInternalServerError, "ошибка сохранения транков") return } writeJSON(w, http.StatusNoContent, nil) } // --- helpers --- func validateTrunk(name string, trunkType models.TrunkType, gateway string) error { if name == "" { return fmt.Errorf("имя транка обязательно") } if gateway == "" { return fmt.Errorf("gateway обязателен") } switch trunkType { case models.TrunkIngress, models.TrunkBalance, models.TrunkFallback: break default: return fmt.Errorf("некорректный тип транка: %s", trunkType) } return nil } func findTrunkIndex(trunks []models.Trunk, id string) int { for i := range trunks { if trunks[i].ID == id { return i } } return -1 } // --- ESL push helpers --- // eslPushGatewayAdd создаёт gateway на FS для ingress-транка. func (a *API) eslPushGatewayAdd(trunk models.Trunk) { if a.eslClient == nil || !a.eslClient.IsConnected() || trunk.Type != models.TrunkIngress { return } profile, name, proxy := a.gatewayParams(trunk) if err := a.eslClient.GatewayAdd(profile, name, proxy); err != nil { log.Printf("[esl] GatewayAdd %s: %v", name, err) } } // eslPushGatewayUpdate обновляет gateway на FS (удаляет старый + создаёт новый). func (a *API) eslPushGatewayUpdate(trunk models.Trunk) { if a.eslClient == nil || !a.eslClient.IsConnected() || trunk.Type != models.TrunkIngress { return } profile, name, proxy := a.gatewayParams(trunk) _ = a.eslClient.GatewayDelete(profile, name) // игнорируем ошибку — может не существовать if err := a.eslClient.GatewayAdd(profile, name, proxy); err != nil { log.Printf("[esl] GatewayUpdate %s: %v", name, err) } } // eslPushGatewayDelete удаляет gateway на FS для ingress-транка. func (a *API) eslPushGatewayDelete(trunk models.Trunk) { if a.eslClient == nil || !a.eslClient.IsConnected() || trunk.Type != models.TrunkIngress { return } profile, name, _ := a.gatewayParams(trunk) if err := a.eslClient.GatewayDelete(profile, name); err != nil { log.Printf("[esl] GatewayDelete %s: %v", name, err) } } // gatewayParams возвращает параметры для создания FS gateway на основе транка. func (a *API) gatewayParams(trunk models.Trunk) (profile, name, proxy string) { cfg := a.readConfig() profile = cfg.ESL.SofiaProfile name = fmt.Sprintf("%s-ingress-%s", cfg.ESL.GatewayPrefix, trunk.ID) proxy = extractHost(trunk.Gateway) return } // extractHost извлекает хост из SIP URI. // "sip:mts-gw.lan:5060" → "mts-gw.lan" func extractHost(gateway string) string { s := strings.TrimPrefix(gateway, "sip:") s = strings.TrimPrefix(s, "sips:") if idx := strings.LastIndex(s, ":"); idx > 0 { // Проверяем, порт ли это (все цифры после двоеточия) port := s[idx+1:] isNumeric := true for _, c := range port { if c < '0' || c > '9' { isNumeric = false break } } if isNumeric { s = s[:idx] } } return s } // readConfig возвращает текущий конфиг (для получения esl.sofia_profile). func (a *API) readConfig() *config.Config { cfg, _ := a.configManager.ReadConfig() if cfg == nil { return &config.Config{ESL: config.ESLConfig{SofiaProfile: "external"}} } return cfg }