Skip to content

Commit

Permalink
feat: 优化 rss2newsletter 代码
Browse files Browse the repository at this point in the history
  • Loading branch information
xbpk3t committed Dec 16, 2024
1 parent e999b71 commit 7c19d00
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 98 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,4 @@ alfred/.workflow/prefs.plist
test
/qs.md
/coverage.out
/rss2newsletter/rss2newsletter.yml
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ includes:
merge:
taskfile: ./gh-merge/Taskfile.yml
dir: gh-merge
rss2newsletter:
r2n:
taskfile: ./rss2newsletter/Taskfile.yml
dir: rss2newsletter

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/samber/lo v1.47.0
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.18.2
golang.org/x/sync v0.8.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCR
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
Expand Down
216 changes: 119 additions & 97 deletions rss2newsletter/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (
"context"
"embed"
"fmt"
"github.com/xbpk3t/docs-alfred/utils"
"html/template"
"log"
"log/slog"
"os"
"sync"
Expand All @@ -18,137 +16,161 @@ import (
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/xbpk3t/docs-alfred/rss2newsletter/pkg"
"github.com/xbpk3t/docs-alfred/utils"
"golang.org/x/sync/errgroup"
)

var wg sync.WaitGroup
// 配置文件路径
var cfgFile string

//go:embed templates/newsletter.tpl
var newsletterTpl embed.FS

// rootCmd represents the base command when called without any subcommands
// EmailConfig 邮件配置
type EmailConfig struct {
From string
To []string
Token string
}

// rootCmd 根命令
var rootCmd = &cobra.Command{
Use: "rss2newsletter",
Short: "A brief description of your application",
Run: func(cmd *cobra.Command, args []string) {
f := pkg.NewConfig(cfgFile)

var res []feeds2.RssFeed

for _, feed := range f.Feeds {
wg.Add(1)
go func(feed pkg.FeedsDetail) {
defer wg.Done()
TypeName := feed.Type
feeds := feed.Urls

// 拼接urls
urls := lo.Map(feeds, func(item pkg.Feeds, index int) string {
return item.Feed
})

// 移除一些feed为空字符串的item
urls = lo.Compact(urls)

allFeeds := f.FetchURLs(urls)
if len(allFeeds) == 0 {
slog.Info("No feed Found", slog.String("Feed Type:", TypeName))
return
}

// 使用MergeAllFeeds合并feeds
combinedFeed, err := f.MergeAllFeeds(TypeName, allFeeds)
Short: "RSS订阅转换为邮件推送工具",
RunE: func(cmd *cobra.Command, args []string) error {
config := pkg.NewConfig(cfgFile)

// 使用 errgroup 进行并发处理
g, _ := errgroup.WithContext(context.Background())
var results []feeds2.RssFeed
var mu sync.Mutex // 保护 results

for _, feed := range config.Feeds {
feed := feed // 避免闭包问题
g.Go(func() error {
rssFeed, err := processFeed(feed, config)
if err != nil {
slog.Info("Merge Feeds Error:", slog.Any("Error", err))
return
return err
}

// 将合并后的Feed转换为所需的Feed格式,并填充Des和URL字段
newFeeds := make([]*feeds2.RssItem, len(combinedFeed.Items))
for i, item := range combinedFeed.Items {
var title string
if !f.Newsletter.IsHideAuthorInTitle && item.Author.Name != "" {
title = fmt.Sprintf("[%s] %s", item.Author.Name, item.Title)
} else {
title = item.Title
}

newFeeds[i] = &feeds2.RssItem{
Title: title,
Link: item.Link.Href,
Category: TypeName, // 使用分类的Type作为Name
PubDate: utils.FormatDate(item.Created),
}
}
mu.Lock()
results = append(results, rssFeed)
mu.Unlock()
return nil
})
}

// 将新的Feeds添加到结果中
res = append(res, feeds2.RssFeed{
Category: TypeName,
Items: newFeeds,
})
}(feed)
if err := g.Wait(); err != nil {
return err
}
wg.Wait()

// 从嵌入的文件系统加载模板
tmpl, err := template.ParseFS(newsletterTpl, "templates/newsletter.tpl")
// 渲染模板
content, err := renderNewsletter(results)
if err != nil {
log.Fatalf("[newsletter] Parse template error: %v", err)
return err
}

// 创建一个用于存储模板渲染结果的缓冲区
var tplBytes bytes.Buffer
// 执行模板渲染,将结果写入缓冲区
if err := tmpl.Execute(&tplBytes, res); err != nil {
log.Fatalf("[newsletter] Render template error: %v", err)
// 发送邮件
emailCfg := EmailConfig{
From: "Acme <onboarding@resend.dev>",
To: []string{"jeffcottlu@gmail.com"},
Token: config.Resend.Token,
}
// 渲染后的字符串现在存储在 tplBytes 中
renderedString := tplBytes.String()

// 打印出渲染后的字符串,或者根据需要进行其他操作
// fmt.Println(renderedString)

sendMailByResend(f.Resend.Token, renderedString)
return sendNewsletter(emailCfg, content)
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
// processFeed 处理单个Feed源
func processFeed(feed pkg.FeedsDetail, config *pkg.Config) (feeds2.RssFeed, error) {
TypeName := feed.Type
urls := lo.Compact(lo.Map(feed.Urls, func(item pkg.Feeds, _ int) string {
return item.Feed
}))

allFeeds := config.FetchURLs(urls)
if len(allFeeds) == 0 {
return feeds2.RssFeed{}, fmt.Errorf("no feed found for type: %s", TypeName)
}

combinedFeed, err := config.MergeAllFeeds(TypeName, allFeeds)
if err != nil {
os.Exit(1)
return feeds2.RssFeed{}, fmt.Errorf("merge feeds error: %w", err)
}

return convertToRssFeed(TypeName, combinedFeed, config.Newsletter.IsHideAuthorInTitle), nil
}

var cfgFile string
// convertToRssFeed 将Feed转换为RssFeed格式
func convertToRssFeed(typeName string, combinedFeed *feeds2.Feed, hideAuthor bool) feeds2.RssFeed {
newFeeds := make([]*feeds2.RssItem, len(combinedFeed.Items))
for i, item := range combinedFeed.Items {
title := getItemTitle(item, hideAuthor)
newFeeds[i] = &feeds2.RssItem{
Title: title,
Link: item.Link.Href,
Category: typeName,
PubDate: utils.FormatDate(item.Created),
}
}
return feeds2.RssFeed{
Category: typeName,
Items: newFeeds,
}
}

func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// getItemTitle 生成文章标题
func getItemTitle(item *feeds2.Item, hideAuthor bool) string {
if !hideAuthor && item.Author.Name != "" {
return fmt.Sprintf("[%s] %s", item.Author.Name, item.Title)
}
return item.Title
}

// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.rss2newsletter.yaml)")
// renderNewsletter 渲染邮件模板
func renderNewsletter(feeds []feeds2.RssFeed) (string, error) {
tmpl, err := template.ParseFS(newsletterTpl, "templates/newsletter.tpl")
if err != nil {
return "", fmt.Errorf("解析模板失败: %w", err)
}

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "rss2newsletter.yml", "config file path")
var tplBytes bytes.Buffer
if err := tmpl.Execute(&tplBytes, feeds); err != nil {
return "", fmt.Errorf("渲染模板失败: %w", err)
}

return tplBytes.String(), nil
}

func sendMailByResend(token, renderedString string) {
ctx := context.TODO()
client := resend.NewClient(token)
// sendNewsletter 发送邮件
func sendNewsletter(emailCfg EmailConfig, content string) error {
ctx := context.Background()
client := resend.NewClient(emailCfg.Token)

params := &resend.SendEmailRequest{
From: "Acme <onboarding@resend.dev>",
To: []string{"jeffcottlu@gmail.com"},
Subject: fmt.Sprintf("new items on %s (w%d)", carbon.Now().ToDateString(), utils.WeekNumOfYear()),
Html: renderedString,
From: emailCfg.From,
To: emailCfg.To,
Subject: fmt.Sprintf("新内容更新 %s (第%d周)", carbon.Now().ToDateString(), utils.WeekNumOfYear()),
Html: content,
}

sent, err := client.Emails.SendWithContext(ctx, params)
if err != nil {
panic(err)
return fmt.Errorf("发送邮件失败: %w", err)
}
fmt.Println(sent.Id)
slog.Info("邮件发送成功", "id", sent.Id)
return nil
}

// Execute 执行根命令
func Execute() {
if err := rootCmd.Execute(); err != nil {
slog.Error("执行命令失败", "error", err)
os.Exit(1)
}
}

func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "rss2newsletter.yml", "配置文件路径")
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

0 comments on commit 7c19d00

Please sign in to comment.