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/sjzar/chatlog/pkg/util/silk" "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("/voice/:key", s.GetVoice) 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) GetVoice(c *gin.Context) { s.GetMedia(c, "voice") } 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 } switch media.Type { case "voice": s.HandleVoice(c, media.Data) default: 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) } } func (s *Service) HandleVoice(c *gin.Context, data []byte) { out, err := silk.Silk2MP3(data) if err != nil { c.Data(http.StatusOK, "audio/silk", data) return } c.Data(http.StatusOK, "audio/mp3", out) }