diff --git a/actions/common/common.go b/actions/common/common.go index fe612c7..efee25a 100644 --- a/actions/common/common.go +++ b/actions/common/common.go @@ -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]) diff --git a/actions/common/users.go b/actions/common/users.go new file mode 100644 index 0000000..6f6fc73 --- /dev/null +++ b/actions/common/users.go @@ -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 +} diff --git a/actions/detail.go b/actions/detail.go index 93a6448..d252a61 100644 --- a/actions/detail.go +++ b/actions/detail.go @@ -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, diff --git a/actions/feed.go b/actions/feed.go new file mode 100644 index 0000000..f25b4fb --- /dev/null +++ b/actions/feed.go @@ -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())) + } + +} diff --git a/actions/index.go b/actions/index.go index 1ef476d..ad09963 100644 --- a/actions/index.go +++ b/actions/index.go @@ -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, diff --git a/helper/html.go b/helper/html.go new file mode 100644 index 0000000..3fc2bd2 --- /dev/null +++ b/helper/html.go @@ -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 +} diff --git a/helper/html_test.go b/helper/html_test.go new file mode 100644 index 0000000..6b0a6e5 --- /dev/null +++ b/helper/html_test.go @@ -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: "Test", flags: EntQuotes}, + want: "<a href='test'>Test</a>", + }, { + name: "t2", + args: args{text: "Test", flags: EntCompat}, + want: "<a href='test'>Test</a>", + }, { + name: "t3", + args: args{text: "T est", 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: "Test", + }, { + name: "t2", + args: args{ + text: "<a href='test'>Test</a>", + flags: EntQuotes, + }, + want: "Test", + }, { + name: "t3", + args: args{ + text: "

this -> "

\n", + flags: EntNoQuotes, + }, + want: "

this -> "

\n", + }, { + name: "t4", + args: args{ + text: "

this -> "

\n", + flags: EntCompat, + }, + want: "

this -> \"

\n", + }, { + name: "t5", + args: args{ + text: "

this -> "

\n", + flags: EntCompat | EntSpace, + }, + want: "

this -> \"

\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) + } + }) + } +} diff --git a/models/model.go b/models/model.go index bf8f1b5..991da0e 100644 --- a/models/model.go +++ b/models/model.go @@ -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) diff --git a/models/wp_users.go b/models/wp_users.go new file mode 100644 index 0000000..b429e92 --- /dev/null +++ b/models/wp_users.go @@ -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" +} diff --git a/route/route.go b/route/route.go index c337630..fe7afc9 100644 --- a/route/route.go +++ b/route/route.go @@ -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") } diff --git a/templates/feed/feed.gohtml b/templates/feed/feed.gohtml new file mode 100644 index 0000000..8369fb6 --- /dev/null +++ b/templates/feed/feed.gohtml @@ -0,0 +1,44 @@ + + + + + {{ .options.blogname }} + + {{.options.siteurl}}/ + {{.options.blogdescription}} + {{.now}} + zh-CN + + hourly + + + 1 + + https://wordpress.org/?v=6.0.2 + {{range $k,$v := .posts}} + + {{$v.PostTitle}} + {{$v.Link}} + {{ $v.CommentLink}} + + {{$v.Date}} + + {{$v.Guid}} + + + {{if gt $v.CommentCount 0 }} + {{$v.CommentLink}} + {{$v.CommentCount}} + {{end}} + + + {{end}} + + diff --git a/templates/layout/sidebar.gohtml b/templates/layout/sidebar.gohtml index 180f550..0785cdf 100644 --- a/templates/layout/sidebar.gohtml +++ b/templates/layout/sidebar.gohtml @@ -58,7 +58,6 @@