Add initial prototype
This commit is contained in:
85
internal/config.go
Normal file
85
internal/config.go
Normal 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
36
internal/config_test.go
Normal 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
18
internal/controller.go
Normal 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
79
internal/rss.go
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user