/* feeder -- newsfeeds aggregator Copyright (C) 2022 Sergey Matveev This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( "crypto/sha512" "encoding/base64" "encoding/hex" "flag" "fmt" "log" "mime" "os" "path" "path/filepath" "strings" "time" "github.com/mmcdole/gofeed" ) func main() { maxEntries := flag.Uint("max-entries", 0, "Max entries to process (0=unlimited)") flag.Parse() mdir := flag.Arg(0) fp := gofeed.NewParser() feed, err := fp.Parse(os.Stdin) if err != nil { log.Fatalln(err) } guids := make(map[string]struct{}, len(feed.Items)) useGUID := true for _, item := range feed.Items { if _, exists := guids[item.GUID]; exists { useGUID = false break } else { guids[item.GUID] = struct{}{} } } feedTitle := feed.Title if len(feedTitle) == 0 { feedTitle, err = filepath.Abs(mdir) if err != nil { log.Fatalln(err) } feedTitle = path.Base(feedTitle) } h := sha512.New() news := 0 var when *time.Time now := time.Now() latest := &time.Time{} for n, item := range feed.Items { if *maxEntries > 0 && n == int(*maxEntries) { break } when = nil if item.PublishedParsed != nil { when = item.PublishedParsed } else if item.UpdatedParsed != nil { when = item.UpdatedParsed } else { when = &now } if latest.Before(*when) { latest = when } var what string if len(item.Content) == 0 { what = item.Description } else { what = item.Content } what = strings.TrimPrefix(what, "") h.Reset() if useGUID { h.Write([]byte(item.GUID)) } else { h.Write([]byte(item.Title)) h.Write([]byte{0}) h.Write([]byte(what)) } fn := hex.EncodeToString(h.Sum(nil)[:sha512.Size/2]) exists := false for _, d := range []string{"cur", "new"} { entries, err := os.ReadDir(path.Join(mdir, d)) if err != nil { log.Fatalln(err) } for _, entry := range entries { if strings.HasPrefix(entry.Name(), fn) { exists = true break } } } if exists { continue } fn = path.Join(mdir, "new", fn) fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.FileMode(0666)) if err != nil { log.Fatalln(err) } fd.WriteString("From: \"" + feedTitle + "\" \n") fd.WriteString("Date: " + when.UTC().Format(time.RFC1123Z) + "\n") fd.WriteString("Subject: " + mime.BEncoding.Encode("UTF-8", item.Title) + "\n") fd.WriteString("MIME-Version: 1.0\n") fd.WriteString("Content-Type: text/html; charset=utf-8\n") fd.WriteString("Content-Transfer-Encoding: base64\n") for _, author := range item.Authors { if len(author.Name) > 0 { fd.WriteString("X-Author: " + author.Name + "\n") } } for _, link := range item.Links { fd.WriteString("X-URL: " + link + "\n") } for _, enc := range item.Enclosures { fd.WriteString("X-Enclosure: " + enc.URL + "\n") } if len(item.Categories) > 0 { fd.WriteString("X-Categories: " + strings.Join(item.Categories, ", ") + "\n") } fd.WriteString("\n") what = base64.StdEncoding.EncodeToString([]byte(what)) for i := 0; i < len(what); i += 72 { b := i + 72 if b > len(what) { b = len(what) } fd.WriteString(what[i:b] + "\n") } fd.Close() if err = os.Chtimes(fn, *when, *when); err != nil { log.Fatalln(err) } news++ } for _, d := range []string{"cur", "new"} { if err = os.Chtimes(path.Join(mdir, d), *latest, *latest); err != nil { log.Fatalln(err) } } fmt.Println(feedTitle) }