Skip to content

Commit 23c32fc

Browse files
authored
Merge pull request #264 from Jinvic/dev
feature: 实现RSS功能
2 parents fa5d9e9 + b46121d commit 23c32fc

File tree

8 files changed

+279
-3
lines changed

8 files changed

+279
-3
lines changed

backend/go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ require (
2626
gorm.io/gorm v1.25.11
2727
)
2828

29+
require (
30+
github.com/aymerick/douceur v0.2.0 // indirect
31+
github.com/gorilla/css v1.0.1 // indirect
32+
)
33+
2934
require (
3035
github.com/BurntSushi/toml v1.4.0 // indirect
3136
github.com/KyleBanks/depth v1.2.1 // indirect
@@ -52,13 +57,16 @@ require (
5257
github.com/go-openapi/spec v0.21.0 // indirect
5358
github.com/go-openapi/swag v0.23.0 // indirect
5459
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
60+
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62
61+
github.com/gorilla/feeds v1.2.0
5562
github.com/jinzhu/inflection v1.0.0 // indirect
5663
github.com/jinzhu/now v1.1.5 // indirect
5764
github.com/josharian/intern v1.0.0 // indirect
5865
github.com/labstack/gommon v0.4.2 // indirect
5966
github.com/mailru/easyjson v0.7.7 // indirect
6067
github.com/mattn/go-colorable v0.1.13 // indirect
6168
github.com/mattn/go-isatty v0.0.20 // indirect
69+
github.com/microcosm-cc/bluemonday v1.0.27
6270
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
6371
github.com/samber/go-type-to-string v1.7.0 // indirect
6472
github.com/swaggo/files/v2 v2.0.1 // indirect

backend/go.sum

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
4343
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
4444
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
4545
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
46+
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
47+
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
4648
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4749
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4850
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -71,10 +73,16 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
7173
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
7274
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
7375
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
76+
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g=
77+
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
7478
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
7579
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
7680
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
7781
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
82+
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
83+
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
84+
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
85+
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
7886
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
7987
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
8088
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -101,6 +109,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
101109
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
102110
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
103111
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
112+
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
113+
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
104114
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
105115
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
106116
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -211,4 +221,4 @@ modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
211221
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
212222
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
213223
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
214-
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
224+
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=

backend/handler/rss.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package handler
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
"time"
9+
10+
"github.com/kingwrcy/moments/db"
11+
"github.com/kingwrcy/moments/vo"
12+
"github.com/labstack/echo/v4"
13+
"github.com/samber/do/v2"
14+
"gorm.io/gorm"
15+
16+
"github.com/gomarkdown/markdown"
17+
"github.com/gomarkdown/markdown/parser"
18+
"github.com/gorilla/feeds"
19+
"github.com/microcosm-cc/bluemonday"
20+
)
21+
22+
type RssHandler struct {
23+
base BaseHandler
24+
hc http.Client
25+
}
26+
27+
func NewRssHandler(injector do.Injector) *RssHandler {
28+
return &RssHandler{
29+
base: do.MustInvoke[BaseHandler](injector),
30+
hc: http.Client{},
31+
}
32+
}
33+
34+
func (r RssHandler) GetRss(c echo.Context) error {
35+
frontendHost := c.QueryParam("frontend_host")
36+
if frontendHost == "" {
37+
frontendHost = c.Request().Host // 如果未传递,则使用后端默认的 Host
38+
}
39+
rss, err := r.generateRss(frontendHost)
40+
if err != nil {
41+
return FailRespWithMsg(c, Fail, "RSS生成失败")
42+
}
43+
return c.String(http.StatusOK, rss)
44+
}
45+
46+
func (r RssHandler) generateRss(host string) (string, error) {
47+
var (
48+
memos []db.Memo
49+
user db.User
50+
sysConfig db.SysConfig
51+
sysConfigVO vo.FullSysConfigVO
52+
)
53+
54+
// 获取系统设置
55+
r.base.db.First(&sysConfig)
56+
_ = json.Unmarshal([]byte(sysConfig.Content), &sysConfigVO)
57+
58+
// 获取管理员信息
59+
r.base.db.First(&user, "Username = ?", "admin")
60+
61+
// 使用自定义RSS
62+
if sysConfigVO.Rss != "" {
63+
return "", nil
64+
}
65+
66+
// 查询动态
67+
tx := r.base.db.Preload("User", func(x *gorm.DB) *gorm.DB {
68+
return x.Select("username", "nickname", "id")
69+
}).Where("pinned = 0")
70+
tx.Order("createdAt desc").Limit(10).Find(&memos)
71+
72+
feed := generateFeed(memos, &sysConfigVO, &user, host)
73+
74+
return feed.ToRss()
75+
76+
// // 将RSS内容写入/rss/default_rss.xml
77+
// target := "/rss/default_rss.xml"
78+
// dir := filepath.Dir(target)
79+
// if err := os.MkdirAll(dir, os.ModePerm); err != nil {
80+
// return "", fmt.Errorf("创建目录失败: %w", err)
81+
// }
82+
// if err := os.WriteFile(target, []byte(rss), 0644); err != nil {
83+
// return "", fmt.Errorf("写入RSS失败: %w", err)
84+
// }
85+
}
86+
87+
func generateFeed(memos []db.Memo, sysConfigVO *vo.FullSysConfigVO, user *db.User, host string) *feeds.Feed {
88+
now := time.Now()
89+
feed := &feeds.Feed{
90+
Title: sysConfigVO.Title,
91+
Link: &feeds.Link{Href: fmt.Sprintf("%s/rss", host)},
92+
Description: user.Slogan,
93+
Author: &feeds.Author{Name: user.Nickname},
94+
Created: now,
95+
}
96+
97+
feed.Items = []*feeds.Item{}
98+
for _, memo := range memos {
99+
feed.Items = append(feed.Items, &feeds.Item{
100+
Title: fmt.Sprintf("Memo #%d", memo.Id),
101+
Link: &feeds.Link{Href: fmt.Sprintf("%s/memo/%d", host, memo.Id)},
102+
Description: parseMarkdownToHtml(getContentWithExt(memo, host)),
103+
Author: &feeds.Author{Name: memo.User.Nickname},
104+
Created: *memo.CreatedAt,
105+
Updated: *memo.UpdatedAt,
106+
})
107+
}
108+
return feed
109+
}
110+
111+
func parseMarkdownToHtml(md string) string {
112+
// 启用扩展
113+
extensions := parser.NoIntraEmphasis | // 忽略单词内部的强调标记
114+
parser.Tables | // 解析表格语法
115+
parser.FencedCode | // 解析围栏代码块
116+
parser.Strikethrough | // 支持删除线语法
117+
parser.HardLineBreak | // 将换行符(\n)转换为 <br> 标签
118+
parser.Footnotes | // 支持脚注语法
119+
parser.MathJax | // 支持 MathJax 数学公式语法
120+
parser.SuperSubscript | // 支持上标和下标语法
121+
parser.EmptyLinesBreakList // 允许两个空行中断列表
122+
p := parser.NewWithExtensions(extensions)
123+
124+
// 将 Markdown 解析为 HTML
125+
html := markdown.ToHTML([]byte(md), p, nil)
126+
127+
// 清理 HTML(防止 XSS 攻击)
128+
cleanHTML := bluemonday.UGCPolicy().SanitizeBytes(html)
129+
130+
return string(cleanHTML)
131+
}
132+
133+
func getContentWithExt(memo db.Memo, host string) string {
134+
content := memo.Content
135+
136+
// 处理链接
137+
if memo.ExternalUrl != "" {
138+
content += fmt.Sprintf("\n\n[%s](%s)", memo.ExternalTitle, memo.ExternalUrl)
139+
}
140+
141+
// 处理图片
142+
if memo.Imgs != "" {
143+
imgs := strings.Split(memo.Imgs, ",")
144+
for _, img := range imgs {
145+
if img[:7] == "/upload" {
146+
img = host + img
147+
}
148+
content += fmt.Sprintf("\n\n![%s](%s)", img, img)
149+
}
150+
}
151+
152+
var ext vo.MemoExt
153+
err := json.Unmarshal([]byte(memo.Ext), &ext)
154+
if err != nil {
155+
ext = vo.MemoExt{
156+
Music: vo.Music{},
157+
Video: vo.Video{},
158+
DoubanBook: vo.DoubanBook{},
159+
DoubanMovie: vo.DoubanMovie{},
160+
}
161+
}
162+
163+
// 处理音乐
164+
if ext.Music.Server != "" {
165+
var title, url string
166+
switch ext.Music.Server {
167+
// 网易云音乐
168+
case "netease":
169+
title = "网易云音乐"
170+
switch ext.Music.Type {
171+
case "search":
172+
ext.Music.ID = "/m/?s=" + ext.Music.ID
173+
default:
174+
ext.Music.ID = "?id=" + ext.Music.ID
175+
}
176+
url = fmt.Sprintf("https://music.163.com/#/%s%s",
177+
ext.Music.Type, ext.Music.ID)
178+
// QQ音乐
179+
case "tencent":
180+
title = "QQ音乐"
181+
switch ext.Music.Type {
182+
case "song":
183+
url = fmt.Sprintf("https://y.qq.com/n/ryqq/songDetail/%s", ext.Music.ID)
184+
case "playlist":
185+
url = fmt.Sprintf("https://y.qq.com/n/ryqq/playlist/%s", ext.Music.ID)
186+
case "album":
187+
url = fmt.Sprintf("https://y.qq.com/n/ryqq/albumDetail/%s", ext.Music.ID)
188+
case "search":
189+
url = fmt.Sprintf("https://y.qq.com/n/ryqq/search?w=%s&t=song", ext.Music.ID)
190+
case "artist":
191+
url = fmt.Sprintf("https://y.qq.com/n/ryqq/singer/%s", ext.Music.ID)
192+
default:
193+
}
194+
// 酷狗音乐
195+
case "kugou":
196+
title = "酷狗音乐"
197+
switch ext.Music.Type {
198+
case "song":
199+
url = fmt.Sprintf("https://www.kugou.com/mixsong/%s.html", ext.Music.ID)
200+
case "playlist":
201+
url = fmt.Sprintf("https://www.kugou.com/songlist/%s/", ext.Music.ID)
202+
case "album":
203+
url = fmt.Sprintf("https://www.kugou.com/album/info/%s/", ext.Music.ID)
204+
case "search":
205+
url = fmt.Sprintf("https://www.kugou.com/yy/html/search.html#searchType=song&searchKeyWord=%s", ext.Music.ID)
206+
case "artist":
207+
url = fmt.Sprintf("https://www.kugou.com/singer/info/%s/", ext.Music.ID)
208+
default:
209+
}
210+
// 虾米音乐 已停止服务
211+
case "xiami":
212+
// 百度音乐 不可用
213+
case "baidu":
214+
default:
215+
}
216+
217+
if url != "" {
218+
content += fmt.Sprintf("\n\n[%s](%s)", title, url)
219+
}
220+
}
221+
222+
// 处理视频
223+
if ext.Video.Type != "" {
224+
var title, url string
225+
switch ext.Video.Type {
226+
case "online":
227+
title = "在线视频"
228+
case "bilibili":
229+
title = "Bilibili视频"
230+
case "youtube":
231+
title = "Youtube视频"
232+
}
233+
url = ext.Video.Value
234+
if ext.Video.Type == "online" && url[:7] == "/upload" {
235+
url = host + url
236+
}
237+
content += fmt.Sprintf("\n\n[%s](%s)", title, url)
238+
}
239+
240+
// 处理豆瓣
241+
if ext.DoubanBook.Url != "" {
242+
content += fmt.Sprintf("\n\n[%s](%s)", ext.DoubanBook.Title, ext.DoubanBook.Url)
243+
}
244+
if ext.DoubanMovie.Url != "" {
245+
content += fmt.Sprintf("\n\n[%s](%s)", ext.DoubanMovie.Title, ext.DoubanMovie.Url)
246+
}
247+
248+
return content
249+
}

backend/router.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func setupRouter(injector do.Injector) {
1616
sycConfigHandler := handler.NewSysConfigHandler(injector)
1717
fileHandler := handler.NewFileHandler(injector)
1818
tagHandler := handler.NewTagHandler(injector)
19+
rssHandler := handler.NewRssHandler(injector)
1920
e := do.MustInvoke[*echo.Echo](injector)
2021
cfg := do.MustInvoke[*vo.AppConfig](injector)
2122

@@ -64,6 +65,9 @@ func setupRouter(injector do.Injector) {
6465
Browse: false,
6566
}))
6667

68+
rssGroup := e.Group("/rss")
69+
rssGroup.GET("", rssHandler.GetRss)
70+
6771
if cfg.EnableSwagger {
6872
e.GET("/swagger/*", echoSwagger.WrapHandler)
6973
}

front/layouts/default.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ useHead({
7676
rel: 'alternate',
7777
type: 'application/rss+xml',
7878
title: '我的 RSS 订阅',
79-
href: sysConfigVO.rss || '',
79+
href: sysConfigVO.rss || `/rss?frontend_host=${encodeURIComponent(window.location.origin)}`,
8080
},
8181
],
8282
style: [

front/nuxt.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export default defineNuxtConfig({
5454
target: "http://localhost:37892",
5555
changeOrigin: true,
5656
},
57+
"/rss": {
58+
target: "http://localhost:37892",
59+
changeOrigin: true,
60+
},
5761
"/swagger": {
5862
target: "http://localhost:37892",
5963
changeOrigin: true,

front/pages/sys/settings.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<UTextarea v-model="state.js" :rows="5"/>
3838
</UFormGroup>
3939
<UFormGroup label="自定义RSS" name="rss" :ui="{label:{base:'font-bold'}}">
40-
<UTextarea v-model="state.rss" :rows="1"/>
40+
<UTextarea v-model="state.rss" :rows="1" placeholder="留空使用默认配置"/>
4141
</UFormGroup>
4242
<UFormGroup label="评论最大字数" name="maxCommentLength" :ui="{label:{base:'font-bold'}}">
4343
<UInput v-model.number="state.maxCommentLength"/>

front/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export type SysConfigVO = {
5858
beiAnNo: string,
5959
css: string,
6060
js: string,
61+
rss: string,
6162
enableAutoLoadNextPage: boolean
6263
enableS3: boolean
6364
enableRegister: boolean

0 commit comments

Comments
 (0)