346 lines
7.9 KiB
Go
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)
|
|
}
|
|
}
|