split main, test parts
This commit is contained in:
parent
8847871bf2
commit
c5dc899751
5 changed files with 136 additions and 28 deletions
|
@ -14,7 +14,7 @@ Use provided `docker-compose.yml` and change `FEED` value. All twitter-api crede
|
|||
- `{{.Link}}` - rss link
|
||||
- `{{.Text}}` - item description
|
||||
|
||||
_default is ` {{.Title}} - {{.Link}}`
|
||||
_default is `{{.Title}} - {{.Link}}`_
|
||||
|
||||
## Parameters
|
||||
|
||||
|
|
63
app/main.go
63
app/main.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -19,15 +20,15 @@ import (
|
|||
"github.com/umputun/rss2twitter/app/rss"
|
||||
)
|
||||
|
||||
var opts struct {
|
||||
type opts struct {
|
||||
Refresh time.Duration `short:"r" long:"refresh" env:"REFRESH" default:"30s" description:"refresh interval"`
|
||||
TimeOut time.Duration `short:"t" long:"timeout" env:"TIMEOUT" default:"5s" description:"rss feed timeout"`
|
||||
Feed string `short:"f" long:"feed" env:"FEED" required:"true" description:"rss feed url"`
|
||||
|
||||
ConsumerKey string `long:"consumer-key" env:"TWI_CONSUMER_KEY" required:"true" description:"twitter consumer key"`
|
||||
ConsumerSecret string `long:"consumer-secret" env:"TWI_CONSUMER_SECRET" required:"true" description:"twitter consumer secret"`
|
||||
AccessToken string `long:"access-token" env:"TWI_ACCESS_TOKEN" required:"true" description:"twitter access token"`
|
||||
AccessSecret string `long:"access-secret" env:"TWI_ACCESS_SECRET" required:"true" description:"twitter access secret"`
|
||||
ConsumerKey string `long:"consumer-key" env:"TWI_CONSUMER_KEY" description:"twitter consumer key"`
|
||||
ConsumerSecret string `long:"consumer-secret" env:"TWI_CONSUMER_SECRET" description:"twitter consumer secret"`
|
||||
AccessToken string `long:"access-token" env:"TWI_ACCESS_TOKEN" description:"twitter access token"`
|
||||
AccessSecret string `long:"access-secret" env:"TWI_ACCESS_SECRET" description:"twitter access secret"`
|
||||
|
||||
Template string `long:"template" env:"TEMPLATE" default:"{{.Title}} - {{.Link}}" description:"twitter message template"`
|
||||
Dry bool `long:"dry" env:"DRY" description:"dry mode"`
|
||||
|
@ -36,36 +37,59 @@ var opts struct {
|
|||
|
||||
var revision = "unknown"
|
||||
|
||||
type notifier interface {
|
||||
Go(ctx context.Context) <-chan rss.Event
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("rss2twitter - %s\n", revision)
|
||||
if _, err := flags.Parse(&opts); err != nil {
|
||||
o := opts{}
|
||||
if _, err := flags.Parse(&o); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if opts.Dbg {
|
||||
if o.Dbg {
|
||||
log.Setup(log.Debug)
|
||||
}
|
||||
|
||||
notifier := rss.Notify{Feed: opts.Feed, Duration: opts.Refresh, Timeout: opts.TimeOut}
|
||||
var pub publisher.Interface = publisher.Twitter{
|
||||
ConsumerKey: opts.ConsumerKey,
|
||||
ConsumerSecret: opts.ConsumerSecret,
|
||||
AccessToken: opts.AccessToken,
|
||||
AccessSecret: opts.AccessSecret,
|
||||
notifier, pub, err := setup(o)
|
||||
if err != nil {
|
||||
log.Printf("[PANIC] failed to setup, %v", err)
|
||||
}
|
||||
|
||||
if opts.Dry { // override publisher to stdout only, no actual twitter publishing
|
||||
pub = publisher.Stdout{}
|
||||
do(context.Background(), notifier, pub, o.Template)
|
||||
|
||||
log.Print("[INFO] terminated")
|
||||
}
|
||||
|
||||
func setup(o opts) (n notifier, p publisher.Interface, err error) {
|
||||
n = &rss.Notify{Feed: o.Feed, Duration: o.Refresh, Timeout: o.TimeOut}
|
||||
p = publisher.Twitter{
|
||||
ConsumerKey: o.ConsumerKey,
|
||||
ConsumerSecret: o.ConsumerSecret,
|
||||
AccessToken: o.AccessToken,
|
||||
AccessSecret: o.AccessSecret,
|
||||
}
|
||||
|
||||
if o.Dry { // override publisher to stdout only, no actual twitter publishing
|
||||
p = publisher.Stdout{}
|
||||
log.Print("[INFO] dry mode")
|
||||
} else {
|
||||
if o.ConsumerKey == "" || o.ConsumerSecret == "" || o.AccessToken == "" || o.AccessSecret == "" {
|
||||
return n, p, errors.New("token credentials missing")
|
||||
}
|
||||
}
|
||||
return n, p, nil
|
||||
}
|
||||
|
||||
log.Printf("[INFO] message template - %q", opts.Template)
|
||||
|
||||
ch := notifier.Go(context.Background())
|
||||
// do runs event loop getting rss events and publishing them
|
||||
func do(ctx context.Context, notif notifier, pub publisher.Interface, tmpl string) {
|
||||
log.Printf("[INFO] message template - %q", tmpl)
|
||||
ch := notif.Go(ctx)
|
||||
for event := range ch {
|
||||
err := pub.Publish(event, func(r rss.Event) string {
|
||||
b1 := bytes.Buffer{}
|
||||
if err := template.Must(template.New("twi").Parse(opts.Template)).Execute(&b1, event); err != nil {
|
||||
if err := template.Must(template.New("twi").Parse(tmpl)).Execute(&b1, event); err != nil {
|
||||
// template failed to parse record, backup predefined format
|
||||
return fmt.Sprintf("%s - %s", r.Title, r.Link)
|
||||
}
|
||||
|
@ -75,7 +99,6 @@ func main() {
|
|||
log.Printf("[WARN] failed to publish, %s", err)
|
||||
}
|
||||
}
|
||||
log.Print("[INFO] terminated")
|
||||
}
|
||||
|
||||
// format cleans text (removes html tags) and shrinks result
|
||||
|
|
|
@ -1,12 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/umputun/rss2twitter/app/rss"
|
||||
)
|
||||
|
||||
func TestSetupDry(t *testing.T) {
|
||||
o := opts{Feed: "http://example.com", Dry: true}
|
||||
n, p, err := setup(o)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, n)
|
||||
assert.Equal(t, "publisher.Stdout", fmt.Sprintf("%T", p))
|
||||
}
|
||||
|
||||
func TestSetupFull(t *testing.T) {
|
||||
o := opts{Feed: "http://example.com", Dry: false,
|
||||
ConsumerKey: "1", ConsumerSecret: "1", AccessToken: "1", AccessSecret: "1"}
|
||||
n, p, err := setup(o)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, n)
|
||||
assert.Equal(t, "publisher.Twitter", fmt.Sprintf("%T", p))
|
||||
}
|
||||
|
||||
func TestSetupFailed(t *testing.T) {
|
||||
o := opts{Feed: "http://example.com", Dry: false,
|
||||
ConsumerKey: "1", ConsumerSecret: "1"}
|
||||
_, _, err := setup(o)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
pub := pubMock{buf: bytes.Buffer{}}
|
||||
notif := notifierMock{delay: 100 * time.Millisecond, events: []rss.Event{
|
||||
{GUID: "1", Title: "t1", Link: "l1", Text: "ttt2"},
|
||||
{GUID: "2", Title: "t2", Link: "l2", Text: "ttt2"},
|
||||
{GUID: "3", Title: "t4", Link: "l3", Text: "ttt3"},
|
||||
}}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
do(ctx, ¬if, &pub, "{{.Title}} - {{.Link}}")
|
||||
cancel()
|
||||
assert.Equal(t, "t1 - l1\nt2 - l2\nt4 - l3\n", pub.buf.String())
|
||||
}
|
||||
|
||||
func TestDoCanceled(t *testing.T) {
|
||||
pub := pubMock{buf: bytes.Buffer{}}
|
||||
notif := notifierMock{delay: 100 * time.Millisecond, events: []rss.Event{
|
||||
{GUID: "1", Title: "t1", Link: "l1", Text: "ttt2"},
|
||||
{GUID: "2", Title: "t2", Link: "l2", Text: "ttt2"},
|
||||
{GUID: "3", Title: "t4", Link: "l3", Text: "ttt3"},
|
||||
}}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
time.AfterFunc(time.Millisecond*150, func() { cancel() })
|
||||
do(ctx, ¬if, &pub, "{{.Title}} - {{.Link}}")
|
||||
assert.Equal(t, "t1 - l1\n", pub.buf.String())
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
tbl := []struct {
|
||||
inp string
|
||||
|
@ -27,3 +82,33 @@ func TestFormat(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type pubMock struct {
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (m *pubMock) Publish(event rss.Event, formatter func(rss.Event) string) error {
|
||||
_, err := m.buf.WriteString(formatter(event) + "\n")
|
||||
return err
|
||||
}
|
||||
|
||||
type notifierMock struct {
|
||||
events []rss.Event
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (m *notifierMock) Go(ctx context.Context) <-chan rss.Event {
|
||||
ch := make(chan rss.Event)
|
||||
go func() {
|
||||
for _, e := range m.events {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-time.After(m.delay):
|
||||
ch <- e
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ type Event struct {
|
|||
Title string
|
||||
Link string
|
||||
Text string
|
||||
guid string
|
||||
GUID string
|
||||
}
|
||||
|
||||
// Go starts notifier and returns events channel
|
||||
|
@ -69,14 +69,14 @@ func (n *Notify) Go(ctx context.Context) <-chan Event {
|
|||
continue
|
||||
}
|
||||
event, err := n.feedEvent(feedData)
|
||||
if lastGUID != event.guid && err == nil {
|
||||
if lastGUID != event.GUID && err == nil {
|
||||
if lastGUID != "" { // don't notify on initial change
|
||||
log.Printf("[INFO] new event %s - %s", event.guid, event.Title)
|
||||
log.Printf("[INFO] new event %s - %s", event.GUID, event.Title)
|
||||
ch <- event
|
||||
} else {
|
||||
log.Printf("[INFO] ignore first event %s - %s", event.guid, event.Title)
|
||||
log.Printf("[INFO] ignore first event %s - %s", event.GUID, event.Title)
|
||||
}
|
||||
lastGUID = event.guid
|
||||
lastGUID = event.GUID
|
||||
}
|
||||
if !waitOrCancel(n.ctx) {
|
||||
log.Print("[WARN] notifier canceled")
|
||||
|
@ -108,7 +108,7 @@ func (n *Notify) feedEvent(feed *gofeed.Feed) (e Event, err error) {
|
|||
e.Title = feed.Items[0].Title
|
||||
e.Link = feed.Items[0].Link
|
||||
e.Text = "\n" + feed.Items[0].Description
|
||||
e.guid = feed.Items[0].GUID
|
||||
e.GUID = feed.Items[0].GUID
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ func TestNotify(t *testing.T) {
|
|||
t.Logf("%+v", e)
|
||||
e.Text = ""
|
||||
assert.Equal(t, Event{ChanTitle: "Радио-Т", Title: "Радио-Т 626",
|
||||
Link: "https://radio-t.com/p/2018/12/01/podcast-626/", guid: "https://radio-t.com/p/2018/12/01//podcast-626/"}, e)
|
||||
Link: "https://radio-t.com/p/2018/12/01/podcast-626/", GUID: "https://radio-t.com/p/2018/12/01//podcast-626/"}, e)
|
||||
assert.True(t, time.Since(st) >= time.Millisecond*250)
|
||||
|
||||
select {
|
||||
|
|
Loading…
Reference in a new issue