├── .gitignore ├── config.json ├── main.go ├── csv.go ├── README.md ├── util.go ├── logic.go └── twitter.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.json 3 | *.exe 4 | go-twitter-follower 5 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "twitterName" : "YOUR_TWITTER_USER", 3 | "interests": [ 4 | "your interests", 5 | "can", 6 | "be", 7 | "listed", 8 | "here" 9 | ], 10 | "twitterAccess": { 11 | "consumerKey": "TWITTER_API_CONSUMER_KEY", 12 | "consumerSecret": "TWITTER_CONSUMER_SECRET", 13 | "accessToken": "TWITTER_ACCESS_TOKEN", 14 | "accessSecret": "ACCESS_SECRET" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // The purpose of this application is to follow new users in hope that they will 2 | // return the favor and follow you back. After a set of time the program will unfollow 3 | // the user. And hopefully, the user that followed you forgets to unfollow you. 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "flag" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | ) 13 | 14 | var clean, unfollowAll bool 15 | var followHours, sleepTime, opsBeforeSleep int 16 | var config Config 17 | 18 | // Parses flags and reads the configuration file. 19 | func init() { 20 | flag.BoolVar(&clean, "clean", false, "Cleans all previous follows from the bot") 21 | flag.BoolVar(&unfollowAll, "unfollowAll", false, "Unfollows all your friends") 22 | flag.IntVar(&followHours, "followHours", 6, "How many hours to follow users") 23 | flag.IntVar(&sleepTime, "sleepTime", 20, "Time in minutes to sleep between each circle") 24 | flag.IntVar(&opsBeforeSleep, "opsBeforeSleep", 10, "Number of operations before sleeping") 25 | flag.Parse() 26 | 27 | filePath := getPath("config.json") 28 | 29 | file, err1 := ioutil.ReadFile(filePath) 30 | if err1 != nil { 31 | checkError("Error while reading file\n", err1) 32 | os.Exit(1) 33 | } 34 | 35 | err2 := json.Unmarshal(file, &config) 36 | if err2 != nil { 37 | log.Fatal("error:", err2) 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | // Starts the bot. 43 | func main() { 44 | startBot() 45 | } 46 | -------------------------------------------------------------------------------- /csv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | "os" 7 | "strconv" 8 | ) 9 | 10 | // Reads from a csv file and returns a slice of UserEntities 11 | func readFromFile(filePath string) []UserEntity { 12 | csvFile, err := os.Open(filePath) 13 | 14 | if err != nil { 15 | csvFile, _ = os.Create(filePath) 16 | } 17 | defer csvFile.Close() 18 | 19 | reader := csv.NewReader(csvFile) 20 | var userEntities []UserEntity 21 | 22 | for { 23 | line, err := reader.Read() 24 | if err == io.EOF { 25 | break 26 | } 27 | checkError("Failed to read lines in file\n", err) 28 | i, _ := strconv.ParseInt(line[1], 10, 64) 29 | userEntities = append(userEntities, UserEntity{ 30 | ScreenName: line[0], 31 | FollowedTimestamp: i, 32 | }) 33 | } 34 | 35 | return userEntities 36 | } 37 | 38 | // Writes the list of user entities to file in order to keep track of 39 | // who to later unfollow and at what time. 40 | func writeListOfFollowsToFile(userEntities []UserEntity) { 41 | file, err := os.Create("follows.csv") 42 | checkError("Cannot create file\n", err) 43 | defer file.Close() 44 | 45 | writer := csv.NewWriter(file) 46 | defer writer.Flush() 47 | for _, value := range userEntities { 48 | timestamp := strconv.FormatInt(value.FollowedTimestamp, 10) 49 | strWrite := []string{value.ScreenName, timestamp} 50 | err := writer.Write(strWrite) 51 | writer.Flush() 52 | checkError("Cannot write to file", err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-twitter-bot 2 | Many users follow you back if you follow them, and this is what this bot aims to exploit. 3 | 4 | The twitter bot will search for users tweeting about configurable topics and give them a follow. Then after 6 hours (by default), it will unfollow them, and in most of the cases that user will forget or miss to unfollow you. 5 | 6 | It is quite a toxic behavior, but it is an efficient way to get new followers if you don't mind that. 7 | 8 | **For more details, please see https://thecuriousdev.org/golang-twitter-bot/** 9 | 10 | **DISCLAIMER!** If you use the bot it's at your own risk. I am not responsible if Twitter would decide to take some action against you. With that in mind, the bots default values prevents spam and most likely you will be fine. 11 | 12 | ## Getting started 13 | There are many ways to get started, you could clone the project (and install Golang on your computer if you don't have it already) and then simply compile the code by executing the command `go build` in the directory which will give you an executable file. 14 | 15 | I have also uploaded a couple of different executables compiled for the most popular platforms which can be found here: https://snieking-owncloud.cloud.seedboxes.cc/index.php/s/JGeOtiFntelfi5I 16 | 17 | ### Configuring the bot 18 | Modify/Create `config.json` file. An example file can be found in this repository. Fill in your details, and make sure that you pick up Twitter API keys from https://apps.twitter.com and enter those into the config file. DO NOT SHARE your API keys with anyone. 19 | 20 | ### Executing it 21 | You start the bot by simply executing the binary. For example: `./go-twitter-bot.exe` or `./go-twitter-bot` 22 | 23 | ## Extra functionality 24 | The twitter bot supports three different running modes and you can also configure some default values. This is done by providing some extra program arguments. Execute `./go-twitter-bot.exe -h` for details. 25 | 26 | `-clean` will clear all previous followed users. 27 | `-unfollowAll` will unfollow all users that your account follows **(USE WITH CAUTION)**. 28 | 29 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Convenient utility functions. 2 | package main 3 | 4 | import ( 5 | "log" 6 | "math/rand" 7 | "os" 8 | "time" 9 | ) 10 | 11 | // Makes a timestamp at the current time and returns it in milliseconds. 12 | func makeTimestamp() int64 { 13 | return time.Now().UnixNano() / int64(time.Millisecond) 14 | } 15 | 16 | // Makes a timestamp at current time, minus provided amount of hours. 17 | func makeTimestampHoursBeforeNow(hours int) int64 { 18 | return makeTimestamp() - (int64(hours) * 3600000) 19 | } 20 | 21 | // Returns a pointer to a true boolean. 22 | func newTrue() *bool { 23 | b := true 24 | return &b 25 | } 26 | 27 | // Returns a pointer to a false boolean. 28 | func newFalse() *bool { 29 | b := false 30 | return &b 31 | } 32 | 33 | // Checks for errors, and if there is an error then it logs it as a Fatal 34 | // together with a provided string. 35 | // This method will kill the application if the error exists. 36 | func checkError(message string, err error) { 37 | if err != nil { 38 | log.Fatal(message, err) 39 | } 40 | } 41 | 42 | // Simply logs the error if it occurred. 43 | func logError(err error) { 44 | if err != nil { 45 | log.Println(err) 46 | } 47 | } 48 | 49 | // Removes the UserEntity with the provided index from the slice. 50 | // It doesn't retain the order as the last element will be put at 51 | // the index of the removed element. 52 | func remove(s []UserEntity, i int) []UserEntity { 53 | s[len(s)-1], s[i] = s[i], s[len(s)-1] 54 | return s[:len(s)-1] 55 | } 56 | 57 | // Returns a random element from the slice. 58 | func randomElementFromSlice(s []string) string { 59 | return s[randomNumberInRange(0, len(s))] 60 | } 61 | 62 | // Returns a random number in a provided range. 63 | // For example a random number between 10-15. 64 | func randomNumberInRange(min, max int) int { 65 | rand.Seed(time.Now().Unix()) 66 | return rand.Intn(max-min) + min 67 | } 68 | 69 | // Returns the path to the base directory appended with the filename. 70 | func getPath(filename string) string { 71 | pwd, _ := os.Getwd() 72 | return pwd + string(os.PathSeparator) + filename 73 | } 74 | -------------------------------------------------------------------------------- /logic.go: -------------------------------------------------------------------------------- 1 | // Handles all the business logic for the twitter bot. 2 | package main 3 | 4 | import ( 5 | "log" 6 | "os" 7 | ) 8 | 9 | // Starts the bot, creating the twitter connection as well as performing 10 | // all the actions until an fatal exception occurs. 11 | func startBot() { 12 | createConnection(config.TwitterAccess) 13 | 14 | if unfollowAll { 15 | unfollowAllFromUserAndExit(config.TwitterName) 16 | } 17 | 18 | for { 19 | followEntries := readFromFile("follows.csv") 20 | 21 | // Unfollow all previous followed users 22 | if clean { 23 | cleanFollowListAndExit(followEntries) 24 | } 25 | 26 | unfollowOldUsers(followEntries) 27 | userIDsFollowed := getMapOfFollowedUsers(config.TwitterName) 28 | followEntries = followNewUsers(followEntries, userIDsFollowed) 29 | writeListOfFollowsToFile(followEntries) 30 | } 31 | } 32 | 33 | // Cleans the provided list of user entities, meaning that all of them 34 | // will be unfollowed. 35 | func cleanFollowListAndExit(userEntities []UserEntity) { 36 | log.Printf("Unfollowing all followed users in list") 37 | for index, element := range userEntities { 38 | unfollow(element.ScreenName) 39 | log.Printf("[%d] Unfollowed: %s", index, element.ScreenName) 40 | } 41 | 42 | writeListOfFollowsToFile([]UserEntity{}) 43 | os.Exit(3) 44 | } 45 | 46 | // Unfollows previous users that we followed. 47 | // Users are considered old if they are older than the configured hours. 48 | func unfollowOldUsers(userEntities []UserEntity) { 49 | log.Printf("Checking if anyone needs to be unfollowed") 50 | for index, element := range userEntities { 51 | if element.FollowedTimestamp < makeTimestampHoursBeforeNow(followHours) { 52 | if len(userEntities) > index { 53 | unfollow(element.ScreenName) 54 | userEntities = remove(userEntities, index) 55 | log.Printf("[%d] Unfollowed: %s", index, element.ScreenName) 56 | } 57 | } else { 58 | log.Printf("[%d] user %s isn't due for unfollow yet", index, element.ScreenName) 59 | } 60 | } 61 | } 62 | 63 | // Unfollows all users of a provided twitterName. 64 | // Recursively calls itself until finished. 65 | func unfollowAllFromUserAndExit(twitterName string) { 66 | users := listFollows(twitterName) 67 | if len(users) < 1 { 68 | log.Printf("User %s doesn't follow any more users. Exiting because work is done.", twitterName) 69 | os.Create("follows.csv") 70 | os.Exit(3) 71 | } else { 72 | for index, element := range users { 73 | unfollow(element) 74 | log.Printf("[%d] Unfollowed: %s", index, element) 75 | } 76 | 77 | unfollowAllFromUserAndExit(twitterName) 78 | } 79 | } 80 | 81 | // Follows all the users in the provided list of user entities. 82 | func followNewUsers(userEntities []UserEntity, userIDsFollowed map[int64]bool) []UserEntity { 83 | log.Printf("\nSearching for new users to follow") 84 | users := searchTweets(randomElementFromSlice(config.Interests), opsBeforeSleep) 85 | for index, element := range users { 86 | if !userIDsFollowed[element.UserID] { 87 | userEntities = append(userEntities, UserEntity{ 88 | ScreenName: element.ScreenName, 89 | FollowedTimestamp: makeTimestamp(), 90 | }) 91 | follow(element.ScreenName) 92 | log.Printf("[%d] followed: %s", index, element.ScreenName) 93 | } else { 94 | log.Printf("User %s is already followed, skipping that user", element.ScreenName) 95 | } 96 | } 97 | 98 | return userEntities 99 | } 100 | -------------------------------------------------------------------------------- /twitter.go: -------------------------------------------------------------------------------- 1 | // This file is supposed to handle all logic when it comes to communicating 2 | // with the Twitter API. It creates the clients, and implements all needed 3 | // operations towards the API. 4 | package main 5 | 6 | import ( 7 | "log" 8 | "time" 9 | 10 | "github.com/dghubble/go-twitter/twitter" 11 | "github.com/dghubble/oauth1" 12 | ) 13 | 14 | var client twitter.Client 15 | var limitTracker LimitTracker 16 | 17 | // Creates and returns the twitter client that will be used to perform 18 | // actions towards the Twitter API. 19 | func createConnection(twitterConf TwitterAccess) { 20 | config := oauth1.NewConfig(twitterConf.ConsumerKey, twitterConf.ConsumerSecret) 21 | token := oauth1.NewToken(twitterConf.AccessToken, twitterConf.AccessSecret) 22 | 23 | // http.Client will automatically authorize requests 24 | httpClient := config.Client(oauth1.NoContext, token) 25 | 26 | // Twitter client 27 | client = *twitter.NewClient(httpClient) 28 | } 29 | 30 | // Follows a provided user. 31 | func follow(user string) { 32 | preventReachingLimit() 33 | _, _, err := client.Friendships.Create(&twitter.FriendshipCreateParams{ 34 | ScreenName: user, 35 | Follow: newTrue(), 36 | }) 37 | logError(err) 38 | } 39 | 40 | // Unfollows a provided user. 41 | func unfollow(user string) { 42 | preventReachingLimit() 43 | _, _, err := client.Friendships.Destroy(&twitter.FriendshipDestroyParams{ 44 | ScreenName: user, 45 | }) 46 | checkError("Failed to unfollow\n", err) 47 | } 48 | 49 | // List all the followers of a provided user. 50 | func listFollows(user string) []string { 51 | users := []string{} 52 | preventReachingLimit() 53 | friends, _, error := client.Friends.List(&twitter.FriendListParams{ScreenName: user, Count: 1000}) 54 | checkError("Failed to fetch friends\n", error) 55 | for _, element := range friends.Users { 56 | users = append(users, element.ScreenName) 57 | } 58 | return users 59 | } 60 | 61 | // Search for tweets based on a provided topic and returns as many users 62 | // who wrote tweets as it can find based on the provided topic and limit 63 | func searchTweets(value string, limit int) []UserEntity { 64 | preventReachingLimit() 65 | search, _, err := client.Search.Tweets(&twitter.SearchTweetParams{ 66 | Query: value, 67 | Count: limit, 68 | }) 69 | 70 | checkError("Failed to search for tweets\n", err) 71 | 72 | var users []UserEntity 73 | for _, element := range search.Statuses { 74 | if element.Lang == "en" { 75 | users = append(users, UserEntity{ 76 | ScreenName: element.User.ScreenName, 77 | UserID: element.User.ID, 78 | }) 79 | } 80 | } 81 | 82 | return users 83 | } 84 | 85 | // Gets information of who a user follows. 86 | // Returns a map where the key is the ID for easier and quicker lookup. 87 | func getMapOfFollowedUsers(user string) map[int64]bool { 88 | preventReachingLimit() 89 | friends, _, err := client.Friends.IDs(&twitter.FriendIDParams{ScreenName: user}) 90 | checkError("Failed to fetch followed users\n", err) 91 | m := make(map[int64]bool) 92 | for _, element := range friends.IDs { 93 | m[element] = true 94 | } 95 | return m 96 | } 97 | 98 | // Twitter has a limitation where you cannot perform more than 15 operations per limit window. 99 | // A limit window is started when you perform your first operation. 100 | // This function tracks the number of operations that have been performed in the active window 101 | // and if we go over it, it will sleep until the window is over. 102 | func preventReachingLimit() { 103 | if limitTracker.WindowStarted.IsZero() { 104 | limitTracker = LimitTracker{Operations: 0, WindowStarted: time.Now()} 105 | } else if limitTracker.Operations > opsBeforeSleep { 106 | windowStartInNano := limitTracker.WindowStarted.Nanosecond() 107 | nowInNano := time.Now().Nanosecond() 108 | nanosSinceStarted := nowInNano - windowStartInNano 109 | shouldSleepForNanos := (time.Duration(15) * time.Minute) - (time.Duration(nanosSinceStarted) * time.Nanosecond) 110 | log.Printf("Sleeping for about %d minutes", shouldSleepForNanos/60000000000.0) 111 | time.Sleep(time.Duration(shouldSleepForNanos) * time.Nanosecond) 112 | } 113 | 114 | if limitTracker.WindowStarted.Add(15 * time.Minute).Before(time.Now()) { 115 | limitTracker = LimitTracker{Operations: 0, WindowStarted: time.Now()} 116 | } else { 117 | limitTracker.Operations++ 118 | } 119 | 120 | } 121 | 122 | // Config holds configuration from the user 123 | type Config struct { 124 | TwitterName string 125 | Interests []string 126 | TwitterAccess TwitterAccess 127 | } 128 | 129 | // TwitterAccess holds the keys, and secrets for the Twitter API 130 | type TwitterAccess struct { 131 | ConsumerKey string 132 | ConsumerSecret string 133 | AccessToken string 134 | AccessSecret string 135 | } 136 | 137 | // UserEntity holds data for a user that we followed 138 | type UserEntity struct { 139 | ScreenName string `json:"screenName"` 140 | UserID int64 `json:"userID"` 141 | FollowedTimestamp int64 `json:"followedTimestamp"` 142 | } 143 | 144 | // LimitTracker tracks the limitations of the Twitter API 145 | // so that we do not exhaust our resources. 146 | type LimitTracker struct { 147 | Operations int 148 | WindowStarted time.Time 149 | } 150 | --------------------------------------------------------------------------------