203 lines
5.3 KiB
Go
203 lines
5.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"infinite-canvas/backend/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func RegisterSkillRoutes(r *gin.RouterGroup, svc *service.Service) {
|
|
r.GET("/skills/image", proxySkillImage)
|
|
r.GET("/skills/capabilities", func(c *gin.Context) {
|
|
if _, err := currentUser(c, svc); err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"capabilities": svc.SkillIntegrationCapabilities()})
|
|
})
|
|
r.GET("/skills/community", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "12"))
|
|
result, err := svc.CommunitySkills(c.Request.Context(), user.ID, service.CommunitySkillsRequest{
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
Sort: c.DefaultQuery("sort", "hot"),
|
|
Search: c.Query("search"),
|
|
Categories: c.QueryArray("categories"),
|
|
})
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, result)
|
|
})
|
|
r.GET("/skills/activated", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skills, err := svc.ActivatedSkills(c.Request.Context(), user.ID)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skills": skills})
|
|
})
|
|
r.GET("/skills/favorites", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skills, err := svc.FavoriteSkills(c.Request.Context(), user.ID)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skills": skills})
|
|
})
|
|
r.GET("/skills/community/:dir", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skill, err := svc.CommunitySkillDetail(c.Request.Context(), user.ID, c.Param("dir"))
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skill": skill})
|
|
})
|
|
r.POST("/skills/:dir/activate", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skill, err := svc.SetSkillActivated(c.Request.Context(), user.ID, c.Param("dir"), true)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skill": skill})
|
|
})
|
|
r.DELETE("/skills/:dir/activate", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skill, err := svc.SetSkillActivated(c.Request.Context(), user.ID, c.Param("dir"), false)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skill": skill})
|
|
})
|
|
r.POST("/skills/:dir/favorite", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skill, err := svc.SetSkillLiked(c.Request.Context(), user.ID, c.Param("dir"), true)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skill": skill})
|
|
})
|
|
r.DELETE("/skills/:dir/favorite", func(c *gin.Context) {
|
|
user, err := currentUser(c, svc)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
skill, err := svc.SetSkillLiked(c.Request.Context(), user.ID, c.Param("dir"), false)
|
|
if err != nil {
|
|
failService(c, err)
|
|
return
|
|
}
|
|
ok(c, gin.H{"skill": skill})
|
|
})
|
|
}
|
|
|
|
func proxySkillImage(c *gin.Context) {
|
|
rawURL := strings.TrimSpace(c.Query("url"))
|
|
target, err := url.Parse(rawURL)
|
|
if err != nil || !allowedSkillImageURL(target) {
|
|
fail(c, http.StatusBadRequest, errInvalidSkillImageURL())
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
|
|
if err != nil {
|
|
fail(c, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
req.Header.Set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
|
req.Header.Set("Referer", "https://www.updream.cn/")
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 InfiniteCanvas/skills-image-proxy")
|
|
client := service.OutboundHTTPClient(15 * time.Second)
|
|
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
if len(via) >= 5 || !allowedSkillImageURL(req.URL) {
|
|
return errInvalidSkillImageURL()
|
|
}
|
|
return nil
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
fail(c, http.StatusBadGateway, err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
fail(c, http.StatusBadGateway, errSkillImageUpstream(resp.StatusCode))
|
|
return
|
|
}
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
c.Header("Cache-Control", "public, max-age=604800")
|
|
if etag := resp.Header.Get("ETag"); etag != "" {
|
|
c.Header("ETag", etag)
|
|
}
|
|
c.Status(http.StatusOK)
|
|
c.Header("Content-Type", contentType)
|
|
_, _ = io.Copy(c.Writer, resp.Body)
|
|
}
|
|
|
|
func allowedSkillImageURL(target *url.URL) bool {
|
|
if target == nil || (target.Scheme != "https" && target.Scheme != "http") {
|
|
return false
|
|
}
|
|
host := strings.ToLower(target.Hostname())
|
|
return host == "updream.cn" || strings.HasSuffix(host, ".updream.cn") || host == "hdslb.com" || strings.HasSuffix(host, ".hdslb.com")
|
|
}
|
|
|
|
func errInvalidSkillImageURL() error {
|
|
return errors.New("不支持的技能图片地址")
|
|
}
|
|
|
|
func errSkillImageUpstream(status int) error {
|
|
return errors.New("技能图片读取失败: " + strconv.Itoa(status))
|
|
}
|