From a2e793a6c3c0f2a177aca3573b298dad747ca4ab Mon Sep 17 00:00:00 2001 From: Andreas Mieke Date: Tue, 24 Mar 2020 15:50:49 +0100 Subject: [PATCH] Adding basic Bot functionality --- cmd/db640bot/main.go | 21 ++++----- internal/bot/bot.go | 83 ++++++++++++++++++++++++++++++++++ internal/config/config.go | 14 +++++- internal/config/config_test.go | 11 ++++- internal/twitter/twitter.go | 26 +++++++++++ 5 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 internal/bot/bot.go diff --git a/cmd/db640bot/main.go b/cmd/db640bot/main.go index 2c6526a..f3fd070 100644 --- a/cmd/db640bot/main.go +++ b/cmd/db640bot/main.go @@ -3,9 +3,11 @@ package main import ( "flag" "log" + "os" + "os/signal" + "git.1750studios.com/ToddShepard/DB640/internal/bot" "git.1750studios.com/ToddShepard/DB640/internal/config" - "git.1750studios.com/ToddShepard/DB640/internal/twitter" ) func main() { @@ -15,18 +17,13 @@ func main() { log.Fatalf("Config file must not be empty!") } - log.Println("Hello World!") - config.LoadConfig(*cfg) - twitter.Init() - stream, err := twitter.GetStreamForTag("#NationalPuppyDay") - if err != nil { - log.Fatalf("Could not establish twitter stream: %+v", err) - } + bot.Init() - twitter.StreamDemux(stream, showTweet) -} + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c -func showTweet(tweet *twitter.Tweet) { - log.Printf("%s\n", tweet.Text) + bot.DeInit() + config.WriteConfig(*cfg) } diff --git a/internal/bot/bot.go b/internal/bot/bot.go new file mode 100644 index 0000000..ae84fd7 --- /dev/null +++ b/internal/bot/bot.go @@ -0,0 +1,83 @@ +package bot + +import ( + "log" + "strings" + + "git.1750studios.com/ToddShepard/DB640/internal/config" + "git.1750studios.com/ToddShepard/DB640/internal/database" + "git.1750studios.com/ToddShepard/DB640/internal/twitter" + "github.com/jinzhu/gorm" +) + +var stream *twitter.Stream + +// Init initzializes the bot and subscribes the magic hashtag feed +func Init() { + var err error + + err = database.Open(config.C.Database.Dialect, config.C.Database.Connection) + if err != nil { + log.Fatalf("could not establish database connection: %+v", err) + } + + twitter.Init() + stream, err = twitter.GetStreamForTag(config.C.Twitter.MagicHashtag) + if err != nil { + log.Fatalf("Could not establish twitter stream: %+v", err) + } + + twitter.StreamDemux(stream, handleHashtagTweet) +} + +// DeInit stops the stream and deinitzializes the bot +func DeInit() { + stream.Stop() + database.Close() +} + +func handleHashtagTweet(tweet *twitter.Tweet) { + msg, tags := twitter.GetTextAndHashtags(tweet) + // Ignore retweets + if tweet.RetweetedStatus != nil { + log.Printf("%s: %s - IGNORED (RT)\n", tweet.User.ScreenName, msg) + return + } + // Ignore replys + if tweet.InReplyToStatusID != 0 { + log.Printf("%s: %s - IGNORED (RPLY)\n", tweet.User.ScreenName, msg) + return + } + // Ignore if only magic hashtag is given + if len(tags) < 2 { + log.Printf("%s: %s - IGNORED (<2#)\n", tweet.User.ScreenName, msg) + return + } + log.Printf("%s: %s\n", tweet.User.ScreenName, msg) + go findCodes(tweet) +} + +func findCodes(tweet *twitter.Tweet) { + _, tags := twitter.GetTextAndHashtags(tweet) + var betriebsstellen []database.Betriebsstelle + for _, tag := range tags { + code := strings.ReplaceAll(tag.Text, "_", " ") + var bs database.Betriebsstelle + if database.Db.First(&bs, "code = ?", code).Error != gorm.ErrRecordNotFound { + betriebsstellen = append(betriebsstellen, bs) + } + } + sendReply(tweet, betriebsstellen) +} + +func sendReply(tweet *twitter.Tweet, betriebsstellen []database.Betriebsstelle) { + var reply string + for _, bs := range betriebsstellen { + reply = reply + bs.Code + ": " + bs.Name + "\n" + } + reply = reply[0 : len(reply)-1] + _, _, err := twitter.SendTweet(reply, tweet) + if err != nil { + log.Printf("Cannot send reply, error: %+v", err) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 1bb9c86..3cdcb71 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,8 @@ import ( // Config main type type Config struct { - Twitter Twitter + Twitter Twitter + Database Database } // Twitter related config @@ -21,6 +22,13 @@ type Twitter struct { ConsumerSecret string AccessKey string AccessSecret string + MagicHashtag string +} + +// Database related config +type Database struct { + Dialect string + Connection string } // C holds the loaded configuration @@ -32,6 +40,10 @@ func LoadDefaults() { C.Twitter.AccessSecret = "ACCESSSECRET" C.Twitter.ConsumerKey = "CONSUMERKEY" C.Twitter.ConsumerSecret = "CONSUMERSECRET" + C.Twitter.MagicHashtag = "#DB640" + + C.Database.Dialect = "sqlite3" + C.Database.Connection = ":memory:" } // LoadConfig loads the configuration from given path diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 81f36bc..a2c052b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,6 +18,9 @@ func TestWriteConfig(t *testing.T) { C.Twitter.AccessSecret = "SuperNiceSecret" C.Twitter.ConsumerKey = "ConsumeMe" C.Twitter.ConsumerSecret = "EatASecret" + C.Twitter.MagicHashtag = "#notsomagic" + C.Database.Dialect = "somedia" + C.Database.Connection = "conn" err := WriteConfig(filepath.Join("..", "..", "test", "data", "doesnotexist.toml")) if err != nil { t.Errorf("Could not write config, got error: %+v", err) @@ -30,7 +33,13 @@ func TestLoadConfig(t *testing.T) { if err != nil { t.Errorf("Cannot read config, error: %+v", err) } - if C.Twitter.AccessKey != "§OneNiceKey" || C.Twitter.AccessSecret != "SuperNiceSecret" || C.Twitter.ConsumerKey != "ConsumeMe" || C.Twitter.ConsumerSecret != "EatASecret" { + if C.Twitter.AccessKey != "§OneNiceKey" || + C.Twitter.AccessSecret != "SuperNiceSecret" || + C.Twitter.ConsumerKey != "ConsumeMe" || + C.Twitter.ConsumerSecret != "EatASecret" || + C.Twitter.MagicHashtag != "#notsomagic" || + C.Database.Dialect != "somedia" || + C.Database.Connection != "conn" { t.Errorf("Could not read config, entries wrong") } t.Cleanup(CleanupConfigTest) diff --git a/internal/twitter/twitter.go b/internal/twitter/twitter.go index b51bc5e..029ce2a 100644 --- a/internal/twitter/twitter.go +++ b/internal/twitter/twitter.go @@ -1,6 +1,8 @@ package twitter import ( + "net/http" + "git.1750studios.com/ToddShepard/DB640/internal/config" "github.com/dghubble/go-twitter/twitter" "github.com/dghubble/oauth1" @@ -12,6 +14,9 @@ var Client *twitter.Client // Tweet as in twitter.Tweet type Tweet = twitter.Tweet +// Stream as in twitter.Stream +type Stream = twitter.Stream + // Init initzializes the twitter client func Init() { conf := oauth1.NewConfig(config.C.Twitter.ConsumerKey, config.C.Twitter.ConsumerSecret) @@ -36,3 +41,24 @@ func StreamDemux(stream *twitter.Stream, cb func(*Tweet)) { demux.Tweet = cb demux.HandleChan(stream.Messages) } + +// SendTweet allows to send a string as tweet (optionally as reply to a specified tweet) +func SendTweet(msg string, replytweet *twitter.Tweet) (*twitter.Tweet, *http.Response, error) { + if replytweet == nil { + // not a reply + return Client.Statuses.Update(msg, nil) + } + // replying + p := twitter.StatusUpdateParams{ + InReplyToStatusID: replytweet.ID, + } + return Client.Statuses.Update("@"+replytweet.User.ScreenName+" "+msg, &p) +} + +// GetTextAndHashtags returns #tweet text and hashtags for short and extended tweets +func GetTextAndHashtags(tweet *twitter.Tweet) (string, []twitter.HashtagEntity) { + if tweet.ExtendedTweet == nil { + return tweet.Text, tweet.Entities.Hashtags + } + return tweet.ExtendedTweet.FullText, tweet.ExtendedTweet.Entities.Hashtags +}