1 // feeder -- newsfeeds aggregator
2 // Copyright (C) 2022-2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
32 "github.com/mmcdole/gofeed"
36 maxEntries := flag.Uint("max-entries", 0, "Max entries to process (0=unlimited)")
39 fp := gofeed.NewParser()
40 feed, err := fp.Parse(os.Stdin)
45 guids := make(map[string]struct{}, len(feed.Items))
47 for _, item := range feed.Items {
48 if _, exists := guids[item.GUID]; exists {
52 guids[item.GUID] = struct{}{}
56 feedTitle := feed.Title
57 if len(feedTitle) == 0 {
58 feedTitle, err = filepath.Abs(mdir)
62 feedTitle = path.Base(feedTitle)
69 latest := &time.Time{}
70 for n, item := range feed.Items {
71 if *maxEntries > 0 && n == int(*maxEntries) {
75 if item.PublishedParsed != nil {
76 when = item.PublishedParsed
77 } else if item.UpdatedParsed != nil {
78 when = item.UpdatedParsed
82 if latest.Before(*when) {
86 if len(item.Content) > len(item.Description) {
89 what = item.Description
91 if media, ok := item.Extensions["media"]; ok {
92 if mediagroups, ok := media["group"]; ok {
93 if len(mediagroups) == 1 {
94 if mediadescription, ok := mediagroups[0].Children["description"]; ok {
95 if len(mediadescription[0].Value) > len(what) {
96 what = mediadescription[0].Value
102 what = strings.TrimPrefix(what, "<![CDATA[")
103 what = strings.TrimSuffix(what, "]]>")
106 h.Write([]byte(item.GUID))
108 h.Write([]byte(item.Title))
110 h.Write([]byte(what))
112 fn := hex.EncodeToString(h.Sum(nil)[:sha512.Size/2])
114 for _, d := range []string{"cur", "new"} {
115 entries, err := os.ReadDir(path.Join(mdir, d))
119 for _, entry := range entries {
120 if strings.HasPrefix(entry.Name(), fn) {
129 fn = path.Join(mdir, "new", fn)
130 fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
134 fd.WriteString("From: \"" + feedTitle + "\" <feeder@localhost>\n")
135 fd.WriteString("Date: " + when.UTC().Format(time.RFC1123Z) + "\n")
136 fd.WriteString("Subject: " + mime.BEncoding.Encode("UTF-8", item.Title) + "\n")
137 fd.WriteString("MIME-Version: 1.0\n")
138 fd.WriteString("Content-Type: text/html; charset=utf-8\n")
139 fd.WriteString("Content-Transfer-Encoding: base64\n")
140 for _, author := range item.Authors {
141 if len(author.Name) > 0 {
142 fd.WriteString("X-Author: " + author.Name + "\n")
145 for _, link := range item.Links {
146 fd.WriteString("X-URL: " + link + "\n")
148 for _, enc := range item.Enclosures {
149 fd.WriteString("X-Enclosure: " + enc.URL + "\n")
151 if len(item.Categories) > 0 {
152 fd.WriteString("X-Categories: " + strings.Join(item.Categories, ", ") + "\n")
155 what = base64.StdEncoding.EncodeToString([]byte(what))
156 for i := 0; i < len(what); i += 72 {
161 fd.WriteString(what[i:b] + "\n")
164 if err = os.Chtimes(fn, *when, *when); err != nil {
169 for _, d := range []string{"cur", "new"} {
170 if err = os.Chtimes(path.Join(mdir, d), *latest, *latest); err != nil {
174 fmt.Println(feedTitle)