commit 478409cf707d6f64be29f6ed3fe683c4700f9e63 Author: jolheiser Date: Tue Mar 23 20:28:06 2021 -0500 Initial commit Signed-off-by: jolheiser diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..c2ba578 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,42 @@ +--- +kind: pipeline +name: compliance +trigger: + event: + - pull_request +steps: + - name: build + pull: always + image: golang:1.16 + commands: + - make test + - make build + - name: check + pull: always + image: golang:1.16 + commands: + - make vet + +--- +kind: pipeline +name: release +trigger: + event: + - push + branch: + - master +steps: + - name: build + pull: always + image: golang:1.16 + commands: + - make build + - name: gitea-release + pull: always + image: jolheiser/drone-gitea-main:latest + settings: + token: + from_secret: gitea_token + base: https://gitea.com + files: + - "lurk" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..515fddb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# GoLand +.idea/ + +# Binaries +/lurk* + +!lurk.sample.toml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..433f7db --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b355d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: build +build: + go build + +.PHONY: fmt +fmt: + go fmt ./... + +.PHONY: test +test: + go test -race ./... + +.PHONY: vet +vet: + go vet ./... \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..23446c3 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# lurk + +A lurker used to notify a Discord channel via webhook +whenever a matching submission is made. + +See the [example config](lurk.sample.toml). \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..3187291 --- /dev/null +++ b/config/config.go @@ -0,0 +1,71 @@ +package config + +import ( + "regexp" + "strings" + + "github.com/pelletier/go-toml" + "go.jolheiser.com/beaver" +) + +type Config struct { + Reddit RedditConfig `toml:"reddit"` + Twitter TwitterConfig `toml:"twitter"` +} + +func (c *Config) loadReddit() { + for _, sub := range c.Reddit.SubReddits { + c.Reddit.Map[strings.ToLower(sub.Name)] = sub + if sub.TitleLimit == 0 || sub.TitleLimit > 253 { + sub.TitleLimit = 253 + } + if sub.BodyLimit == 0 || sub.BodyLimit > 2045 { + sub.BodyLimit = 2045 + } + sub.FlairAllowlistRe = make([]*regexp.Regexp, len(sub.FlairAllowlist)) + for idx, f := range sub.FlairAllowlist { + sub.FlairAllowlistRe[idx] = regexp.MustCompile(f) + } + sub.FlairBlocklistRe = make([]*regexp.Regexp, len(sub.FlairBlocklist)) + for idx, f := range sub.FlairBlocklist { + sub.FlairBlocklistRe[idx] = regexp.MustCompile(f) + } + sub.TitleAllowlistRe = make([]*regexp.Regexp, len(sub.TitleAllowlist)) + for idx, t := range sub.TitleAllowlist { + sub.TitleAllowlistRe[idx] = regexp.MustCompile(t) + } + sub.TitleBlocklistRe = make([]*regexp.Regexp, len(sub.TitleBlocklist)) + for idx, t := range sub.TitleBlocklist { + sub.TitleBlocklistRe[idx] = regexp.MustCompile(t) + } + sub.BodyAllowlistRe = make([]*regexp.Regexp, len(sub.BodyAllowlist)) + for idx, b := range sub.BodyAllowlist { + sub.BodyAllowlistRe[idx] = regexp.MustCompile(b) + } + sub.BodyBlocklistRe = make([]*regexp.Regexp, len(sub.BodyBlocklist)) + for idx, b := range sub.BodyBlocklist { + sub.BodyBlocklistRe[idx] = regexp.MustCompile(b) + } + } +} + +func Load(configPath string) (*Config, error) { + cfg := Config{ + Reddit: RedditConfig{ + Map: make(map[string]*SubReddit), + }, + } + + tree, err := toml.LoadFile(configPath) + if err != nil { + return nil, err + } + + if err := tree.Unmarshal(&cfg); err != nil { + return nil, err + } + + cfg.loadReddit() + beaver.Debug(cfg) + return &cfg, nil +} diff --git a/config/reddit.go b/config/reddit.go new file mode 100644 index 0000000..81437b0 --- /dev/null +++ b/config/reddit.go @@ -0,0 +1,53 @@ +package config + +import ( + "fmt" + "regexp" +) + +type RedditConfig struct { + SubReddits []*SubReddit `toml:"sub"` + Map map[string]*SubReddit `toml:"-"` + + // Agent file + AppName string `toml:"app_name"` + Version string `toml:"version"` + + ClientID string `toml:"client_id"` + ClientSecret string `toml:"client_secret"` + + Username string `toml:"username"` + Password string `toml:"password"` +} + +type SubReddit struct { + Name string `toml:"name"` + IconURL string `toml:"icon_url"` + FlairAllowlist []string `toml:"flair_allowlist"` + FlairAllowlistRe []*regexp.Regexp `toml:"-"` + FlairBlocklist []string `toml:"flair_blocklist"` + FlairBlocklistRe []*regexp.Regexp `toml:"-"` + TitleAllowlist []string `toml:"title_allowlist"` + TitleAllowlistRe []*regexp.Regexp `toml:"-"` + TitleBlocklist []string `toml:"title_blocklist"` + TitleBlocklistRe []*regexp.Regexp `toml:"-"` + TitleLimit int `toml:"title_limit"` + BodyAllowlist []string `toml:"body_allowlist"` + BodyAllowlistRe []*regexp.Regexp `toml:"-"` + BodyBlocklist []string `toml:"body_blocklist"` + BodyBlocklistRe []*regexp.Regexp `toml:"-"` + BodyLimit int `toml:"body_limit"` + Webhook string `toml:"webhook"` +} + +func (r *RedditConfig) UserAgent() string { + return fmt.Sprintf("%s/%s by /u/%s", r.AppName, r.Version, r.Username) +} + +func (r *RedditConfig) SubRedditNames() []string { + names := make([]string, len(r.SubReddits)) + for idx, sub := range r.SubReddits { + names[idx] = sub.Name + } + return names +} diff --git a/config/twitter.go b/config/twitter.go new file mode 100644 index 0000000..782cc72 --- /dev/null +++ b/config/twitter.go @@ -0,0 +1,18 @@ +package config + +type TwitterConfig struct { + ConsumerKey string `toml:"consumer_key"` + ConsumerSecret string `toml:"consumer_secret"` + AccessToken string `toml:"access_token"` + AccessSecret string `toml:"access_secret"` + + Filters []Filter `toml:"filter"` +} + +type Filter struct { + Follows []string `toml:"follows"` + FollowStrict bool `toml:"follow_strict"` + Locations []string `toml:"locations"` + Tracks []string `toml:"tracks"` + Webhook string `toml:"webhook"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e79abc1 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module go.jolheiser.com/lurk + +go 1.16 + +require ( + github.com/dghubble/go-twitter v0.0.0-20200725221434-4bc8ad7ad1b4 + github.com/dghubble/oauth1 v0.6.0 + github.com/pelletier/go-toml v1.8.1 + github.com/turnage/graw v0.0.0-20200404033202-65715eea1cd0 + go.jolheiser.com/beaver v1.0.2 + go.jolheiser.com/disco v0.0.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7abafca --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dghubble/go-twitter v0.0.0-20200725221434-4bc8ad7ad1b4 h1:I60CX3+rWlBGPXR13jwxxBWB2GhlhZkEEh5o1eDLzJg= +github.com/dghubble/go-twitter v0.0.0-20200725221434-4bc8ad7ad1b4/go.mod h1:xfg4uS5LEzOj8PgZV7SQYRHbG7jPUnelEiaAVJxmhJE= +github.com/dghubble/oauth1 v0.6.0 h1:m1yC01Ohc/eF38jwZ8JUjL1a+XHHXtGQgK+MxQbmSx0= +github.com/dghubble/oauth1 v0.6.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk= +github.com/dghubble/sling v1.3.0 h1:pZHjCJq4zJvc6qVQ5wN1jo5oNZlNE0+8T/h0XeXBUKU= +github.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/turnage/graw v0.0.0-20200404033202-65715eea1cd0 h1:ss0FREpyYvw5V9t5XWWWIvUJnRgWFzdftvCXd8WQNhU= +github.com/turnage/graw v0.0.0-20200404033202-65715eea1cd0/go.mod h1:aAkq4I/q1izZSSwHvzhDn9NA+eGxgTSuibwP3MZRlQY= +github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb h1:qR56NGRvs2hTUbkn6QF8bEJzxPIoMw3Np3UigBeJO5A= +github.com/turnage/redditproto v0.0.0-20151223012412-afedf1b6eddb/go.mod h1:GyqJdEoZSNoxKDb7Z2Lu/bX63jtFukwpaTP9ZIS5Ei0= +go.jolheiser.com/beaver v1.0.2 h1:KA2D6iO8MQhZi1nZYi/Chak/f1Cxfrs6b1XO623+Khk= +go.jolheiser.com/beaver v1.0.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g= +go.jolheiser.com/disco v0.0.2 h1:UGYNqO7NQSBGB/OoS9WE5o/jYvmx1G0Bq3qQRM42Bkw= +go.jolheiser.com/disco v0.0.2/go.mod h1:tY3HkJmMrzXH/bPgDWKHn1DUzDxkemD80OHLgHSA5uQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/handler/reddit.go b/handler/reddit.go new file mode 100644 index 0000000..0fcc741 --- /dev/null +++ b/handler/reddit.go @@ -0,0 +1,122 @@ +package handler + +import ( + "context" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "go.jolheiser.com/lurk/config" + + "github.com/turnage/graw/reddit" + "go.jolheiser.com/beaver" + "go.jolheiser.com/disco" +) + +var httpClient = &http.Client{Timeout: time.Minute} + +type Reddit struct { + Config *config.Config +} + +func (r *Reddit) Post(p *reddit.Post) error { + sub := r.Config.Reddit.Map[strings.ToLower(p.Subreddit)] + + if err := checkPost(r.Config, p); err != nil { + beaver.Debugf("%s: %v", p.Subreddit, err) + return nil + } + + title := p.Title + if len(title) > sub.TitleLimit { + title = title[:sub.TitleLimit] + "..." + } + description := p.SelfText + if len(description) > sub.BodyLimit { + description = description[:sub.BodyLimit] + "..." + } + + e := disco.Webhook{ + Username: "/r/" + p.Subreddit, + AvatarURL: sub.IconURL, + Embeds: []*disco.Embed{ + { + Title: title, + URL: p.URL, + Description: description, + Color: 0x007D96, + Timestamp: disco.Now(), + Author: &disco.Author{ + Name: "/u/" + p.Author, + URL: fmt.Sprintf("https://reddit.com/user/%s", p.Author), + }, + }, + }, + } + + if sub.Webhook == "" { + beaver.Errorf("no webhook for %s", p.Subreddit) + return nil + } + + req, err := e.Request(context.Background(), sub.Webhook) + if err != nil { + beaver.Error(err) + return nil + } + + resp, err := httpClient.Do(req) + if err != nil { + beaver.Error(err) + return nil + } + + if resp.StatusCode != http.StatusNoContent { + beaver.Error(resp.Status) + return nil + } + + return nil +} + +func checkPost(c *config.Config, p *reddit.Post) error { + sub := c.Reddit.Map[strings.ToLower(p.Subreddit)] + + // Check blocklist first + // Any match means we ignore + if matchesAny(p.LinkFlairText, sub.FlairBlocklistRe) { + return fmt.Errorf("flair matched blocklisted regex: %s", p.LinkFlairText) + } + if matchesAny(p.Title, sub.TitleBlocklistRe) { + return fmt.Errorf("title matched blocklisted regex: %s", p.Title) + } + if matchesAny(p.SelfText, sub.BodyBlocklistRe) { + return fmt.Errorf("body matched blocklisted regex: %s", p.SelfText) + } + + // Check allowlist + // Any match means we pass + // If no allowlist, pass + if len(sub.FlairAllowlistRe) > 0 && !matchesAny(p.LinkFlairText, sub.FlairAllowlistRe) { + return fmt.Errorf("flair didn't match any allowlisted regex: %s", p.LinkFlairText) + } + if len(sub.TitleAllowlistRe) > 0 && !matchesAny(p.Title, sub.TitleAllowlistRe) { + return fmt.Errorf("title didn't match any allowlisted regex: %s", p.Title) + } + if len(sub.BodyAllowlistRe) > 0 && !matchesAny(p.SelfText, sub.BodyAllowlistRe) { + return fmt.Errorf("body didn't match any allowlisted regex: %s", p.SelfText) + } + + return nil +} + +func matchesAny(input string, re []*regexp.Regexp) bool { + for _, r := range re { + if r.MatchString(input) { + return true + } + } + return false +} diff --git a/handler/twitter.go b/handler/twitter.go new file mode 100644 index 0000000..74baab1 --- /dev/null +++ b/handler/twitter.go @@ -0,0 +1,57 @@ +package handler + +import ( + "context" + "fmt" + + "go.jolheiser.com/lurk/config" + + "github.com/dghubble/go-twitter/twitter" + "go.jolheiser.com/beaver" + "go.jolheiser.com/disco" +) + +type Twitter struct { + Filter config.Filter + Stream *twitter.Stream +} + +func (t *Twitter) Run() { + beaver.Debugf("setting up stream for %v", t.Filter) + demux := twitter.NewSwitchDemux() + demux.Tweet = t.Tweet + + beaver.Debugf("streaming %v", t.Filter) + demux.HandleChan(t.Stream.Messages) + beaver.Debugf("disconnected from stream: %v", t.Filter) +} + +func (t *Twitter) Tweet(tweet *twitter.Tweet) { + beaver.Debugf("new tweet for %v", t.Filter) + + if t.Filter.FollowStrict { + if tweet.InReplyToStatusIDStr != "" { + beaver.Debug("tweet is a reply") + return + } + var match bool + for _, id := range t.Filter.Follows { + if id == tweet.User.IDStr { + match = true + break + } + } + if !match { + beaver.Debug("tweet did not match any follow IDs") + return + } + } + + w := &disco.Webhook{ + Username: tweet.User.Name, + Content: fmt.Sprintf("https://twitter.com/%d/status/%d", tweet.User.ID, tweet.ID), + } + if _, err := w.Send(context.Background(), t.Filter.Webhook); err != nil { + beaver.Error(err) + } +} diff --git a/lurk.sample.toml b/lurk.sample.toml new file mode 100644 index 0000000..55f86d1 --- /dev/null +++ b/lurk.sample.toml @@ -0,0 +1,48 @@ +# Reddit information +[reddit] + # Reddit Agent Info + app_name = "Lurk" + version = "0.1" + + # https://www.reddit.com/prefs/apps + client_id = "" + client_secret = "" + + # Reddit credentials + username = "" + password = "" + + # A list of subreddites to monitor + # allow/blocklist can be omitted in real config if empty + [[reddit.sub]] + name = "" + icon_url = "" + flair_allowlist = [] + flair_blocklist = [] + title_allowlist = [] + title_blocklist = [] + title_limit = 0 + body_allowlist = [] + body_blocklist = [] + body_limit = 0 + webhook = "" + +# Twitter information +[twitter] + # Auth + consumer_key = "" + consumer_secret = "" + access_token = "" + access_secret = "" + + # A list of filters to watch for + # Empty fields can be omitted in a real config + [[twitter.filter]] + # follows must use a Twitter user's ID + # https://tweeterid.com/ + follows = [] + # strict mode means only original tweets will be ingested + follow_strict = false + locations = [] + tracks = [] + webhook = "" diff --git a/main.go b/main.go new file mode 100644 index 0000000..9255b47 --- /dev/null +++ b/main.go @@ -0,0 +1,118 @@ +package main + +import ( + "flag" + "os" + "os/signal" + "syscall" + + "go.jolheiser.com/lurk/config" + "go.jolheiser.com/lurk/handler" + + "github.com/dghubble/go-twitter/twitter" + "github.com/dghubble/oauth1" + "github.com/turnage/graw" + "github.com/turnage/graw/reddit" + "go.jolheiser.com/beaver" +) + +var ( + configPath string + debug bool +) + +func main() { + flag.StringVar(&configPath, "config", "lurk.toml", "Path to lurk's config file") + flag.BoolVar(&debug, "debug", false, "Turn on debug mode") + flag.Parse() + + beaver.Console.Format = beaver.FormatOptions{ + TimePrefix: true, + StackPrefix: true, + StackLimit: 15, + LevelPrefix: true, + LevelColor: true, + } + if debug { + beaver.Console.Level = beaver.DEBUG + } + + cfg, err := config.Load(configPath) + if err != nil { + beaver.Fatal(err) + } + + // Reddit + go lurkReddit(cfg) + + // Twitter + go lurkTwitter(cfg) + + beaver.Info("Lurk is ready to start lurking!") + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + <-ch +} + +func lurkReddit(cfg *config.Config) { + bot, err := reddit.NewBot(reddit.BotConfig{ + Agent: cfg.Reddit.UserAgent(), + App: reddit.App{ + ID: cfg.Reddit.ClientID, + Secret: cfg.Reddit.ClientSecret, + Username: cfg.Reddit.Username, + Password: cfg.Reddit.Password, + }, + }) + if err != nil { + beaver.Fatal(err) + } + + _, wait, err := graw.Run(&handler.Reddit{ + Config: cfg, + }, bot, graw.Config{ + Subreddits: cfg.Reddit.SubRedditNames(), + }) + if err != nil { + beaver.Errorf("could not run reddit bot: %v", err) + return + } + if err := wait(); err != nil { + beaver.Fatal(err) + } +} + +func lurkTwitter(cfg *config.Config) { + twitterConfig := oauth1.NewConfig(cfg.Twitter.ConsumerKey, cfg.Twitter.ConsumerSecret) + token := oauth1.NewToken(cfg.Twitter.AccessToken, cfg.Twitter.AccessSecret) + + httpClient := twitterConfig.Client(oauth1.NoContext, token) + client := twitter.NewClient(httpClient) + + // Just to test if we have valid auth + _, _, err := client.Timelines.HomeTimeline(&twitter.HomeTimelineParams{ + Count: 1, + }) + if err != nil { + beaver.Fatal(err) + } + + for _, filter := range cfg.Twitter.Filters { + stream, err := client.Streams.Filter(&twitter.StreamFilterParams{ + Follow: filter.Follows, + Locations: filter.Locations, + StallWarnings: twitter.Bool(false), + Track: filter.Tracks, + }) + if err != nil { + beaver.Fatal(err) + } + + lurker := &handler.Twitter{ + Filter: filter, + Stream: stream, + } + + go lurker.Run() + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..32358ac --- /dev/null +++ b/main_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "os" + "regexp" + "testing" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +func TestMatchesAny(t *testing.T) { + tt := []struct { + Regex string + Input string + Match bool + }{ + {Regex: `JAVA`, Input: "java", Match: false}, + {Regex: `(?i)JAVA`, Input: "java", Match: true}, + {Regex: `white`, Input: "whitelist", Match: true}, + {Regex: `\bwhite\b`, Input: "whitelist", Match: false}, + {Regex: `\bwhite\b`, Input: "white list", Match: true}, + {Regex: `\bwhite\b`, Input: "a white list", Match: true}, + {Regex: `\swhite\s`, Input: "whitelist", Match: false}, + {Regex: `\swhite\s`, Input: "white list", Match: false}, + {Regex: `\swhite\s`, Input: "a white list", Match: true}, + } + + for _, tc := range tt { + t.Run(tc.Regex, func(t *testing.T) { + r := regexp.MustCompile(tc.Regex) + if r.MatchString(tc.Input) != tc.Match { + t.Fail() + } + }) + } +}