diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..062b559 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + + +botsConfig.json +build/botsConfig.json +repliesConfig.json +keywordsConfig.json +temp +text.txt diff --git a/README.md b/README.md index 4f4a55c..6517471 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# echo-botnet +# echo-botnet [![Go Report Card](https://goreportcard.com/badge/github.com/arnaucode/echo-botnet)](https://goreportcard.com/report/github.com/arnaucode/echo-botnet) A twitter botnet with autonomous bots replying tweets with pre-configured replies + + +Echo (Ēkhō): https://en.wikipedia.org/wiki/Echo_(mythology)# + + +Needs the files: +``` +botsConfig.json --> contains the tokens of the bots +keywordsConfig.json --> contains the keywords to track from Twitter API +repliesConfig.json --> contains the replies +``` + +configuration file example (botsConfig.json): +``` +[{ + "title": "account1", + "consumer_key": "xxxxxxxxxxxxx", + "consumer_secret": "xxxxxxxxxxxxx", + "access_token_key": "xxxxxxxxxxxxx", + "access_token_secret": "xxxxxxxxxxxxx" + }, + { + "title": "account2", + "consumer_key": "xxxxxxxxxxxxx", + "consumer_secret": "xxxxxxxxxxxxx", + "access_token_key": "xxxxxxxxxxxxx", + "access_token_secret": "xxxxxxxxxxxxx" + }, + { + "title": "account3", + "consumer_key": "xxxxxxxxxxxxx", + "consumer_secret": "xxxxxxxxxxxxx", + "access_token_key": "xxxxxxxxxxxxx", + "access_token_secret": "xxxxxxxxxxxxx" + } +] + +``` diff --git a/build/botsConfigDEMO.json b/build/botsConfigDEMO.json new file mode 100644 index 0000000..551eeba --- /dev/null +++ b/build/botsConfigDEMO.json @@ -0,0 +1,22 @@ +[{ + "title": "account1", + "consumer_key": "xxxxxxxxxxxxx", + "consumer_secret": "xxxxxxxxxxxxx", + "access_token_key": "xxxxxxxxxxxxx", + "access_token_secret": "xxxxxxxxxxxxx" + }, + { + "title": "account2", + "consumer_key": "xxxxxxxxxxxxx", + "consumer_secret": "xxxxxxxxxxxxx", + "access_token_key": "xxxxxxxxxxxxx", + "access_token_secret": "xxxxxxxxxxxxx" + }, + { + "title": "account3", + "consumer_key": "xxxxxxxxxxxxx", + "consumer_secret": "xxxxxxxxxxxxx", + "access_token_key": "xxxxxxxxxxxxx", + "access_token_secret": "xxxxxxxxxxxxx" + } +] diff --git a/build/echo-botnet b/build/echo-botnet new file mode 100755 index 0000000..a6bf441 Binary files /dev/null and b/build/echo-botnet differ diff --git a/build/keywordsConfigDEMO.json b/build/keywordsConfigDEMO.json new file mode 100644 index 0000000..d913766 --- /dev/null +++ b/build/keywordsConfigDEMO.json @@ -0,0 +1,6 @@ +[ + "word1", + "word2", + "wordToTrack3", + "word4" +] diff --git a/build/repliesConfigDEMO.json b/build/repliesConfigDEMO.json new file mode 100644 index 0000000..e93b678 --- /dev/null +++ b/build/repliesConfigDEMO.json @@ -0,0 +1,7 @@ +[ + "Reply message 1", + "Reply message 2", + "Reply message 3", + "Hey wassup friend!", + "You like the botnet?" +] diff --git a/color.go b/color.go new file mode 100644 index 0000000..e945ac5 --- /dev/null +++ b/color.go @@ -0,0 +1,57 @@ +package main + +import "fmt" + +//Color struct, defines the color +type Color struct{} + +var c Color + +//DarkGray color +func (c Color) DarkGray(t string) { + fmt.Print("\x1b[30;1m") //dark gray + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} + +//Red color +func (c Color) Red(t string) { + fmt.Print("\x1b[31;1m") //red + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} + +//Green color +func (c Color) Green(t string) { + fmt.Print("\x1b[32;1m") //green + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} + +//Yellow color +func (c Color) Yellow(t string) { + fmt.Print("\x1b[33;1m") //yellow + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} + +//Blue color +func (c Color) Blue(t string) { + fmt.Print("\x1b[34;1m") //blue + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} + +//Purple color +func (c Color) Purple(t string) { + fmt.Print("\x1b[35;1m") //purple + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} + +//Cyan color +func (c Color) Cyan(t string) { + fmt.Print("\x1b[36;1m") //cyan + fmt.Println(t) + fmt.Print("\x1b[0m") //defaultColor +} diff --git a/echo-botnet b/echo-botnet new file mode 100755 index 0000000..a6bf441 Binary files /dev/null and b/echo-botnet differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..5c9b4ab --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import "fmt" + +const version = "0.1-dev" + +func main() { + c.Yellow("echo-botnet") + fmt.Println("---------------") + c.Cyan("echo-botnet initialized") + c.Purple("https://github.com/arnaucode/echo-botnet") + fmt.Println("version " + version) + + keywords := readKeywordsConfig() + c.Cyan("keywords configured: ") + fmt.Println(keywords) + replies := readRepliesConfig() + c.Cyan("replies configured: ") + fmt.Println(replies) + + fmt.Println("Reading botsConfig.json file") + botnet := readConfigTokensAndConnect() + c.Cyan("[list of configured bots]:") + for _, v := range botnet.ScreenNames { + c.Cyan(v) + } + streamTweets(botnet, keywords, replies) +} diff --git a/readConfigTokensAndConnect.go b/readConfigTokensAndConnect.go new file mode 100644 index 0000000..60da68c --- /dev/null +++ b/readConfigTokensAndConnect.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/dghubble/go-twitter/twitter" + "github.com/dghubble/oauth1" +) + +//Config stores the data from json botsConfig.json file +type Config struct { + Title string `json:"title"` + Consumer_key string `json:"consumer_key"` + Consumer_secret string `json:"consumer_secret"` + Access_token_key string `json:"access_token_key"` + Access_token_secret string `json:"access_token_secret"` +} + +//Botnet stores each bot configured +type Botnet struct { + ScreenNames []string + Clients []*twitter.Client +} + +func readConfigTokensAndConnect() (botnet Botnet) { + var config []Config + var clients []*twitter.Client + + file, e := ioutil.ReadFile("botsConfig.json") + if e != nil { + fmt.Println("error:", e) + } + content := string(file) + json.Unmarshal([]byte(content), &config) + fmt.Println("botnetConfig.json read comlete") + + fmt.Print("connecting to twitter api --> ") + for i := 0; i < len(config); i++ { + configu := oauth1.NewConfig(config[i].Consumer_key, config[i].Consumer_secret) + token := oauth1.NewToken(config[i].Access_token_key, config[i].Access_token_secret) + httpClient := configu.Client(oauth1.NoContext, token) + // twitter client + client := twitter.NewClient(httpClient) + clients = append(clients, client) + botnet.ScreenNames = append(botnet.ScreenNames, config[i].Title) + } + botnet.Clients = clients + + fmt.Println("connection successfull") + + return botnet +} diff --git a/readKeywordsConfig.go b/readKeywordsConfig.go new file mode 100644 index 0000000..0912943 --- /dev/null +++ b/readKeywordsConfig.go @@ -0,0 +1,19 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" +) + +func readKeywordsConfig() []string { + var keywords []string + file, e := ioutil.ReadFile("keywordsConfig.json") + if e != nil { + fmt.Println("error:", e) + } + content := string(file) + json.Unmarshal([]byte(content), &keywords) + + return keywords +} diff --git a/readRepliesConfig.go b/readRepliesConfig.go new file mode 100644 index 0000000..c7f2c94 --- /dev/null +++ b/readRepliesConfig.go @@ -0,0 +1,19 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" +) + +func readRepliesConfig() []string { + var replies []string + file, e := ioutil.ReadFile("repliesConfig.json") + if e != nil { + fmt.Println("error:", e) + } + content := string(file) + json.Unmarshal([]byte(content), &replies) + + return replies +} diff --git a/streamTweets.go b/streamTweets.go new file mode 100644 index 0000000..f12e3bf --- /dev/null +++ b/streamTweets.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/dghubble/go-twitter/twitter" +) + +func isRT(tweet *twitter.Tweet) bool { + tweetWords := strings.Split(tweet.Text, " ") + for i := 0; i < len(tweetWords); i++ { + if tweetWords[i] == "RT" { + return true + } + } + return false +} +func isFromOwnBot(flock Botnet, tweet *twitter.Tweet) bool { + for i := 0; i < len(flock.ScreenNames); i++ { + if flock.ScreenNames[i] == tweet.User.ScreenName { + return true + } + } + return false +} + +func getRandomReplyFromReplies(replies []string) string { + rand.Seed(time.Now().UnixNano()) + random := rand.Intn(len(replies)) + return replies[random] +} + +func replyTweet(client *twitter.Client, text string, inReplyToStatusID int64) { + tweet, httpResp, err := client.Statuses.Update(text, &twitter.StatusUpdateParams{ + InReplyToStatusID: inReplyToStatusID, + }) + if err != nil { + fmt.Println(err) + } + if httpResp.Status != "200 OK" { + c.Red("error: " + httpResp.Status) + c.Purple("maybe twitter has blocked the account, CTRL+C, wait 15 minutes and try again") + } + fmt.Print("tweet posted: ") + c.Green(tweet.Text) +} + +func startStreaming(botnet Botnet, bot *twitter.Client, botScreenName string, keywords []string, replies []string) { + // Convenience Demux demultiplexed stream messages + demux := twitter.NewSwitchDemux() + demux.Tweet = func(tweet *twitter.Tweet) { + if isRT(tweet) == false && isFromOwnBot(botnet, tweet) == false { + //processTweet(botnetUser, botScreenName, keywords, tweet) + fmt.Println("[bot @" + botScreenName + "] - New tweet detected:") + c.Yellow(tweet.Text) + reply := getRandomReplyFromReplies(replies) + fmt.Print("reply: ") + c.Green(reply) + fmt.Println(tweet.User.ScreenName) + fmt.Println(tweet.ID) + replyTweet(bot, "@"+tweet.User.ScreenName+" "+reply, tweet.ID) + waitMinutes(1) + } + } + demux.DM = func(dm *twitter.DirectMessage) { + fmt.Println(dm.SenderID) + } + demux.Event = func(event *twitter.Event) { + fmt.Printf("%#v\n", event) + } + + fmt.Println("Starting Stream...") + // FILTER + filterParams := &twitter.StreamFilterParams{ + Track: keywords, + StallWarnings: twitter.Bool(true), + } + stream, err := bot.Streams.Filter(filterParams) + if err != nil { + log.Fatal(err) + } + // Receive messages until stopped or stream quits + demux.HandleChan(stream.Messages) + + // Wait for SIGINT and SIGTERM (HIT CTRL-C) + /*ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + log.Println(<-ch) + + fmt.Println("Stopping Stream...") + stream.Stop()*/ +} +func streamTweets(botnet Botnet, keywords []string, replies []string) { + fmt.Println("total keywords: " + strconv.Itoa(len(keywords))) + c.Purple("keywords to follow: ") + fmt.Println(keywords) + c.Green("Starting to stream tweets") + for i := 0; i < len(botnet.Clients); i++ { + go startStreaming(botnet, botnet.Clients[i], botnet.ScreenNames[i], keywords, replies) + //wait 35 seconds to start the next bot + waitSeconds(35) + } +} diff --git a/waitTime.go b/waitTime.go new file mode 100644 index 0000000..27e44b9 --- /dev/null +++ b/waitTime.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "strconv" + "time" +) + +func waitMinutes(minutes int) { + //wait to avoid the twitter api limitation + timeToSleep := time.Duration(minutes) * time.Minute + fmt.Println("waiting " + strconv.Itoa(minutes) + " min to avoid twitter api limitation") + fmt.Println(time.Now().Local()) + time.Sleep(timeToSleep) +} +func waitSeconds(seconds int) { + //wait to avoid the twitter api limitation + timeToSleep := time.Duration(seconds) * time.Second + fmt.Println("waiting " + strconv.Itoa(seconds) + " seconds to avoid twitter api limitation") + fmt.Println(time.Now().Local()) + time.Sleep(timeToSleep) +}