netplay-lobby-server-go/controller/sessioncontroller.go

205 lines
5.5 KiB
Go
Raw Normal View History

2024-12-24 22:09:17 +08:00
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)
}