package controller import ( "errors" "fmt" "html/template" "io" "net" "net/http" "strconv" "time" "github.com/labstack/echo/v4" "github.com/libretro/netplay-lobby-server-go/domain" "github.com/libretro/netplay-lobby-server-go/model/entity" ) // Template 抽象模板渲染。 type Template struct { templates *template.Template } // Render 实现 echo 模板渲染接口 func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } // SessionDomain 接口,用于将控制器逻辑与领域代码解耦。 type SessionDomain interface { Add(request *domain.AddSessionRequest, ip net.IP) (*entity.Session, error) Get(roomID int32) (*entity.Session, error) List() ([]entity.Session, error) GetTunnel(tunnelName string) *domain.MitmInfo PurgeOld() error } // ListSessionsResponse 是一个自定义的 DTO,用于向后兼容。 type SessionsResponse struct { Fields entity.Session `json:"fields"` } // SessionController 处理所有与会话相关的请求 type SessionController struct { sessionDomain SessionDomain } // NewSessionController 返回一个新的会话控制器 func NewSessionController(sessionDomain SessionDomain) *SessionController { return &SessionController{sessionDomain} } // RegisterRoutes 在 echo 框架实例中注册所有控制器路由。 func (c *SessionController) RegisterRoutes(server *echo.Echo) { server.POST("/add", c.Add) server.POST("/add/", c.Add) // 旧路径 server.GET("/list", c.List) server.GET("/list/", c.List) // 旧路径 server.GET("/tunnel", c.Tunnel) server.GET("/tunnel/", c.Tunnel) // 旧路径 server.GET("/", c.Index) server.GET("/:roomID", c.Get) server.GET("/:roomID/", c.Get) // 旧路径 } // PrerenderTemplates 预渲染所有模板 func (c *SessionController) PrerenderTemplates(server *echo.Echo, filePattern string) error { templates, err := template.New("").Funcs( template.FuncMap{ "prettyBool": func(b bool) string { if b { return "Yes" } return "No" }, "prettyDate": func(d time.Time) string { utc, _ := time.LoadLocation("UTC") return d.In(utc).Format(time.RFC822) }, }, ).ParseGlob(filePattern) if err != nil { return fmt.Errorf("无法解析模板: %w", err) } t := &Template{ templates: templates, } server.Renderer = t return nil } // Index 处理器 // GET / func (c *SessionController) Index(ctx echo.Context) error { logger := ctx.Logger() sessions, err := c.sessionDomain.List() if err != nil { logger.Errorf("无法渲染会话列表: %v", err) return ctx.NoContent(http.StatusInternalServerError) } return ctx.Render(http.StatusOK, "index.html", sessions) } // Get 处理器 // GET /:roomID func (c *SessionController) Get(ctx echo.Context) error { logger := ctx.Logger() roomIDString := ctx.Param("roomID") roomID, err := strconv.ParseInt(roomIDString, 10, 32) if err != nil { logger.Errorf("无法通过 roomID 获取会话。RoomID 不是有效的 int32: %v", err) return ctx.NoContent(http.StatusBadRequest) } session, err := c.sessionDomain.Get(int32(roomID)) if err != nil || session == nil { logger.Errorf("无法获取会话: %v", err) return ctx.NoContent(http.StatusNotFound) } // 出于兼容旧实现的原因,我们需要将会话放在一个包装对象中, // 该对象的会话可以通过键 "fields" 访问。旧实现还返回一个条目的列表... response := make([]SessionsResponse, 1) response[0] = SessionsResponse{*session} return ctx.JSONPretty(http.StatusOK, response, " ") } // List 处理器 // GET /list func (c *SessionController) List(ctx echo.Context) error { logger := ctx.Logger() sessions, err := c.sessionDomain.List() if err != nil { logger.Errorf("无法渲染会话列表: %v", err) return ctx.NoContent(http.StatusInternalServerError) } // 出于兼容旧实现的原因,我们需要将会话放在一个包装对象中, // 该对象的会话可以通过键 "fields" 访问 response := make([]SessionsResponse, len(sessions)) for i, session := range sessions { response[i].Fields = session } return ctx.JSONPretty(http.StatusOK, response, " ") } // Add 处理器 // POST /add func (c *SessionController) Add(ctx echo.Context) error { logger := ctx.Logger() var err error var session *entity.Session var req domain.AddSessionRequest if err := ctx.Bind(&req); err != nil { logger.Errorf("无法解析传入的会话: %v", err) return ctx.NoContent(http.StatusBadRequest) } ip := net.ParseIP(ctx.RealIP()) if session, err = c.sessionDomain.Add(&req, ip); err != nil { logger.Errorf("不会添加会话: %v", err) if errors.Is(err, domain.ErrSessionRejected) { logger.Errorf("会话被拒绝: %v", session) return ctx.NoContent(http.StatusBadRequest) } else if errors.Is(err, domain.ErrRateLimited) { return ctx.NoContent(http.StatusTooManyRequests) } return ctx.NoContent(http.StatusBadRequest) } result := "status=OK\n" result += session.PrintForRetroarch() return ctx.String(http.StatusOK, result) } // Tunnel 处理器 // GET /tunnel func (c *SessionController) Tunnel(ctx echo.Context) error { logger := ctx.Logger() tunnelName := ctx.QueryParam("name") if tunnelName == "" { return ctx.NoContent(http.StatusBadRequest) } tunnel := c.sessionDomain.GetTunnel(tunnelName) if tunnel == nil { logger.Errorf("找不到隧道服务器: '%s'", tunnelName) return ctx.NoContent(http.StatusNotFound) } result := "status=OK\n" result += tunnel.PrintForRetroarch() return ctx.String(http.StatusOK, result) }