diff --git a/actions/feed.go b/actions/feed.go index 73c0e45..484d3bb 100644 --- a/actions/feed.go +++ b/actions/feed.go @@ -1,90 +1,114 @@ package actions import ( - "bytes" "fmt" "github.com/gin-gonic/gin" "github/fthvgb1/wp-go/actions/common" + "github/fthvgb1/wp-go/cache" "github/fthvgb1/wp-go/helper" - "github/fthvgb1/wp-go/logs" "github/fthvgb1/wp-go/models" "github/fthvgb1/wp-go/plugins" - "github/fthvgb1/wp-go/templates" - "html" - "html/template" + "github/fthvgb1/wp-go/rss2" "net/http" "strings" "time" - "unicode/utf8" ) -func Feed() func(ctx *gin.Context) { - fs, err := template.ParseFS(templates.TemplateFs, "feed/feed.gohtml") - if err != nil { - panic(err) +var feedCache = cache.NewSliceCache[string](feed, time.Hour) +var tmp = "Mon, 02 Jan 2006 15:04:05 GMT" + +func FeedCached(c *gin.Context) { + if !isCacheExpired(c, feedCache.SetTime()) { + c.Status(http.StatusNotModified) + c.Abort() + return } - return func(c *gin.Context) { - c.Header("Content-Type", "application/rss+xml; charset=UTF-8") - c.Header("Cache-Control", "no-cache, must-revalidate, max-age=0") - c.Header("Expires", "Wed, 11 Jan 1984 05:00:00 GMT") - //c.Header("Last-Modified", "false") - c.Header("ETag", helper.StringMd5("gmt")) - r := common.RecentPosts(c, 10) - ids := helper.SliceMap(r, func(t models.WpPosts) uint64 { - return t.Id - }) - posts, err := common.GetPostsByIds(c, ids) - if err != nil { - c.Status(http.StatusInternalServerError) - c.Abort() - return - } - type p struct { - models.WpPosts - Cates string - CommentLink string - Username string - Category string - Link string - Description string - Date string - } - rr := helper.SliceMap(posts, func(t models.WpPosts) p { - common.PasswordProjectTitle(&t) - if t.PostPassword != "" { - common.PasswdProjectContent(&t) - } - l := "" - if t.CommentStatus == "open" || t.CommentCount > 0 { - l = fmt.Sprintf("%s/p/%d#comments", models.Options["siteurl"], t.Id) - } - user := common.GetUser(c, t.PostAuthor) - content := plugins.DigestRaw(t.PostContent, utf8.RuneCountInString(t.PostContent), t.Id) - t.PostContent = content - return p{ - WpPosts: t, - Cates: strings.Join(t.Categories, "、"), - CommentLink: l, - Username: user.DisplayName, - Link: fmt.Sprintf("%s/p/%d", models.Options["siteurl"], t.Id), - Description: plugins.DigestRaw(content, 55, t.Id), - Date: t.PostDateGmt.Format(time.RFC1123Z), - } - }) - h := gin.H{ - "posts": rr, - "options": models.Options, - "now": time.Now().Format(time.RFC1123Z), - } - - var buf bytes.Buffer - err = fs.Execute(&buf, h) - if err != nil { - logs.ErrPrintln(err, "parse template") - return - } - - c.String(http.StatusOK, html.UnescapeString(buf.String())) - } - + c.Next() +} + +func isCacheExpired(c *gin.Context, lastTime time.Time) bool { + eTag := helper.StringMd5(lastTime.Format(tmp)) + since := c.Request.Header.Get("If-Modified-Since") + cTag := c.Request.Header.Get("If-None-Match") + if since != "" && cTag != "" { + cGMT, err := time.Parse(tmp, since) + if err == nil && lastTime.Unix() <= cGMT.Unix() && eTag == cTag { + c.Status(http.StatusNotModified) + return false + } + } + return true +} + +func Feed(c *gin.Context) { + s, err := feedCache.GetCache(c, time.Second, c) + if err != nil { + c.Status(http.StatusInternalServerError) + c.Abort() + c.Error(err) + return + } + lastTimeGMT := feedCache.SetTime().Format(tmp) + eTag := helper.StringMd5(lastTimeGMT) + c.Header("Content-Type", "application/rss+xml; charset=UTF-8") + c.Header("Last-Modified", lastTimeGMT) + c.Header("ETag", eTag) + c.String(http.StatusOK, s[0]) +} + +func feed(arg ...any) (xml []string, err error) { + c := arg[0].(*gin.Context) + r := common.RecentPosts(c, 10) + ids := helper.SliceMap(r, func(t models.WpPosts) uint64 { + return t.Id + }) + posts, err := common.GetPostsByIds(c, ids) + if err != nil { + return + } + rs := rss2.Rss2{ + Title: models.Options["blogname"], + AtomLink: fmt.Sprintf("%s/feed", models.Options["home"]), + Link: models.Options["siteurl"], + Description: models.Options["blogdescription"], + LastBuildDate: time.Now().Format(time.RFC1123Z), + Language: "zh-CN", + UpdatePeriod: "hourly", + UpdateFrequency: 1, + Generator: models.Options["home"], + Items: nil, + } + + rs.Items = helper.SliceMap(posts, func(t models.WpPosts) rss2.Item { + desc := "无法提供摘要。这是一篇受保护的文章。" + common.PasswordProjectTitle(&t) + if t.PostPassword != "" { + common.PasswdProjectContent(&t) + } else { + desc = plugins.DigestRaw(t.PostContent, 55, t.Id) + } + l := "" + if t.CommentStatus == "open" && t.CommentCount > 0 { + l = fmt.Sprintf("%s/p/%d#comments", models.Options["siteurl"], t.Id) + } else if t.CommentStatus == "open" && t.CommentCount == 0 { + l = fmt.Sprintf("%s/p/%d#respond", models.Options["siteurl"], t.Id) + } + user := common.GetUser(c, t.PostAuthor) + + return rss2.Item{ + Title: t.PostTitle, + Creator: user.DisplayName, + Guid: t.Guid, + SlashComments: int(t.CommentCount), + Content: t.PostContent, + Category: strings.Join(t.Categories, "、"), + CommentLink: l, + CommentRss: fmt.Sprintf("%s/p/%d/feed", models.Options["siteurl"], t.Id), + Link: fmt.Sprintf("%s/p/%d", models.Options["siteurl"], t.Id), + Description: desc, + PubDate: t.PostDateGmt.Format(time.RFC1123Z), + } + }) + xml = []string{rs.GetXML()} + return } diff --git a/cache/slice.go b/cache/slice.go index d53a607..16fcc46 100644 --- a/cache/slice.go +++ b/cache/slice.go @@ -17,6 +17,10 @@ type SliceCache[T any] struct { incr int } +func (c *SliceCache[T]) SetTime() time.Time { + return c.setTime +} + func NewSliceCache[T any](fun func(...any) ([]T, error), duration time.Duration) *SliceCache[T] { return &SliceCache[T]{ mutex: &sync.Mutex{}, diff --git a/route/route.go b/route/route.go index fe7afc9..371b0b4 100644 --- a/route/route.go +++ b/route/route.go @@ -52,7 +52,7 @@ func SetupRouter() *gin.Engine { r.GET("/p/date/:year/:month/page/:page", actions.Index) r.POST("/login", actions.Login) r.GET("/p/:id", actions.Detail) - r.GET("/feed", actions.Feed()) + r.GET("/feed", actions.FeedCached, actions.Feed) if helper.IsContainInArr(gin.Mode(), []string{gin.DebugMode, gin.TestMode}) { pprof.Register(r, "dev/pprof") } diff --git a/rss2/rss2.go b/rss2/rss2.go new file mode 100644 index 0000000..26ad662 --- /dev/null +++ b/rss2/rss2.go @@ -0,0 +1,142 @@ +package rss2 + +import ( + "fmt" + "github/fthvgb1/wp-go/helper" + "strings" +) + +var template = ` + + + + {$title} + + {$link} + {$description} + {$lastBuildDate} + {$lang} + + {$updatePeriod} + + + {$updateFrequency} + + {$generator} + {$items} + + + +` +var templateItems = ` + + {$title} + {$link} + {$comments} + + {$pubDate} + + {$guid} + + + {$commentRss} + {$commentNumber} + + +` + +type Rss2 struct { + Title string + AtomLink string + Link string + Description string + LastBuildDate string + Language string + UpdatePeriod string + UpdateFrequency int + Generator string + Items []Item +} + +type Item struct { + Title string + Link string + CommentLink string + Creator string + PubDate string + Category string + Guid string + Description string + Content string + CommentRss string + SlashComments int +} + +func (r Rss2) GetXML() (xml string) { + xml = template + for k, v := range map[string]string{ + "{$title}": r.Title, + "{$link}": r.Link, + "{$feedLink}": r.AtomLink, + "{$description}": r.Description, + "{$lastBuildDate}": r.LastBuildDate, + "{$lang}": r.Language, + "{$updatePeriod}": r.UpdatePeriod, + "{$updateFrequency}": fmt.Sprintf("%d", r.UpdateFrequency), + "{$generator}": r.Generator, + "{$items}": strings.Join(helper.SliceMap(r.Items, func(t Item) string { + return t.GetXml() + }), ""), + } { + xml = strings.Replace(xml, k, v, -1) + } + return +} + +func (i Item) GetXml() (xml string) { + xml = templateItems + for k, v := range map[string]string{ + "{$title}": i.Title, + "{$link}": i.Link, + "{$comments}": i.GetComments(), + "{$author}": i.Creator, + "{$pubDate}": i.PubDate, + "{$category}": i.Category, + "{$guid}": i.Guid, + "{$description}": i.Description, + "{$content}": i.Content, + "{$commentRss}": i.getCommentRss(), + "{$commentNumber}": i.getSlashComments(), + } { + xml = strings.Replace(xml, k, v, -1) + } + return +} + +func (i Item) GetComments() string { + r := "" + if i.CommentLink != "" { + r = fmt.Sprintf("%s", i.CommentLink) + } + return r +} + +func (i Item) getCommentRss() (r string) { + if i.CommentLink != "" && i.SlashComments > 0 { + r = fmt.Sprintf("%s", i.CommentRss) + } + return +} +func (i Item) getSlashComments() (r string) { + if i.SlashComments > 0 { + r = fmt.Sprintf("%d", i.SlashComments) + } + return +}