307 lines
8.6 KiB
Go
307 lines
8.6 KiB
Go
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) {
|
||
profile = a.readConfig().ESL.SofiaProfile
|
||
name = fmt.Sprintf("pulse-ingress-%s", 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
|
||
}
|