summaryrefslogblamecommitdiffstats
path: root/cedar.go
blob: ebd22c838c6caeae7e203b10b6b23aaa85bf70a4 (plain) (tree)
1
2
3
4
5
6
7
8
9


                                                           


            
                       
                      
              
             

                  
            
                 
              
                 

 
















                                          

                 

                                            

                  
             


                                                                                



                                 

                                                                  
                                

         
                     
                                  





                                                
                                        


                                                  
                        

 










                                           















                                                      












                                            
















                                                        
                                       









                                        












                                    















                                                            


                  








                                         
































                                                       


                  















                                       




                                              


                                                  
                 
                                        
         



                                



                  







                                      




                                                      
 
// SPDX-License-Identifier: ISC
// Copyright © 2020 rsiddharth <rsiddharth@ninthfloor.org>

package main

import (
	"encoding/json"
	"encoding/xml"
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path"
	"strings"
)

type Link struct {
	XMLName xml.Name `xml:"link"`
	Href    string   `xml:"href,attr"`
}

type Entry struct {
	XMLName xml.Name `xml:"entry"`
	Id      string   `xml:"id"`
	Title   string   `xml:"title"`
	Link    Link
}

type Feed struct {
	XMLName xml.Name `xml:"feed"`
	Entry   []Entry  `xml:"entry"`
}

type Ids []string

const sendmail string = "/usr/sbin/sendmail"

var emailTo string

func init() {
	flag.StringVar(&emailTo, "t", "", "Email address for sending emails to")
}

func newsFeed() ([]byte, error) {
	// Init feed.
	feed := make([]byte, 0)

	resp, err := http.Get("https://fsf.org.in/news/feed.atom")
	if err != nil {
		return feed, err
	}

	// Read feed.
	chunk := make([]byte, 100)
	for {
		c, err := resp.Body.Read(chunk)
		if c < 1 {
			break
		}
		if err != nil && err != io.EOF {
			return feed, err
		}
		feed = append(feed, chunk[0:c]...)
	}
	return feed, nil
}

func parseFeed(feed []byte) (Feed, error) {
	f := Feed{}

	err := xml.Unmarshal(feed, &f)
	if err != nil {
		return f, err
	}

	return f, nil
}

func readFile(f *os.File) ([]byte, error) {
	bs, chunk := make([]byte, 0), make([]byte, 10)
	for {
		n, err := f.Read(chunk)
		if err != nil && err != io.EOF {
			return bs, err
		}
		bs = append(bs, chunk[0:n]...)

		if err == io.EOF {
			break
		}
	}
	return bs, nil
}

func writeFile(f os.File, cache Ids) error {
	bs, err := json.Marshal(cache)
	if err != nil {
		return err
	}

	n, err := f.Write(bs)
	if n != len(bs) {
		return err
	}
	return nil
}

func cacheFor(section string) (Ids, error) {
	cache := make(Ids, 0)

	h, _ := os.UserHomeDir()
	d := path.Join(h, ".cedar")

	err := os.MkdirAll(d, 0700)
	if err != nil {
		return cache, err
	}

	f, err := os.Open(path.Join(d, section+".json"))
	if os.IsNotExist(err) {
		return cache, nil
	}

	bs, err := readFile(f)
	if len(bs) == 0 || err != nil {
		return cache, err
	}

	err = json.Unmarshal(bs, &cache)
	if err != nil {
		return cache, err
	}
	return cache, nil
}

func (cache *Ids) add(entry Entry) {
	n := len(*cache)

	// Expand cache
	c := make(Ids, n+1)
	copy(c, *cache)

	// Cache entry
	c[n] = entry.Id

	*cache = c
}

func (cache Ids) save(section string) error {
	h, _ := os.UserHomeDir()
	d := path.Join(h, ".cedar")

	f, err := os.OpenFile(path.Join(d, section+".json"),
		os.O_CREATE|os.O_WRONLY,
		0600)
	if err != nil {
		return err
	}

	err = writeFile(*f, cache)
	if err != nil {
		return err
	}

	return nil
}

func (entry Entry) in(cache Ids) bool {
	for i := 0; i < len(cache); i++ {
		if entry.Id == cache[i] {
			return true
		}
	}
	return false
}

func (entry Entry) makeEmail(section string) string {
	return fmt.Sprintf(`To: %s
Subject: FSF India - %s - %s

FSF India published "%s":

   %s
`,
		emailTo,
		strings.Title(section),
		entry.Title,
		entry.Title,
		entry.Link.Href)
}

func (entry Entry) email(section string) error {
	cmd := exec.Command(sendmail, "-t")

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return err
	}

	io.WriteString(stdin, entry.makeEmail(section))
	stdin.Close()

	_, err = cmd.CombinedOutput()
	if err != nil {
		return err
	}
	fmt.Printf("Successfully sent %s to %s\n",
		entry.Id, emailTo)

	return nil
}

func processNews() error {
	newsXML, err := newsFeed()
	if err != nil {
		return err
	}

	news, err := parseFeed(newsXML)
	if err != nil {
		return err
	}

	cache, err := cacheFor("news")
	if err != nil {
		return err
	}

	for i := 0; i < len(news.Entry); i++ {
		if news.Entry[i].in(cache) {
			continue
		}

		err := news.Entry[i].email("news")
		if err != nil {
			return err
		}
		cache.add(news.Entry[i])
	}
	err = cache.save("news")
	if err != nil {
		return err
	}

	return nil
}

func main() {
	flag.Parse()

	// Quit if emailTo is not set.
	if flag.NFlag() != 1 {
		flag.PrintDefaults()
		return
	}

	err := processNews()
	if err != nil {
		fmt.Printf("Error: %v\n", err.Error())
	}
}