增加回复可见功能
This commit is contained in:
@@ -19,6 +19,20 @@ func NewCommentService(filter *SensitiveFilter, settings *ForumSettingsService)
|
||||
return &CommentService{filter: filter, settings: settings}
|
||||
}
|
||||
|
||||
// HasUserReplied 用户是否已在该帖发表过有效评论(已发布或审核中,不含被拒)
|
||||
func (s *CommentService) HasUserReplied(postID, userID uint) bool {
|
||||
if postID == 0 || userID == 0 {
|
||||
return false
|
||||
}
|
||||
var count int64
|
||||
err := model.DB.Model(&model.Comment{}).
|
||||
Where("post_id = ? AND user_id = ? AND status IN ?", postID, userID,
|
||||
[]string{model.ContentStatusPublished, model.ContentStatusPending}).
|
||||
Limit(1).
|
||||
Count(&count).Error
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
type CommentCreateInput struct {
|
||||
UserID uint
|
||||
PostID uint
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
var (
|
||||
membersOnlyBlockRe = regexp.MustCompile(`(?is)<members-only\b[^>]*>([\s\S]*?)</members-only>`)
|
||||
replyOnlyBlockRe = regexp.MustCompile(`(?is)<reply-only\b[^>]*>([\s\S]*?)</reply-only>`)
|
||||
// style/script 内文本不能进搜索/摘要,否则会出现 "* {color:red}" 之类噪声
|
||||
styleOrScriptRe = regexp.MustCompile(`(?is)<(style|script)\b[^>]*>[\s\S]*?</(style|script)>`)
|
||||
htmlTagRe = regexp.MustCompile(`<[^>]+>`)
|
||||
@@ -16,21 +17,39 @@ var (
|
||||
|
||||
// RedactMembersOnlyHTML 未登录时移除会员专属区块内的正文,保留长度提示供前端展示
|
||||
func RedactMembersOnlyHTML(html string) string {
|
||||
return redactGatedBlocks(html, membersOnlyBlockRe, "members-only")
|
||||
}
|
||||
|
||||
// RedactReplyOnlyHTML 未回复时移除「回复可见」区块内的正文,保留长度提示供前端展示
|
||||
func RedactReplyOnlyHTML(html string) string {
|
||||
return redactGatedBlocks(html, replyOnlyBlockRe, "reply-only")
|
||||
}
|
||||
|
||||
// RedactGatedPostHTML 搜索/SEO 等场景:同时遮盖登录可见与回复可见正文
|
||||
func RedactGatedPostHTML(html string) string {
|
||||
return RedactReplyOnlyHTML(RedactMembersOnlyHTML(html))
|
||||
}
|
||||
|
||||
func redactGatedBlocks(html string, re *regexp.Regexp, tag string) string {
|
||||
if html == "" {
|
||||
return html
|
||||
}
|
||||
return membersOnlyBlockRe.ReplaceAllStringFunc(html, func(full string) string {
|
||||
m := membersOnlyBlockRe.FindStringSubmatch(full)
|
||||
return re.ReplaceAllStringFunc(html, func(full string) string {
|
||||
m := re.FindStringSubmatch(full)
|
||||
inner := ""
|
||||
if len(m) > 1 {
|
||||
inner = m[1]
|
||||
}
|
||||
length := membersContentLength(inner)
|
||||
return `<members-only data-locked="true" data-length="` + strconv.Itoa(length) + `"></members-only>`
|
||||
length := gatedContentLength(inner)
|
||||
gate := "login"
|
||||
if tag == "reply-only" {
|
||||
gate = "reply"
|
||||
}
|
||||
return `<` + tag + ` data-gate="` + gate + `" data-locked="true" data-length="` + strconv.Itoa(length) + `"></` + tag + `>`
|
||||
})
|
||||
}
|
||||
|
||||
func membersContentLength(html string) int {
|
||||
func gatedContentLength(html string) int {
|
||||
text := strings.TrimSpace(htmlTagRe.ReplaceAllString(html, ""))
|
||||
if text == "" {
|
||||
return 0
|
||||
|
||||
28
service/content_test.go
Normal file
28
service/content_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedactReplyOnlyHTML(t *testing.T) {
|
||||
in := `<p>公开</p><reply-only><p>秘密答案</p></reply-only>`
|
||||
out := RedactReplyOnlyHTML(in)
|
||||
if strings.Contains(out, "秘密答案") {
|
||||
t.Fatalf("不应保留回复可见正文,得到: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, `data-locked="true"`) || !strings.Contains(out, "reply-only") {
|
||||
t.Fatalf("应保留锁定壳,得到: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "公开") {
|
||||
t.Fatalf("不应误删公开段落,得到: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactGatedPostHTML(t *testing.T) {
|
||||
in := `<members-only><p>登录密</p></members-only><reply-only><p>回复密</p></reply-only>`
|
||||
out := RedactGatedPostHTML(in)
|
||||
if strings.Contains(out, "登录密") || strings.Contains(out, "回复密") {
|
||||
t.Fatalf("门控正文应被遮盖,得到: %q", out)
|
||||
}
|
||||
}
|
||||
@@ -359,7 +359,7 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp
|
||||
UserID: userID,
|
||||
Title: title,
|
||||
Content: content,
|
||||
ContentPlain: StripHTMLForSearch(content),
|
||||
ContentPlain: StripHTMLForSearch(RedactGatedPostHTML(content)),
|
||||
Tags: tags,
|
||||
PostType: postType,
|
||||
QuestionResolved: false,
|
||||
@@ -420,7 +420,7 @@ func (s *PostService) Update(userID, postID uint, isAdmin bool, title, content,
|
||||
"board_id": nextBoardID,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"content_plain": StripHTMLForSearch(content),
|
||||
"content_plain": StripHTMLForSearch(RedactGatedPostHTML(content)),
|
||||
"tags": tags,
|
||||
"post_type": nextType,
|
||||
"question_resolved": nextResolved,
|
||||
|
||||
@@ -17,16 +17,16 @@ func postContentHTMLPolicy() *bluemonday.Policy {
|
||||
p := bluemonday.UGCPolicy()
|
||||
|
||||
// TipTap / Markdown 转换会用到的结构
|
||||
p.AllowElements("div", "span", "u", "s", "center", "members-only")
|
||||
p.AllowElements("div", "span", "u", "s", "center", "members-only", "reply-only")
|
||||
p.AllowAttrs("class").OnElements(
|
||||
"p", "div", "span", "pre", "code", "img", "a",
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"blockquote", "ul", "ol", "li", "table", "thead", "tbody", "tr", "th", "td",
|
||||
"members-only",
|
||||
"members-only", "reply-only",
|
||||
)
|
||||
p.AllowAttrs("colspan", "rowspan").OnElements("th", "td")
|
||||
p.AllowAttrs(
|
||||
"data-locked", "data-length",
|
||||
"data-locked", "data-length", "data-gate",
|
||||
"data-code-copy", "data-code-fold", "data-lang", "data-full",
|
||||
"data-code-style", "data-line-numbers", "data-collapsed",
|
||||
"data-line-count", "data-lineno-digits",
|
||||
|
||||
@@ -34,11 +34,12 @@ func TestSanitizePostHTML_StripsInlineStyleAndScript(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSanitizePostHTML_KeepsMembersOnlyAndImageGroup(t *testing.T) {
|
||||
in := `<members-only data-locked="false"><p>密</p></members-only>` +
|
||||
in := `<members-only data-gate="login"><p>密</p></members-only>` +
|
||||
`<reply-only data-gate="reply"><p>回复可见</p></reply-only>` +
|
||||
`<div data-image-group data-layout="cols-2" class="image-group"><img src="/uploads/posts/a.jpg" alt="x"></div>` +
|
||||
`<p data-clear-float class="article-clear-float">清浮动</p>`
|
||||
out := SanitizePostHTML(in)
|
||||
for _, want := range []string{"members-only", "data-image-group", "data-layout", "data-clear-float", "清浮动"} {
|
||||
for _, want := range []string{"members-only", "reply-only", "data-gate", "回复可见", "data-image-group", "data-layout", "data-clear-float", "清浮动"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("缺少 %q,得到: %q", want, out)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user