Initial commit

Signed-off-by: jolheiser <john.olheiser@gmail.com>
pull/4/head
jolheiser 2021-03-23 20:28:06 -05:00
commit 478409cf70
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
15 changed files with 676 additions and 0 deletions

42
.drone.yml 100644
View File

@ -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"

7
.gitignore vendored 100644
View File

@ -0,0 +1,7 @@
# GoLand
.idea/
# Binaries
/lurk*
!lurk.sample.toml

19
LICENSE 100644
View File

@ -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.

15
Makefile 100644
View File

@ -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 ./...

6
README.md 100644
View File

@ -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).

71
config/config.go 100644
View File

@ -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
}

53
config/reddit.go 100644
View File

@ -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
}

18
config/twitter.go 100644
View File

@ -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"`
}

12
go.mod 100644
View File

@ -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
)

50
go.sum 100644
View File

@ -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=

122
handler/reddit.go 100644
View File

@ -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
}

57
handler/twitter.go 100644
View File

@ -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)
}
}

48
lurk.sample.toml 100644
View File

@ -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 = ""

118
main.go 100644
View File

@ -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()
}
}

38
main_test.go 100644
View File

@ -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()
}
})
}
}