feed
This commit is contained in:
parent
7bdc47559a
commit
be5accaa10
|
@ -26,6 +26,7 @@ var postListIdsCache *cache.MapCache[string, PostIds]
|
|||
var searchPostIdsCache *cache.MapCache[string, PostIds]
|
||||
var maxPostIdCache *cache.SliceCache[uint64]
|
||||
var TotalRaw int
|
||||
var usersCache *cache.MapCache[uint64, models.WpUsers]
|
||||
|
||||
func InitActionsCommonCache() {
|
||||
archivesCaches = &Arch{
|
||||
|
@ -53,6 +54,9 @@ func InitActionsCommonCache() {
|
|||
postCommentCaches = cache.NewMapCacheByFn[uint64, []models.WpComments](postComments, vars.Conf.CommentsCacheTime)
|
||||
|
||||
maxPostIdCache = cache.NewSliceCache[uint64](getMaxPostId, vars.Conf.MaxPostIdCacheTime)
|
||||
|
||||
usersCache = cache.NewMapCacheByBatchFn[uint64, models.WpUsers](getUsers, time.Hour)
|
||||
usersCache.SetCacheFunc(getUser)
|
||||
}
|
||||
|
||||
func ClearCache() {
|
||||
|
@ -147,8 +151,11 @@ func postComments(args ...any) ([]models.WpComments, error) {
|
|||
}, nil, 0)
|
||||
}
|
||||
|
||||
func RecentComments(ctx context.Context) (r []models.WpComments) {
|
||||
func RecentComments(ctx context.Context, n int) (r []models.WpComments) {
|
||||
r, err := recentCommentsCaches.GetCache(ctx, time.Second)
|
||||
if len(r) > n {
|
||||
r = r[0:n]
|
||||
}
|
||||
logs.ErrPrintln(err, "get recent comment")
|
||||
return
|
||||
}
|
||||
|
@ -158,7 +165,7 @@ func recentComments(...any) (r []models.WpComments, err error) {
|
|||
{"post_status", "publish"},
|
||||
}, "comment_ID,comment_author,comment_post_ID,post_title", "", models.SqlBuilder{{"comment_date_gmt", "desc"}}, models.SqlBuilder{
|
||||
{"a", "left join", "wp_posts b", "a.comment_post_ID=b.ID"},
|
||||
}, 5)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
func GetContextPost(ctx context.Context, id uint64, date time.Time) (prev, next models.WpPosts, err error) {
|
||||
|
@ -177,7 +184,7 @@ func getPostContext(arg ...any) (r PostContext, err error) {
|
|||
{"post_date", ">", t.Format("2006-01-02 15:04:05")},
|
||||
{"post_status", "in", ""},
|
||||
{"post_type", "post"},
|
||||
}, "ID,post_title,post_password", nil, []any{"publish", "private"})
|
||||
}, "ID,post_title,post_password", nil, []any{"publish"})
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
|
@ -188,7 +195,7 @@ func getPostContext(arg ...any) (r PostContext, err error) {
|
|||
{"post_date", "<", t.Format("2006-01-02 15:04:05")},
|
||||
{"post_status", "in", ""},
|
||||
{"post_type", "post"},
|
||||
}, "ID,post_title", models.SqlBuilder{{"post_date", "desc"}}, []any{"publish", "private"})
|
||||
}, "ID,post_title", models.SqlBuilder{{"post_date", "desc"}}, []any{"publish"})
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
|
@ -239,15 +246,18 @@ func categories(...any) (terms []models.WpTermsMy, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func RecentPosts(ctx context.Context) (r []models.WpPosts) {
|
||||
func RecentPosts(ctx context.Context, n int) (r []models.WpPosts) {
|
||||
r, err := recentPostsCaches.GetCache(ctx, time.Second)
|
||||
if n < len(r) {
|
||||
r = r[:n]
|
||||
}
|
||||
logs.ErrPrintln(err, "get recent post")
|
||||
return
|
||||
}
|
||||
func recentPosts(...any) (r []models.WpPosts, err error) {
|
||||
r, err = models.Find[models.WpPosts](models.SqlBuilder{{
|
||||
"post_type", "post",
|
||||
}, {"post_status", "publish"}}, "ID,post_title,post_password", "", models.SqlBuilder{{"post_date", "desc"}}, nil, 5)
|
||||
}, {"post_status", "publish"}}, "ID,post_title,post_password", "", models.SqlBuilder{{"post_date", "desc"}}, nil, 10)
|
||||
for i, post := range r {
|
||||
if post.PostPassword != "" {
|
||||
PasswordProjectTitle(&r[i])
|
||||
|
|
28
actions/common/users.go
Normal file
28
actions/common/users.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github/fthvgb1/wp-go/logs"
|
||||
"github/fthvgb1/wp-go/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getUsers(...any) (m map[uint64]models.WpUsers, err error) {
|
||||
m = make(map[uint64]models.WpUsers)
|
||||
r, err := models.Find[models.WpUsers](nil, "*", "", nil, nil, 0)
|
||||
for _, user := range r {
|
||||
m[user.Id] = user
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getUser(a ...any) (r models.WpUsers, err error) {
|
||||
id := a[0].(uint64)
|
||||
return models.FindOneById[models.WpUsers](id)
|
||||
}
|
||||
|
||||
func GetUser(ctx *gin.Context, uid uint64) models.WpUsers {
|
||||
r, err := usersCache.GetCache(ctx, uid, time.Second, uid)
|
||||
logs.ErrPrintln(err, "get user", uid)
|
||||
return r
|
||||
}
|
|
@ -27,10 +27,10 @@ func Detail(c *gin.Context) {
|
|||
hh := detailHandler{
|
||||
c,
|
||||
}
|
||||
recent := common.RecentPosts(c)
|
||||
recent := common.RecentPosts(c, 5)
|
||||
archive := common.Archives()
|
||||
categoryItems := common.Categories(c)
|
||||
recentComments := common.RecentComments(c)
|
||||
recentComments := common.RecentComments(c, 5)
|
||||
var h = gin.H{
|
||||
"title": models.Options["blogname"],
|
||||
"options": models.Options,
|
||||
|
|
90
actions/feed.go
Normal file
90
actions/feed.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github/fthvgb1/wp-go/actions/common"
|
||||
"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"
|
||||
"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)
|
||||
}
|
||||
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" {
|
||||
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()))
|
||||
}
|
||||
|
||||
}
|
|
@ -133,8 +133,6 @@ func (h *indexHandle) parseParams() {
|
|||
h.setTitleLR(helper.StrJoin(`"`, s, `"`, "的搜索结果"), models.Options["blogname"])
|
||||
h.search = s
|
||||
h.scene = plugins.Search
|
||||
} else {
|
||||
h.status = append(h.status, "private")
|
||||
}
|
||||
p := h.c.Query("paged")
|
||||
if p == "" {
|
||||
|
@ -162,9 +160,9 @@ func Index(c *gin.Context) {
|
|||
h := newIndexHandle(c)
|
||||
h.parseParams()
|
||||
archive := common.Archives()
|
||||
recent := common.RecentPosts(c)
|
||||
recent := common.RecentPosts(c, 5)
|
||||
categoryItems := common.Categories(c)
|
||||
recentComments := common.RecentComments(c)
|
||||
recentComments := common.RecentComments(c, 5)
|
||||
ginH := gin.H{
|
||||
"options": models.Options,
|
||||
"recentPosts": recent,
|
||||
|
|
60
helper/html.go
Normal file
60
helper/html.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var entitlesMap = map[int][]string{
|
||||
EntCompat: {"&", """, "<", ">"},
|
||||
EntQuotes: {"&", """, "'", "<", ">"},
|
||||
EntNoQuotes: {"&", "<", ">"},
|
||||
EntSpace: {" "},
|
||||
}
|
||||
var unEntitlesMap = map[int][]string{
|
||||
EntCompat: {"&", "\"", "<", ">"},
|
||||
EntQuotes: {"&", "\"", "'", "<", ">"},
|
||||
EntNoQuotes: {"&", "<", ">"},
|
||||
EntSpace: {" "},
|
||||
}
|
||||
|
||||
const (
|
||||
EntCompat = 1
|
||||
EntQuotes = 2
|
||||
EntNoQuotes = 4
|
||||
EntSpace = 8
|
||||
)
|
||||
|
||||
func htmlSpecialChars(text string, flags int) string {
|
||||
r, ok := unEntitlesMap[flags]
|
||||
e := entitlesMap[flags]
|
||||
if !ok {
|
||||
r = unEntitlesMap[EntCompat]
|
||||
e = entitlesMap[EntCompat]
|
||||
}
|
||||
if flags&EntSpace == EntSpace {
|
||||
r = append(r, unEntitlesMap[EntSpace]...)
|
||||
e = append(e, entitlesMap[EntSpace]...)
|
||||
}
|
||||
|
||||
for i, entitle := range r {
|
||||
text = strings.Replace(text, entitle, e[i], -1)
|
||||
}
|
||||
return text
|
||||
}
|
||||
func htmlSpecialCharsDecode(text string, flags int) string {
|
||||
r, ok := entitlesMap[flags]
|
||||
u := unEntitlesMap[flags]
|
||||
if !ok {
|
||||
r = entitlesMap[EntCompat]
|
||||
u = unEntitlesMap[EntCompat]
|
||||
}
|
||||
if flags&EntSpace == EntSpace {
|
||||
r = append(r, entitlesMap[EntSpace]...)
|
||||
u = append(u, unEntitlesMap[EntSpace]...)
|
||||
}
|
||||
|
||||
for i, entitle := range r {
|
||||
text = strings.Replace(text, entitle, u[i], -1)
|
||||
}
|
||||
return text
|
||||
}
|
92
helper/html_test.go
Normal file
92
helper/html_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package helper
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_htmlSpecialChars(t *testing.T) {
|
||||
type args struct {
|
||||
text string
|
||||
flags int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "t1",
|
||||
args: args{text: "<a href='test'>Test</a>", flags: EntQuotes},
|
||||
want: "<a href='test'>Test</a>",
|
||||
}, {
|
||||
name: "t2",
|
||||
args: args{text: "<a href='test'>Test</a>", flags: EntCompat},
|
||||
want: "<a href='test'>Test</a>",
|
||||
}, {
|
||||
name: "t3",
|
||||
args: args{text: "<a href='test'>T est</a>", flags: EntCompat | EntSpace},
|
||||
want: "<a href='test'>T est</a>",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := htmlSpecialChars(tt.args.text, tt.args.flags); got != tt.want {
|
||||
t.Errorf("htmlSpecialChars() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_htmlSpecialCharsDecode(t *testing.T) {
|
||||
type args struct {
|
||||
text string
|
||||
flags int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "t1",
|
||||
args: args{
|
||||
text: "<a href='test'>Test</a>",
|
||||
flags: EntCompat,
|
||||
},
|
||||
want: "<a href='test'>Test</a>",
|
||||
}, {
|
||||
name: "t2",
|
||||
args: args{
|
||||
text: "<a href='test'>Test</a>",
|
||||
flags: EntQuotes,
|
||||
},
|
||||
want: "<a href='test'>Test</a>",
|
||||
}, {
|
||||
name: "t3",
|
||||
args: args{
|
||||
text: "<p>this -> "</p>\n",
|
||||
flags: EntNoQuotes,
|
||||
},
|
||||
want: "<p>this -> "</p>\n",
|
||||
}, {
|
||||
name: "t4",
|
||||
args: args{
|
||||
text: "<p>this -> "</p>\n",
|
||||
flags: EntCompat,
|
||||
},
|
||||
want: "<p>this -> \"</p>\n",
|
||||
}, {
|
||||
name: "t5",
|
||||
args: args{
|
||||
text: "<p>this -> "</p>\n",
|
||||
flags: EntCompat | EntSpace,
|
||||
},
|
||||
want: "<p>this -> \"</p>\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := htmlSpecialCharsDecode(tt.args.text, tt.args.flags); got != tt.want {
|
||||
t.Errorf("htmlSpecialCharsDecode() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -213,7 +213,7 @@ func SimplePagination[T Model](where ParseWhere, fields, group string, page, pag
|
|||
return
|
||||
}
|
||||
|
||||
func FindOneById[T Model](id int) (T, error) {
|
||||
func FindOneById[T Model, I ~int | ~uint64 | ~int64 | ~int32](id I) (T, error) {
|
||||
var r T
|
||||
sql := fmt.Sprintf("select * from `%s` where `%s`=?", r.Table(), r.PrimaryKey())
|
||||
err := db.Db.Get(&r, sql, id)
|
||||
|
|
24
models/wp_users.go
Normal file
24
models/wp_users.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type WpUsers struct {
|
||||
Id uint64 `gorm:"column:ID" db:"ID" json:"ID"`
|
||||
UserLogin string `gorm:"column:user_login" db:"user_login" json:"user_login"`
|
||||
UserPass string `gorm:"column:user_pass" db:"user_pass" json:"user_pass"`
|
||||
UserNicename string `gorm:"column:user_nicename" db:"user_nicename" json:"user_nicename"`
|
||||
UserEmail string `gorm:"column:user_email" db:"user_email" json:"user_email"`
|
||||
UserUrl string `gorm:"column:user_url" db:"user_url" json:"user_url"`
|
||||
UserRegistered time.Time `gorm:"column:user_registered" db:"user_registered" json:"user_registered"`
|
||||
UserActivationKey string `gorm:"column:user_activation_key" db:"user_activation_key" json:"user_activation_key"`
|
||||
UserStatus int `gorm:"column:user_status" db:"user_status" json:"user_status"`
|
||||
DisplayName string `gorm:"column:display_name" db:"display_name" json:"display_name"`
|
||||
}
|
||||
|
||||
func (u WpUsers) Table() string {
|
||||
return "wp_users"
|
||||
}
|
||||
|
||||
func (u WpUsers) PrimaryKey() string {
|
||||
return "ID"
|
||||
}
|
|
@ -52,6 +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())
|
||||
if helper.IsContainInArr(gin.Mode(), []string{gin.DebugMode, gin.TestMode}) {
|
||||
pprof.Register(r, "dev/pprof")
|
||||
}
|
||||
|
|
44
templates/feed/feed.gohtml
Normal file
44
templates/feed/feed.gohtml
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
>
|
||||
|
||||
<channel>
|
||||
<title>{{ .options.blogname }}</title>
|
||||
<atom:link href="{{.options.home}}/feed" rel="self" type="application/rss+xml"/>
|
||||
<link>{{.options.siteurl}}/</link>
|
||||
<description>{{.options.blogdescription}}</description>
|
||||
<lastBuildDate>{{.now}}</lastBuildDate>
|
||||
<language>zh-CN</language>
|
||||
<sy:updatePeriod>
|
||||
hourly
|
||||
</sy:updatePeriod>
|
||||
<sy:updateFrequency>
|
||||
1
|
||||
</sy:updateFrequency>
|
||||
<generator>https://wordpress.org/?v=6.0.2</generator>
|
||||
{{range $k,$v := .posts}}
|
||||
<item>
|
||||
<title>{{$v.PostTitle}}</title>
|
||||
<link>{{$v.Link}}</link>
|
||||
<comments>{{ $v.CommentLink}}</comments>
|
||||
<dc:creator><![CDATA[{{$v.Username}}]]></dc:creator>
|
||||
<pubDate>{{$v.Date}}</pubDate>
|
||||
<category><![CDATA[{{$v.Cates}}]]></category>
|
||||
<guid isPermaLink="false">{{$v.Guid}}</guid>
|
||||
<description><![CDATA[{{$v.Description}}]]></description>
|
||||
<content:encoded><![CDATA[{{$v.PostContent}}]]></content:encoded>
|
||||
{{if gt $v.CommentCount 0 }}
|
||||
<wfw:commentRss>{{$v.CommentLink}}</wfw:commentRss>
|
||||
<slash:comments>{{$v.CommentCount}}</slash:comments>
|
||||
{{end}}
|
||||
|
||||
</item>
|
||||
{{end}}
|
||||
</channel>
|
||||
</rss>
|
|
@ -58,7 +58,6 @@
|
|||
<aside id="meta-2" class="widget widget_meta"><h2 class="widget-title">其他操作</h2>
|
||||
<nav aria-label="其他操作">
|
||||
<ul>
|
||||
<li><a href="/wp-login.php">登录</a></li>
|
||||
<li><a href="/feed">条目feed</a></li>
|
||||
<li><a href="/comments/feed">评论feed</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
)
|
||||
|
||||
//go:embed posts layout
|
||||
//go:embed posts layout feed
|
||||
var TemplateFs embed.FS
|
||||
|
||||
type FsTemplate struct {
|
||||
|
|
Loading…
Reference in New Issue
Block a user