|
@@ -1,150 +0,0 @@
|
|
|
-package main
|
|
|
-
|
|
|
-import (
|
|
|
- "bytes"
|
|
|
- "context"
|
|
|
- "errors"
|
|
|
- "fmt"
|
|
|
- "os"
|
|
|
- "os/signal"
|
|
|
- "runtime"
|
|
|
- "strings"
|
|
|
- "syscall"
|
|
|
- "text/template"
|
|
|
- "time"
|
|
|
-
|
|
|
- "github.com/denisbrodbeck/striphtmltags"
|
|
|
- log "github.com/go-pkgz/lgr"
|
|
|
- flags "github.com/jessevdk/go-flags"
|
|
|
-
|
|
|
- "github.com/umputun/rss2twitter/app/publisher"
|
|
|
- "github.com/umputun/rss2twitter/app/rss"
|
|
|
-)
|
|
|
-
|
|
|
-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" 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"`
|
|
|
- Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
|
|
-}
|
|
|
-
|
|
|
-var revision = "unknown"
|
|
|
-
|
|
|
-type notifier interface {
|
|
|
- Go(ctx context.Context) <-chan rss.Event
|
|
|
-}
|
|
|
-
|
|
|
-func main() {
|
|
|
- fmt.Printf("rss2twitter - %s\n", revision)
|
|
|
- o := opts{}
|
|
|
- if _, err := flags.Parse(&o); err != nil {
|
|
|
- os.Exit(1)
|
|
|
- }
|
|
|
-
|
|
|
- if o.Dbg {
|
|
|
- log.Setup(log.Debug)
|
|
|
- }
|
|
|
-
|
|
|
- notif, pub, err := setup(o)
|
|
|
- if err != nil {
|
|
|
- log.Printf("[PANIC] failed to setup, %v", err)
|
|
|
- }
|
|
|
-
|
|
|
- ctx, cancel := context.WithCancel(context.Background())
|
|
|
- go func() { // catch SIGTERM signal and invoke graceful termination
|
|
|
- stop := make(chan os.Signal, 1)
|
|
|
- signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
|
|
- <-stop
|
|
|
- log.Printf("[WARN] interrupt signal")
|
|
|
- cancel()
|
|
|
- }()
|
|
|
-
|
|
|
- do(ctx, notif, 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
|
|
|
-}
|
|
|
-
|
|
|
-// 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(tmpl)).Execute(&b1, event); err != nil {
|
|
|
- // template failed to parse record, backup predefined format
|
|
|
- return fmt.Sprintf("%s - %s", r.Title, r.Link)
|
|
|
- }
|
|
|
- return strings.Replace(format(b1.String(), 275), `\n`, "\n", -1) // \n in template
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- log.Printf("[WARN] failed to publish, %s", err)
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// format cleans text (removes html tags) and shrinks result
|
|
|
-func format(inp string, max int) string {
|
|
|
- res := striphtmltags.StripTags(inp)
|
|
|
- if len([]rune(res)) > max {
|
|
|
- snippet := []rune(res)[:max]
|
|
|
- // go back in snippet and found first space
|
|
|
- for i := len(snippet) - 1; i >= 0; i-- {
|
|
|
- if snippet[i] == ' ' {
|
|
|
- snippet = snippet[:i]
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
- res = string(snippet) + " ..."
|
|
|
- }
|
|
|
- return res
|
|
|
-}
|
|
|
-
|
|
|
-// getDump reads runtime stack and returns as a string
|
|
|
-func getDump() string {
|
|
|
- maxSize := 5 * 1024 * 1024
|
|
|
- stacktrace := make([]byte, maxSize)
|
|
|
- length := runtime.Stack(stacktrace, true)
|
|
|
- if length > maxSize {
|
|
|
- length = maxSize
|
|
|
- }
|
|
|
- return string(stacktrace[:length])
|
|
|
-}
|
|
|
-
|
|
|
-func init() {
|
|
|
- // catch SIGQUIT and print stack traces
|
|
|
- sigChan := make(chan os.Signal)
|
|
|
- go func() {
|
|
|
- for range sigChan {
|
|
|
- log.Printf("[INFO] SIGQUIT detected, dump:\n%s", getDump())
|
|
|
- }
|
|
|
- }()
|
|
|
- signal.Notify(sigChan, syscall.SIGQUIT)
|
|
|
-}
|