Files
chatlog/internal/chatlog/http/route.go
2025-04-01 19:41:40 +08:00

346 lines
7.9 KiB
Go

package http
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/sjzar/chatlog/internal/errors"
"github.com/sjzar/chatlog/pkg/util"
"github.com/sjzar/chatlog/pkg/util/dat2img"
"github.com/gin-gonic/gin"
)
// EFS holds embedded file system data for static assets.
//
//go:embed static
var EFS embed.FS
func (s *Service) initRouter() {
router := s.GetRouter()
staticDir, _ := fs.Sub(EFS, "static")
router.StaticFS("/static", http.FS(staticDir))
router.StaticFileFS("/favicon.ico", "./favicon.ico", http.FS(staticDir))
router.StaticFileFS("/", "./index.htm", http.FS(staticDir))
// Media
router.GET("/image/:key", s.GetImage)
router.GET("/video/:key", s.GetVideo)
router.GET("/file/:key", s.GetFile)
router.GET("/data/*path", s.GetMediaData)
// MCP Server
{
router.GET("/sse", s.mcp.HandleSSE)
router.POST("/messages", s.mcp.HandleMessages)
// mcp inspector is shit
// https://github.com/modelcontextprotocol/inspector/blob/aeaf32f/server/src/index.ts#L155
router.POST("/message", s.mcp.HandleMessages)
}
// API V1 Router
api := router.Group("/api/v1")
{
api.GET("/chatlog", s.GetChatlog)
api.GET("/contact", s.GetContacts)
api.GET("/chatroom", s.GetChatRooms)
api.GET("/session", s.GetSessions)
}
router.NoRoute(s.NoRoute)
}
// NoRoute handles 404 Not Found errors. If the request URL starts with "/api"
// or "/static", it responds with a JSON error. Otherwise, it redirects to the root path.
func (s *Service) NoRoute(c *gin.Context) {
path := c.Request.URL.Path
switch {
case strings.HasPrefix(path, "/api"), strings.HasPrefix(path, "/static"):
c.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
default:
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
c.Redirect(http.StatusFound, "/")
}
}
func (s *Service) GetChatlog(c *gin.Context) {
q := struct {
Time string `form:"time"`
Talker string `form:"talker"`
Limit int `form:"limit"`
Offset int `form:"offset"`
Format string `form:"format"`
}{}
if err := c.BindQuery(&q); err != nil {
errors.Err(c, err)
return
}
var err error
start, end, ok := util.TimeRangeOf(q.Time)
if !ok {
errors.Err(c, errors.InvalidArg("time"))
}
if q.Limit < 0 {
q.Limit = 0
}
if q.Offset < 0 {
q.Offset = 0
}
messages, err := s.db.GetMessages(start, end, q.Talker, q.Limit, q.Offset)
if err != nil {
errors.Err(c, err)
return
}
switch strings.ToLower(q.Format) {
case "csv":
case "json":
// json
c.JSON(http.StatusOK, messages)
default:
// plain text
c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Flush()
for _, m := range messages {
c.Writer.WriteString(m.PlainText(len(q.Talker) == 0, c.Request.Host))
c.Writer.WriteString("\n")
c.Writer.Flush()
}
}
}
func (s *Service) GetContacts(c *gin.Context) {
q := struct {
Key string `form:"key"`
Limit int `form:"limit"`
Offset int `form:"offset"`
Format string `form:"format"`
}{}
if err := c.BindQuery(&q); err != nil {
errors.Err(c, err)
return
}
list, err := s.db.GetContacts(q.Key, q.Limit, q.Offset)
if err != nil {
errors.Err(c, err)
return
}
format := strings.ToLower(q.Format)
switch format {
case "json":
// json
c.JSON(http.StatusOK, list)
default:
// csv
if format == "csv" {
// 浏览器访问时,会下载文件
c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
} else {
c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Flush()
c.Writer.WriteString("UserName,Alias,Remark,NickName\n")
for _, contact := range list.Items {
c.Writer.WriteString(fmt.Sprintf("%s,%s,%s,%s\n", contact.UserName, contact.Alias, contact.Remark, contact.NickName))
}
c.Writer.Flush()
}
}
func (s *Service) GetChatRooms(c *gin.Context) {
q := struct {
Key string `form:"key"`
Limit int `form:"limit"`
Offset int `form:"offset"`
Format string `form:"format"`
}{}
if err := c.BindQuery(&q); err != nil {
errors.Err(c, err)
return
}
list, err := s.db.GetChatRooms(q.Key, q.Limit, q.Offset)
if err != nil {
errors.Err(c, err)
return
}
format := strings.ToLower(q.Format)
switch format {
case "json":
// json
c.JSON(http.StatusOK, list)
default:
// csv
if format == "csv" {
// 浏览器访问时,会下载文件
c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
} else {
c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Flush()
c.Writer.WriteString("Name,Remark,NickName,Owner,UserCount\n")
for _, chatRoom := range list.Items {
c.Writer.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d\n", chatRoom.Name, chatRoom.Remark, chatRoom.NickName, chatRoom.Owner, len(chatRoom.Users)))
}
c.Writer.Flush()
}
}
func (s *Service) GetSessions(c *gin.Context) {
q := struct {
Key string `form:"key"`
Limit int `form:"limit"`
Offset int `form:"offset"`
Format string `form:"format"`
}{}
if err := c.BindQuery(&q); err != nil {
errors.Err(c, err)
return
}
sessions, err := s.db.GetSessions(q.Key, q.Limit, q.Offset)
if err != nil {
errors.Err(c, err)
return
}
format := strings.ToLower(q.Format)
switch format {
case "csv":
c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Flush()
c.Writer.WriteString("UserName,NOrder,NickName,Content,NTime\n")
for _, session := range sessions.Items {
c.Writer.WriteString(fmt.Sprintf("%s,%d,%s,%s,%s\n", session.UserName, session.NOrder, session.NickName, strings.ReplaceAll(session.Content, "\n", "\\n"), session.NTime))
}
c.Writer.Flush()
case "json":
// json
c.JSON(http.StatusOK, sessions)
default:
c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Flush()
for _, session := range sessions.Items {
c.Writer.WriteString(session.PlainText(120))
c.Writer.WriteString("\n")
}
c.Writer.Flush()
}
}
func (s *Service) GetImage(c *gin.Context) {
s.GetMedia(c, "image")
}
func (s *Service) GetVideo(c *gin.Context) {
s.GetMedia(c, "video")
}
func (s *Service) GetFile(c *gin.Context) {
s.GetMedia(c, "file")
}
func (s *Service) GetMedia(c *gin.Context, _type string) {
key := c.Param("key")
if key == "" {
errors.Err(c, errors.InvalidArg(key))
return
}
media, err := s.db.GetMedia(_type, key)
if err != nil {
errors.Err(c, err)
return
}
if c.Query("info") != "" {
c.JSON(http.StatusOK, media)
return
}
c.Redirect(http.StatusFound, "/data/"+media.Path)
}
func (s *Service) GetMediaData(c *gin.Context) {
relativePath := filepath.Clean(c.Param("path"))
absolutePath := filepath.Join(s.ctx.DataDir, relativePath)
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{
"error": "File not found",
})
return
}
ext := strings.ToLower(filepath.Ext(absolutePath))
switch {
case ext == ".dat":
s.HandleDatFile(c, absolutePath)
default:
// 直接返回文件
c.File(absolutePath)
}
}
func (s *Service) HandleDatFile(c *gin.Context, path string) {
b, err := os.ReadFile(path)
if err != nil {
errors.Err(c, err)
return
}
out, ext, err := dat2img.Dat2Image(b)
if err != nil {
c.File(path)
return
}
switch ext {
case "jpg":
c.Data(http.StatusOK, "image/jpeg", out)
case "png":
c.Data(http.StatusOK, "image/png", out)
case "gif":
c.Data(http.StatusOK, "image/gif", out)
case "bmp":
c.Data(http.StatusOK, "image/bmp", out)
default:
c.File(path)
}
}