Skip to content

Commit 2b5eccd

Browse files
committed
Fixes #14
Previously, the bot only looked up 5,000 of the user’s follows and followers. This meant that the bot would sometimes mistakenly think a user hadn’t followed back when an account had more than 5,000 followers. This patch fixes this bug by using Twitter API cursors - it looks up the entire list of follows and followers for the account it’s working with and stores them locally. Note that it is necessary to store the follows and followers locally because, especially for accounts with a large number of followers, the API limit can be reached very quickly.
1 parent d0c8d95 commit 2b5eccd

File tree

1 file changed

+148
-72
lines changed

1 file changed

+148
-72
lines changed

twitter_follow_bot.py

Lines changed: 148 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
# -*- coding: utf-8 -*-
22

33
"""
4-
Copyright 2014 Randal S. Olson
4+
Copyright 2015 Randal S. Olson
55
66
This file is part of the Twitter Follow Bot library.
77
88
The Twitter Follow Bot library is free software: you can redistribute it and/or
99
modify it under the terms of the GNU General Public License as published by the
10-
Free Software Foundation, either version 3 of the License, or (at your option) any
11-
later version.
10+
Free Software Foundation, either version 3 of the License, or (at your option)
11+
any later version.
1212
13-
The Twitter Follow Bot library is distributed in the hope that it will be useful,
14-
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15-
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
The Twitter Follow Bot library is distributed in the hope that it will be
14+
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
16+
License for more details.
1617
17-
You should have received a copy of the GNU General Public License along with the Twitter
18-
Follow Bot library. If not, see http://www.gnu.org/licenses/.
18+
You should have received a copy of the GNU General Public License along with
19+
the Twitter Follow Bot library. If not, see http://www.gnu.org/licenses/.
1920
"""
2021

2122
from twitter import Twitter, OAuth, TwitterHTTPError
@@ -28,25 +29,124 @@
2829
CONSUMER_SECRET = ""
2930
TWITTER_HANDLE = ""
3031

31-
# put the full path and file name of the file you want to store your "already followed"
32-
# list in
33-
ALREADY_FOLLOWED_FILE = "already-followed.csv"
32+
# put the full path and file name of the file you want to keep track of all
33+
# the accounts you've ever followed
34+
ALREADY_FOLLOWED_FILE = "already-followed.txt"
3435

36+
# put the full paths and file names of the files you want to keep track of
37+
# your follows in
38+
FOLLOWS_FILE = "following.txt"
39+
FOLLOWERS_FILE = "followers.txt"
40+
41+
# make sure all of the sync files exist
42+
for sync_file in [ALREADY_FOLLOWED_FILE, FOLLOWS_FILE, FOLLOWERS_FILE]:
43+
if not os.path.isfile(sync_file):
44+
with open(sync_file, "wb") as out_file:
45+
out_file.write("")
46+
47+
# create an authorized connection to the Twitter API
3548
t = Twitter(auth=OAuth(OAUTH_TOKEN, OAUTH_SECRET,
36-
CONSUMER_KEY, CONSUMER_SECRET))
49+
CONSUMER_KEY, CONSUMER_SECRET))
50+
51+
52+
def sync_follows():
53+
"""
54+
Syncs the user's followers and follows locally so it isn't necessary
55+
to repeatedly look them up via the Twitter API.
56+
57+
It is important to run this method at least daily so the bot is working
58+
with a relatively up-to-date version of the user's follows.
59+
"""
60+
61+
# sync the user's followers (accounts following the user)
62+
followers_status = t.followers.ids(screen_name=TWITTER_HANDLE)
63+
followers = set(followers_status["ids"])
64+
next_cursor = followers_status["next_cursor"]
65+
66+
with open(FOLLOWERS_FILE, "wb") as out_file:
67+
for follower in followers:
68+
out_file.write("%s\n" % (follower))
69+
70+
while next_cursor != 0:
71+
followers_status = t.followers.ids(
72+
screen_name=TWITTER_HANDLE, cursor=next_cursor)
73+
followers = set(followers_status["ids"])
74+
next_cursor = followers_status["next_cursor"]
75+
76+
with open(FOLLOWERS_FILE, "ab") as out_file:
77+
for follower in followers:
78+
out_file.write("%s\n" % (follower))
79+
80+
# sync the user's follows (accounts the user is following)
81+
following_status = t.friends.ids(screen_name=TWITTER_HANDLE)
82+
following = set(following_status["ids"])
83+
next_cursor = following_status["next_cursor"]
84+
85+
with open(ALREADY_FOLLOWED_FILE, "wb") as out_file:
86+
for follow in following:
87+
out_file.write("%s\n" % (follow))
88+
89+
while next_cursor != 0:
90+
following_status = t.friends.ids(
91+
screen_name=TWITTER_HANDLE, cursor=next_cursor)
92+
following = set(following_status["ids"])
93+
next_cursor = following_status["next_cursor"]
94+
95+
with open(ALREADY_FOLLOWED_FILE, "ab") as out_file:
96+
for follow in following:
97+
out_file.write("%s\n" % (follow))
98+
99+
100+
def get_do_not_follow_list():
101+
"""
102+
Returns the set of users the bot has already followed in the past.
103+
"""
104+
105+
dnf_list = []
106+
with open(ALREADY_FOLLOWED_FILE, "rb") as in_file:
107+
for line in in_file:
108+
dnf_list.append(int(line))
109+
110+
return set(dnf_list)
111+
112+
113+
def get_followers_list():
114+
"""
115+
Returns the set of users that are currently following the user.
116+
"""
117+
118+
followers_list = []
119+
with open(FOLLOWERS_FILE, "rb") as in_file:
120+
for line in in_file:
121+
followers_list.append(int(line))
122+
123+
return set(followers_list)
124+
125+
126+
def get_follows_list():
127+
"""
128+
Returns the set of users that the user is currently following.
129+
"""
130+
131+
follows_list = []
132+
with open(FOLLOWS_FILE, "rb") as in_file:
133+
for line in in_file:
134+
follows_list.append(int(line))
135+
136+
return set(follows_list)
37137

38138

39139
def search_tweets(q, count=100, result_type="recent"):
40140
"""
41-
Returns a list of tweets matching a certain phrase (hashtag, word, etc.)
141+
Returns a list of tweets matching a phrase (hashtag, word, etc.).
42142
"""
43143

44144
return t.search.tweets(q=q, result_type=result_type, count=count)
45145

46146

47147
def auto_fav(q, count=100, result_type="recent"):
48148
"""
49-
Favorites tweets that match a certain phrase (hashtag, word, etc.)
149+
Favorites tweets that match a phrase (hashtag, word, etc.).
50150
"""
51151

52152
result = search_tweets(q, count, result_type)
@@ -62,12 +162,13 @@ def auto_fav(q, count=100, result_type="recent"):
62162

63163
# when you have already favorited a tweet, this error is thrown
64164
except TwitterHTTPError as e:
65-
print("error: %s" % (str(e)))
165+
if "you have already favorited this status" not in str(e).lower():
166+
print("error: %s" % (str(e)))
66167

67168

68169
def auto_rt(q, count=100, result_type="recent"):
69170
"""
70-
Retweets tweets that match a certain phrase (hashtag, word, etc.)
171+
Retweets tweets that match a phrase (hashtag, word, etc.).
71172
"""
72173

73174
result = search_tweets(q, count, result_type)
@@ -86,37 +187,13 @@ def auto_rt(q, count=100, result_type="recent"):
86187
print("error: %s" % (str(e)))
87188

88189

89-
def get_do_not_follow_list():
90-
"""
91-
Returns list of users the bot has already followed.
92-
"""
93-
94-
# make sure the "already followed" file exists
95-
if not os.path.isfile(ALREADY_FOLLOWED_FILE):
96-
with open(ALREADY_FOLLOWED_FILE, "w") as out_file:
97-
out_file.write("")
98-
99-
# read in the list of user IDs that the bot has already followed in the
100-
# past
101-
do_not_follow = set()
102-
dnf_list = []
103-
with open(ALREADY_FOLLOWED_FILE) as in_file:
104-
for line in in_file:
105-
dnf_list.append(int(line))
106-
107-
do_not_follow.update(set(dnf_list))
108-
del dnf_list
109-
110-
return do_not_follow
111-
112-
113190
def auto_follow(q, count=100, result_type="recent"):
114191
"""
115-
Follows anyone who tweets about a specific phrase (hashtag, word, etc.)
192+
Follows anyone who tweets about a phrase (hashtag, word, etc.).
116193
"""
117194

118195
result = search_tweets(q, count, result_type)
119-
following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])
196+
following = get_follows_list()
120197
do_not_follow = get_do_not_follow_list()
121198

122199
for tweet in result["statuses"]:
@@ -138,17 +215,18 @@ def auto_follow(q, count=100, result_type="recent"):
138215
quit()
139216

140217

141-
def auto_follow_followers_for_user(user_screen_name, count=100):
218+
def auto_follow_followers_of_user(user_screen_name, count=100):
142219
"""
143-
Follows the followers of a user
220+
Follows the followers of a specified user.
144221
"""
145-
following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])
146-
followers_for_user = set(t.followers.ids(screen_name=user_screen_name)["ids"][:count]);
222+
following = get_follows_list()
223+
followers_of_user = set(
224+
t.followers.ids(screen_name=user_screen_name)["ids"][:count])
147225
do_not_follow = get_do_not_follow_list()
148-
149-
for user_id in followers_for_user:
226+
227+
for user_id in followers_of_user:
150228
try:
151-
if (user_id not in following and
229+
if (user_id not in following and
152230
user_id not in do_not_follow):
153231

154232
t.friendships.create(user_id=user_id, follow=False)
@@ -157,13 +235,14 @@ def auto_follow_followers_for_user(user_screen_name, count=100):
157235
except TwitterHTTPError as e:
158236
print("error: %s" % (str(e)))
159237

238+
160239
def auto_follow_followers():
161240
"""
162-
Follows back everyone who's followed you
241+
Follows back everyone who's followed you.
163242
"""
164243

165-
following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])
166-
followers = set(t.followers.ids(screen_name=TWITTER_HANDLE)["ids"])
244+
following = get_follows_list()
245+
followers = get_followers_list()
167246

168247
not_following_back = followers - following
169248

@@ -176,34 +255,29 @@ def auto_follow_followers():
176255

177256
def auto_unfollow_nonfollowers():
178257
"""
179-
Unfollows everyone who hasn't followed you back
258+
Unfollows everyone who hasn't followed you back.
180259
"""
181260

182-
following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])
183-
followers = set(t.followers.ids(screen_name=TWITTER_HANDLE)["ids"])
261+
following = get_follows_list()
262+
followers = get_followers_list()
184263

185264
# put user IDs here that you want to keep following even if they don't
186265
# follow you back
266+
# you can look up Twitter account IDs here: http://gettwitterid.com
187267
users_keep_following = set([])
188268

189269
not_following_back = following - followers
190270

191-
# make sure the "already followed" file exists
192-
if not os.path.isfile(ALREADY_FOLLOWED_FILE):
193-
with open(ALREADY_FOLLOWED_FILE, "w") as out_file:
194-
out_file.write("")
195-
196271
# update the "already followed" file with users who didn't follow back
197272
already_followed = set(not_following_back)
198-
af_list = []
273+
already_followed_list = []
199274
with open(ALREADY_FOLLOWED_FILE) as in_file:
200275
for line in in_file:
201-
af_list.append(int(line))
276+
already_followed_list.append(int(line))
202277

203-
already_followed.update(set(af_list))
204-
del af_list
278+
already_followed.update(set(already_followed_list))
205279

206-
with open(ALREADY_FOLLOWED_FILE, "w") as out_file:
280+
with open(ALREADY_FOLLOWED_FILE, "wb") as out_file:
207281
for val in already_followed:
208282
out_file.write(str(val) + "\n")
209283

@@ -215,17 +289,18 @@ def auto_unfollow_nonfollowers():
215289

216290
def auto_mute_following():
217291
"""
218-
Mutes everyone that you are following
292+
Mutes everyone that you are following.
219293
"""
220-
following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])
294+
following = get_follows_list()
221295
muted = set(t.mutes.users.ids(screen_name=TWITTER_HANDLE)["ids"])
222296

223297
not_muted = following - muted
224298

225299
# put user IDs of people you do not want to mute here
300+
# you can look up Twitter account IDs here: http://gettwitterid.com
226301
users_keep_unmuted = set([])
227-
228-
# mute all
302+
303+
# mute all
229304
for user_id in not_muted:
230305
if user_id not in users_keep_unmuted:
231306
t.mutes.users.create(user_id=user_id)
@@ -234,14 +309,15 @@ def auto_mute_following():
234309

235310
def auto_unmute():
236311
"""
237-
Unmutes everyone that you have muted
312+
Unmutes everyone that you have muted.
238313
"""
239314
muted = set(t.mutes.users.ids(screen_name=TWITTER_HANDLE)["ids"])
240315

241316
# put user IDs of people you want to remain muted here
317+
# you can look up Twitter account IDs here: http://gettwitterid.com
242318
users_keep_muted = set([])
243-
244-
# mute all
319+
320+
# unmute all
245321
for user_id in muted:
246322
if user_id not in users_keep_muted:
247323
t.mutes.users.destroy(user_id=user_id)

0 commit comments

Comments
 (0)