~gjabell/mfn

60b7af8cf04d70202fe33f731633968f971180ee — Galen Abell 1 year, 2 months ago fd0ff95
Refactor to allow multiple notification targets
4 files changed, 120 insertions(+), 70 deletions(-)

M config.go
M main.go
A notify.go
A webhook.go
M config.go => config.go +4 -4
@@ 7,7 7,7 @@ import (
	"github.com/BurntSushi/toml"
)

type config struct {
type Config struct {
	Server             string             `toml:"server"`
	Username           string             `toml:"username"`
	Password           string             `toml:"password"`


@@ 17,8 17,8 @@ type config struct {
	WebhookURL         string             `toml:"webhook_url"`
}

func loadConfig(configPath string) (*config, error) {
	var c config
func LoadConfig(configPath string) (*Config, error) {
	var c Config

	if _, err := toml.DecodeFile(configPath, &c); err != nil {
		return nil, err


@@ 31,7 31,7 @@ func loadConfig(configPath string) (*config, error) {
	return &c, nil
}

func (c *config) validate() error {
func (c *Config) validate() error {
	for k, v := range map[string]string{
		"server url":       c.Server,
		"username":         c.Username,

M main.go => main.go +37 -66
@@ 1,17 1,12 @@
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/getopt"



@@ 19,7 14,11 @@ import (
)

var version = "0.1.0"
var webhookTemplate *template.Template

type Mfn struct {
	conf      *Config
	notifiers map[string]Notifier
}

func loadLastEntry(filepath string) (int64, error) {
	bytes, err := ioutil.ReadFile(filepath)


@@ 44,55 43,13 @@ func saveLastEntry(filepath string, id int64) error {
	return ioutil.WriteFile(filepath, []byte(strconv.FormatInt(id, 10)), 0644)
}

func sendWebhook(entry *mf.Entry, webhookURL string) error {
	var text bytes.Buffer
	if err := webhookTemplate.Execute(&text, entry); err != nil {
		return err
	}

	body := map[string]string{"text": text.String(), "format": "html", "displayName": "Miniflux", "avatarUrl": "https://miniflux.app/favicon.ico"}
	jsonVal, err := json.Marshal(body)
	if err != nil {
		return err
	}

	res, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonVal))
	if err != nil {
		return err
	}
	resBytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil
	}
	log.Printf("Webhook response: %s", string(resBytes))

	return nil
}

func testWebhook(webhookURL string) error {
	e := &mf.Entry{
		ID:         0,
		UserID:     0,
		FeedID:     0,
		Status:     "unread",
		Hash:       "deadbeef",
		Title:      "Test Entry",
		URL:        "https://miniflux.app",
		Date:       time.Now(),
		Content:    "This is a test miniflux entry",
		Author:     "Author",
		Starred:    false,
		Enclosures: make([]*mf.Enclosure, 0),
		Feed:       nil,
	}

	return sendWebhook(e, webhookURL)
}

func main() {
	showHelp := getopt.Bool("h", false, "show usage")
	webhookTest := getopt.Bool("t", false, "test the webhook endpoint")
	showVersion := getopt.Bool("v", false, "show version")

	var testNotifier string
	getopt.StringVar(&testNotifier, "t", "", "test a notifier")

	var configPath string
	getopt.StringVar(&configPath, "c", "", "configuration file path")



@@ 102,7 59,7 @@ func main() {
	}

	if *showHelp {
		fmt.Println("Usage: mfn [-v] [-t] -c config")
		fmt.Println("Usage: mfn [-v] [-t endpoint] -c config")
		os.Exit(0)
	}



@@ 115,29 72,41 @@ func main() {
		log.Fatal("You must specify a config location")
	}

	conf, err := loadConfig(configPath)
	conf, err := LoadConfig(configPath)
	if err != nil {
		log.Fatalf("Failed to load config: %s\n", err)
	}
	webhookTemplate = conf.WebhookTemplate

	if *webhookTest {
		log.Println("Sending test webhook...")
		if err := testWebhook(conf.WebhookURL); err != nil {
			log.Fatalf("Test webhook failed: %s\n", err)
	notifiers := make(map[string]Notifier)
	notifiers["webhook"] = NewWebhook(conf)

	mfn := &Mfn{
		conf:      conf,
		notifiers: notifiers,
	}

	if testNotifier != "" {
		notifier, ok := mfn.notifiers[testNotifier]
		if !ok {
			log.Fatalf("Invalid notifier: %s\n", testNotifier)
		}

		log.Println("Sending test notification...")
		if err := notifier.Test(); err != nil {
			log.Fatalf("Test notification failed: %s\n", err)
		}
		os.Exit(0)
	}

	if err := execNotifications(conf); err != nil {
	if err := mfn.execNotifications(); err != nil {
		log.Fatal(err)
	}
}

func execNotifications(conf *config) error {
	client := mf.New(conf.Server, conf.Username, conf.Password)
func (mfn *Mfn) execNotifications() error {
	client := mf.New(mfn.conf.Server, mfn.conf.Username, mfn.conf.Password)

	lastEntry, err := loadLastEntry(conf.DbPath)
	lastEntry, err := loadLastEntry(mfn.conf.DbPath)
	if err != nil {
		return fmt.Errorf("failed to load last entry: %s", err)
	}


@@ 178,12 147,14 @@ func execNotifications(conf *config) error {
			continue
		}

		if err := sendWebhook(e, conf.WebhookURL); err != nil {
			log.Printf("Failed to send webhook: %s\n", err)
		for name, notifier := range mfn.notifiers {
			if err := notifier.Notify(e); err != nil {
				log.Printf("Failed to send %s notification: %s\n", name, err)
			}
		}
	}
	log.Printf("Saving new last entry: %d\n", lastEntry)
	if err := saveLastEntry(conf.DbPath, lastEntry); err != nil {
	if err := saveLastEntry(mfn.conf.DbPath, lastEntry); err != nil {
		return fmt.Errorf("failed to save new entries: %s", err)
	}


A notify.go => notify.go +8 -0
@@ 0,0 1,8 @@
package main

import mf "miniflux.app/client"

type Notifier interface {
	Notify(*mf.Entry) error
	Test() error
}

A webhook.go => webhook.go +71 -0
@@ 0,0 1,71 @@
package main

import (
	"bytes"
	"encoding/json"
	"html/template"
	"io/ioutil"
	"log"
	"net/http"
	"time"

	mf "miniflux.app/client"
)

type Webhook struct {
	// Notifier
	template *template.Template
	url      string
}

func NewWebhook(conf *Config) *Webhook {
	return &Webhook{
		template: conf.WebhookTemplate,
		url:      conf.WebhookURL,
	}
}

func (w *Webhook) Notify(entry *mf.Entry) error {
	var text bytes.Buffer
	if err := w.template.Execute(&text, entry); err != nil {
		return err
	}

	body := map[string]string{"text": text.String(), "format": "html", "displayName": "Miniflux", "avatarUrl": "https://miniflux.app/favicon.ico"}
	jsonVal, err := json.Marshal(body)
	if err != nil {
		return err
	}

	res, err := http.Post(w.url, "application/json", bytes.NewBuffer(jsonVal))
	if err != nil {
		return err
	}
	resBytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil
	}
	log.Printf("Webhook response: %s", string(resBytes))

	return nil
}

func (w *Webhook) Test() error {
	e := &mf.Entry{
		ID:         0,
		UserID:     0,
		FeedID:     0,
		Status:     "unread",
		Hash:       "deadbeef",
		Title:      "Test Entry",
		URL:        "https://miniflux.app",
		Date:       time.Now(),
		Content:    "This is a test miniflux entry",
		Author:     "Author",
		Starred:    false,
		Enclosures: make([]*mf.Enclosure, 0),
		Feed:       nil,
	}

	return w.Notify(e)
}