8000 支持生成 RSS 订阅 by Jinvic · Pull Request #264 · kingwrcy/moments · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

支持生成 RSS 订阅 #264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ require (
gorm.io/gorm v1.25.11
)

require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
)

require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
Expand All @@ -52,13 +57,16 @@ require (
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62
github.com/gorilla/feeds v1.2.0
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microcosm-cc/bluemonday v1.0.27
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/samber/go-type-to-string v1.7.0 // indirect
github.com/swaggo/files/v2 v2.0.1 // indirect
Expand Down
12 changes: 11 additions & 1 deletion backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -71,10 +73,16 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g=
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
Expand All @@ -101,6 +109,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -211,4 +221,4 @@ modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
249 changes: 249 additions & 0 deletions backend/handler/rss.go
< 10000 tr data-hunk="1923e4c3a0ec74f87fe6dc4f8c0db9d5d4ba951ee6bb9acaa097a0aad4e7f9b8" class="show-top-border">
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package handler

import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"

"github.com/kingwrcy/moments/db"
"github.com/kingwrcy/moments/vo"
"github.com/labstack/echo/v4"
"github.com/samber/do/v2"
"gorm.io/gorm"

"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/parser"
"github.com/gorilla/feeds"
"github.com/microcosm-cc/bluemonday"
)

type RssHandler struct {
base BaseHandler
hc http.Client
}

func NewRssHandler(injector do.Injector) *RssHandler {
return &RssHandler{
base: do.MustInvoke[BaseHandler](injector),
hc: http.Client{},
}
}

func (r RssHandler) GetRss(c echo.Context) error {
frontendHost := c.QueryParam("frontend_host")
if frontendHost == "" {
frontendHost = c.Request().Host // 如果未传递,则使用后端默认的 Host
}
rss, err := r.generateRss(frontendHost)
if err != nil {
return FailRespWithMsg(c, Fail, "RSS生成失败")
}
return c.String(http.StatusOK, rss)
}

func (r RssHandler) generateRss(host string) (string, error) {
var (
memos []db.Memo
user db.User
sysConfig db.SysConfig
sysConfigVO vo.FullSysConfigVO
)

// 获取系统设置
r.base.db.First(&sysConfig)
_ = json.Unmarshal([]byte(sysConfig.Content), &sysConfigVO)

// 获取管理员信息
r.base.db.First(&user, "Username = ?", "admin")

// 使用自定义RSS
if sysConfigVO.Rss != "" {
return "", nil
}

// 查询动态
tx := r.base.db.Preload("User", func(x *gorm.DB) *gorm.DB {
return x.Select("username", "nickname", "id")
}).Where("pinned = 0")
tx.Order("createdAt desc").Limit(10).Find(&memos)

feed := generateFeed(memos, &sysConfigVO, &user, host)

return feed.ToRss()

// // 将RSS内容写入/rss/default_rss.xml
// target := "/rss/default_rss.xml"
// dir := filepath.Dir(target)
// if err := os.MkdirAll(dir, os.ModePerm); err != nil {
// return "", fmt.Errorf("创建目录失败: %w", err)
// }
// if err := os.WriteFile(target, []byte(rss), 0644); err != nil {
// return "", fmt.Errorf("写入RSS失败: %w", err)
// }
}

func generateFeed(memos []db.Memo, sysConfigVO *vo.FullSysConfigVO, user *db.User, host string) *feeds.Feed {
now := time.Now()
feed := &feeds.Feed{
Title: sysConfigVO.Title,
Link: &feeds.Link{Href: fmt.Sprintf("%s/rss", host)},
Description: user.Slogan,
Author: &feeds.Author{Name: user.Nickname},
Created: now,
}

feed.Items = []*feeds.Item{}
for _, memo := range memos {
feed.Items = append(feed.Items, &feeds.Item{
Title: fmt.Sprintf("Memo #%d", memo.Id),
Link: &feeds.Link{Href: fmt.Sprintf("%s/memo/%d", host, memo.Id)},
Description: parseMarkdownToHtml(getContentWithExt(memo, host)),
Author: &feeds.Author{Name: memo.User.Nickname},
Created: *memo.CreatedAt,
Updated: *memo.UpdatedAt,
})
}
return feed
}

func parseMarkdownToHtml(md string) string {
// 启用扩展
extensions := parser.NoIntraEmphasis | // 忽略单词内部的强调标记
parser.Tables | // 解析表格语法
parser.FencedCode | // 解析围栏代码块
parser.Strikethrough | // 支持删除线语法
parser.HardLineBreak | // 将换行符(\n)转换为 <br> 标签
parser.Footnotes | // 支持脚注语法
parser.MathJax | // 支持 MathJax 数学公式语法
parser.SuperSubscript | // 支持上标和下标语法
parser.EmptyLinesBreakList // 允许两个空行中断列表
p := parser.NewWithExtensions(extensions)

// 将 Markdown 解析为 HTML
html := markdown.ToHTML([]byte(md), p, nil)

// 清理 HTML(防止 XSS 攻击)
cleanHTML := bluemonday.UGCPolicy().SanitizeBytes(html)

return string(cleanHTML)
}

func getContentWithExt(memo db.Memo, host string) string {
content := memo.Content

// 处理链接
if memo.ExternalUrl != "" {
content += fmt.Sprintf("\n\n[%s](%s)", memo.ExternalTitle, memo.ExternalUrl)
}

// 处理图片
if memo.Imgs != "" {
imgs := strings.Split(memo.Imgs, ",")
for _, img := range imgs {
if img[:7] == "/upload" {
img = host + img
}
content += fmt.Sprintf("\n\n![%s](%s)", img, img)
}
}

var ext vo.MemoExt
err := json.Unmarshal([]byte(memo.Ext), &ext)
if err != nil {
ext = vo.MemoExt{
Music: vo.Music{},
Video: vo.Video{},
DoubanBook: vo.DoubanBook{},
DoubanMovie: vo.DoubanMovie{},
}
}

// 处理音乐
if ext.Music.Server != "" {
var title, url string
switch ext.Music.Server {
// 网易云音乐
case "netease":
title = "网易云音乐"
switch ext.Music.Type {
case "search":
ext.Music.ID = "/m/?s=" + ext.Music.ID
default:
ext.Music.ID = "?id=" + ext.Music.ID
}
url = fmt.Sprintf("https://music.163.com/#/%s%s",
ext.Music.Type, ext.Music.ID)
// QQ音乐
case "tencent":
title = "QQ音乐"
switch ext.Music.Type {
case "song":
url = fmt.Sprintf("https://y.qq.com/n/ryqq/songDetail/%s", ext.Music.ID)
case "playlist":
url = fmt.Sprintf("https://y.qq.com/n/ryqq/playlist/%s", ext.Music.ID)
case "album":
url = fmt.Sprintf("https://y.qq.com/n/ryqq/albumDetail/%s", ext.Music.ID)
case "search":
url = fmt.Sprintf("https://y.qq.com/n/ryqq/search?w=%s&t=song", ext.Music.ID)
case "artist":
url = fmt.Sprintf("https://y.qq.com/n/ryqq/singer/%s", ext.Music.ID)
default:
}
// 酷狗音乐
case "kugou":
title = "酷狗音乐"
switch ext.Music.Type {
case "song":
url = fmt.Sprintf("https://www.kugou.com/mixsong/%s.html", ext.Music.ID)
case "playlist":
url = fmt.Sprintf("https://www.kugou.com/songlist/%s/", ext.Music.ID)
case "album":
url = fmt.Sprintf("https://www.kugou.com/album/info/%s/", ext.Music.ID)
case "search":
url = fmt.Sprintf("https://www.kugou.com/yy/html/search.html#searchType=song&searchKeyWord=%s", ext.Music.ID)
case "artist":
url = fmt.Sprintf("https://www.kugou.com/singer/info/%s/", ext.Music.ID)
default:
}
// 虾米音乐 已停止服务
case "xiami":
// 百度音乐 不可用
case "baidu":
default:
}

if url != "" {
content += fmt.Sprintf("\n\n[%s](%s)", title, url)
}
}

// 处理视频
if ext.Video.Type != "" {
var title, url string
switch ext.Video.Type {
case "online":
title = "在线视频"
case "bilibili":
title = "Bilibili视频"
case "youtube":
title = "Youtube视频"
}
url = ext.Video.Value
if ext.Video.Type == "online" && url[:7] == "/upload" {
url = host + url
}
content += fmt.Sprintf("\n\n[%s](%s)", title, url)
}

// 处理豆瓣
if ext.DoubanBook.Url != "" {
content += fmt.Sprintf("\n\n[%s](%s)", ext.DoubanBook.Title, ext.DoubanBook.Url)
}
if ext.DoubanMovie.Url != "" {
content += fmt.Sprintf("\n\n[%s](%s)", ext.DoubanMovie.Title, ext.DoubanMovie.Url)
}

return content
}
4 changes: 4 additions & 0 deletions backend/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func setupRouter(injector do.Injector) {
sycConfigHandler := handler.NewSysConfigHandler(injector)
fileHandler := handler.NewFileHandler(injector)
tagHandler := handler.NewTagHandler(injector)
rssHandler := handler.NewRssHandler(injector)
e := do.MustInvoke[*echo.Echo](injector)
cfg := do.MustInvoke[*vo.AppConfig](injector)

Expand Down Expand Up @@ -64,6 +65,9 @@ func setupRouter(injector do.Injector) {
Browse: false,
}))

rssGroup := e.Group("/rss")
rssGroup.GET("", rssHandler.GetRss)

if cfg.EnableSwagger {
e.GET("/swagger/*", echoSwagger.WrapHandler)
}
Expand Down
2 changes: 1 addition & 1 deletion front/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ useHead({
rel: 'alternate',
type: 'application/rss+xml',
title: '我的 RSS 订阅',
href: sysConfigVO.rss || '',
href: sysConfigVO.rss || `/rss?frontend_host=${encodeURIComponent(window.location.origin)}`,
},
],
style: [
Expand Down
4 changes: 4 additions & 0 deletions front/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export default defineNuxtConfig({
target: "http://localhost:37892",
changeOrigin: true,
},
"/rss": {
target: "http://localhost:37892",
changeOrigin: true,
},
"/swagger": {
target: "http://localhost:37892",
changeOrigin: true,
Expand Down
2 changes: 1 addition & 1 deletion front/pages/sys/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<UTextarea v-model="state.js" :rows="5"/>
</UFormGroup>
<UFormGroup label="自定义RSS" name="rss" :ui="{label:{base:'font-bold'}}">
<UTextarea v-model="state.rss" :rows="1"/>
<UTextarea v-model="state.rss" :rows="1" placeholder="留空使用默认配置"/>
</UFormGroup>
<UFormGroup label="评论最大字数" name="maxCommentLength" :ui="{label:{base:'font-bold'}}">
<UInput v-model.number="state.maxCommentLength"/>
Expand Down
1 change: 1 addition & 0 deletions front/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type SysConfigVO = {
beiAnNo: string,
css: string,
js: string,
rss: string,
enableAutoLoadNextPage: boolean
enableS3: boolean
enableRegister: boolean
Expand Down
0