Add initial prototype

This commit is contained in:
Dave Gallant
2020-03-31 21:31:52 -04:00
parent 89681da2ca
commit 6be207ffb7
15 changed files with 558 additions and 81 deletions

85
internal/config.go Normal file
View File

@@ -0,0 +1,85 @@
package internal
import (
"errors"
"io/ioutil"
"log"
"os"
"runtime"
"gopkg.in/yaml.v2"
)
// Configuration stores the global config
type Configuration struct {
Feeds []string `yaml:"feeds"`
ExternalViewer string `yaml:"externalViewer,omitempty"`
}
// DefaultConfiguration can be used if a config is missing
var DefaultConfiguration = Configuration{
Feeds: []string{
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/golang/.rss",
"https://www.zdnet.com/topic/security/rss.xml",
},
}
// Determines the default viewer
func DetermineExternalViewer() (string, error) {
switch os := runtime.GOOS; os {
case "linux":
return "xdg-open", nil
case "darwin":
return "open", nil
}
return "", errors.New("Unable to determine a default external viewer")
}
// LoadConfiguration takes a filename (configuration) and loads it.
func LoadConfiguration(file string) Configuration {
var config Configuration
// If the configuration file does not exist,
// write a default config
_, err := os.Stat(file)
if os.IsNotExist(err) {
WriteConfig(DefaultConfiguration, file)
}
data, err := ioutil.ReadFile(file)
if err != nil {
log.Println(err)
os.Exit(1)
}
if config.ExternalViewer == "" {
config.ExternalViewer, err = DetermineExternalViewer()
if err != nil {
log.Println(err)
os.Exit(1)
}
}
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Panicln(err)
}
return config
}
// WriteConfig writes a config to disk
func WriteConfig(config Configuration, file string) error {
c, err := yaml.Marshal(&config)
if err != nil {
log.Fatalf("Unable to marshal default config: %v", err)
}
err = ioutil.WriteFile(file, c, 0644)
if err != nil {
log.Fatalf("Unable to write default config: %v", err)
}
return nil
}

36
internal/config_test.go Normal file
View File

@@ -0,0 +1,36 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestLoadConfiguration tests loading the example config
func TestLoadConfiguration(t *testing.T) {
exampleConfig := LoadConfiguration("../config-example.yaml")
expectedFeeds := []string{
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/golang/.rss",
"https://www.reddit.com/r/linux/.rss",
"https://www.zdnet.com/topic/security/rss.xml",
"https://aws.amazon.com/blogs/security/feed/",
"https://www.archlinux.org/feeds/news/",
}
assert.Equal(
t,
expectedFeeds,
exampleConfig.Feeds,
"Expected configuration does not match.",
)
// ExternalViewer should default to either 'xdg-open' on Linux,
// or 'open' on macOS
assert.Contains(
t,
[]string{"xdg-open", "open"},
exampleConfig.ExternalViewer,
)
}

18
internal/controller.go Normal file
View File

@@ -0,0 +1,18 @@
package internal
import "time"
// Controller keeps everything together
type Controller struct {
Config Configuration
lastUpdate time.Time
Rss *RSS
}
// Init initiates the controller with config
func (c *Controller) Init(config string) {
c.Config = LoadConfiguration(config)
c.Rss = &RSS{}
c.Rss.New(c)
c.Rss.Update()
}

79
internal/rss.go Normal file
View File

@@ -0,0 +1,79 @@
package internal
import (
"fmt"
"log"
"net/http"
"sync"
browser "github.com/EDDYCJY/fake-useragent"
"github.com/mmcdole/gofeed"
)
type RSS struct {
Feeds []*gofeed.Feed
c *Controller
}
func (r *RSS) New(c *Controller) {
r.c = c
}
// Update fetches all articles for all feeds
func (r *RSS) Update() {
fp := gofeed.NewParser()
var wg sync.WaitGroup
var mux sync.Mutex
r.Feeds = []*gofeed.Feed{}
for _, f := range r.c.Config.Feeds {
f := f
wg.Add(1)
go func() {
defer wg.Done()
feed, err := r.FetchURL(fp, f)
if err != nil {
log.Printf("error fetching url: %s, err: %v", f, err)
}
mux.Lock()
if feed != nil {
r.Feeds = append(r.Feeds, feed)
}
mux.Unlock()
}()
}
wg.Wait()
}
// FetchURL fetches the feed URL and parses it
func (r *RSS) FetchURL(fp *gofeed.Parser, url string) (feed *gofeed.Feed, err error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
userAgent := browser.Firefox()
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp != nil {
defer func() {
ce := resp.Body.Close()
if ce != nil {
err = ce
}
}()
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("Failed to get url %v, %v", resp.StatusCode, resp.Status)
}
return fp.Parse(resp.Body)
}