Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.pyc
.*.swp
/schedule.csv
/credentials.sh
/log/*
/venv
9 changes: 9 additions & 0 deletions credentials.sh.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export TWITTER_HANDLE="MyScreenName"

# Go to https://apps.twitter.com/ and create a new app with read/write
# access, then generate a user access token.

export API_KEY='this comes from your app'
export API_SECRET='this comes from your app'
export ACCESS_TOKEN='this is for your user'
export ACCESS_TOKEN_SECRET='this is for your user'
40 changes: 40 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python

import csv
import datetime
import codecs
import sys

from os.path import dirname, join as pjoin

from twitter_follow_bot import auto_follow


def job_valid_now(job):
start = parse_date(job['start_date'])
end = parse_date(job['end_date'])
return start <= datetime.date.today() <= end


def parse_date(date_string):
return datetime.datetime.strptime(
date_string, '%Y-%m-%d').date()


def run_job(job):
print("Running {}".format(job))

auto_follow_query = job.get('auto_follow')
if auto_follow_query is not None:
auto_follow(auto_follow_query)


def main():
with open(pjoin(dirname(__file__), 'schedule.csv'), 'r') as f:
for row in csv.DictReader(f):
if job_valid_now(row):
run_job(row)

if __name__ == '__main__':
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
main()
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
twitter==1.15.0
pytz==2014.10
66 changes: 66 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash

THIS_SCRIPT=$(readlink -f $0)
THIS_DIR=$(dirname ${THIS_SCRIPT})

DATE_NOW=$(date +%Y-%m-%d_%H-%M-%S)
LOG_DIR=${THIS_DIR}/log

function setup_log_output {
mkdir -p ${LOG_DIR}
LOG_FILE=${LOG_DIR}/${DATE_NOW}.log
ln -sf ${LOG_FILE} ${LOG_DIR}/latest
}

function setup_virtualenv {
VENV_DIR=${THIS_DIR}/venv

if [ ! -d "${VENV_DIR}" ]; then
if [ -s "${THIS_DIR}/.python_version" ]; then
virtualenv ${VENV_DIR} -p "$(cat ${THIS_DIR}/.python_version)" >> ${LOG_FILE}
else
virtualenv ${VENV_DIR} >> ${LOG_FILE}
fi
fi
source ${THIS_DIR}/venv/bin/activate
}

function install_dependencies {
pip install -r ${THIS_DIR}/requirements.txt >> ${LOG_FILE}
}

function source_settings_and_credentials {
for ENV_FILE in settings.sh credentials.sh
do
if [ -s "${THIS_DIR}/${ENV_FILE}" ]; then
source "${THIS_DIR}/${ENV_FILE}"
fi
done
}

function delete_old_logs {
find ${LOG_DIR} -type f -iname '*.log' -mtime +30 -delete
}

function run_main_code {
export PYTHONIOENCODING="utf-8"
command=${THIS_DIR}/main.py
# Line buffering, see http://unix.stackexchange.com/a/25378
stdbuf -oL -eL run-one ${command} >> ${LOG_FILE} 2>&1
RETCODE=$?
if [ ${RETCODE} != 0 ]; then
echo "$@ exited with code: ${RETCODE}"
git remote -v
tail -v -n 100 ${LOG_FILE}
exit 2
fi

grep -n '^ERROR:' ${LOG_FILE}
}

setup_log_output
setup_virtualenv
install_dependencies
source_settings_and_credentials
run_main_code
delete_old_logs
2 changes: 2 additions & 0 deletions schedule.csv.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
start_date,end_date,auto_follow
2014-09-03,2014-09-04,"@SomeEvent excited"
195 changes: 147 additions & 48 deletions twitter_follow_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,138 @@

The Twitter Follow Bot library is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your option) any
later version.
Free Software Foundation, either version 3 of the License, or (at your option)
any later version.

The Twitter Follow Bot library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
The Twitter Follow Bot library is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.

You should have received a copy of the GNU General Public License along with the Twitter
Follow Bot library. If not, see http://www.gnu.org/licenses/.
You should have received a copy of the GNU General Public License along with
the Twitter Follow Bot library. If not, see http://www.gnu.org/licenses/.
"""

from __future__ import unicode_literals

import csv
import datetime

from collections import OrderedDict

from twitter import Twitter, OAuth, TwitterHTTPError
import os
from os.path import dirname, join as pjoin

import logging
logger = logging.getLogger(__name__)


API_KEY = os.environ['API_KEY']
API_SECRET = os.environ['API_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_TOKEN_SECRET = os.environ['ACCESS_TOKEN_SECRET']
TWITTER_HANDLE = os.environ['TWITTER_HANDLE']

# put your tokens, keys, secrets, and Twitter handle in the following variables
OAUTH_TOKEN = ""
OAUTH_SECRET = ""
CONSUMER_KEY = ""
CONSUMER_SECRET = ""
TWITTER_HANDLE = ""
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'

# put the full path and file name of the file you want to store your "already followed"
# list in
# put the full path and file name of the file you want to store your "already
# followed" list in
ALREADY_FOLLOWED_FILE = "already-followed.csv"

t = Twitter(auth=OAuth(OAUTH_TOKEN, OAUTH_SECRET,
CONSUMER_KEY, CONSUMER_SECRET))
t = Twitter(auth=OAuth(ACCESS_TOKEN, ACCESS_TOKEN_SECRET,
API_KEY, API_SECRET))


class FollowLog(object):

FOLLOW_LOG = pjoin(dirname(__file__), 'follow_log.csv')
FOLLOW_LOG_FIELDS = ('twitter_id', 'screen_name', 'follow_datetime',
'unfollow_datetime', 'follow_reason')

def __init__(self):
self._following = OrderedDict()

@staticmethod
def _empty_row(twitter_id):
row = {k: None for k in FollowLog.FOLLOW_LOG_FIELDS}
row['twitter_id'] = twitter_id
return row

@staticmethod
def _serialize_row(row):
row['follow_datetime'] = row['follow_datetime'].strftime(
DATETIME_FORMAT)
return row

@staticmethod
def _deserialize_row(row):
row['follow_datetime'] = datetime.datetime.strptime(
row['follow_datetime'], DATETIME_FORMAT)
return row

def __enter__(self):
self._load_from_csv()
return self

def __exit__(self, exception_type, exception_value, traceback):
if exception_type is None:
self._save_to_csv()

def _load_from_csv(self):
if not os.path.exists(self.FOLLOW_LOG):
self._following = OrderedDict()
return

with open(self.FOLLOW_LOG, 'r') as f:
self._following = OrderedDict(
[(int(row['twitter_id']), self._deserialize_row(row))
for row in csv.DictReader(f)])

def _save_to_csv(self):
tmp_filename = self.FOLLOW_LOG + '.tmp'
with open(tmp_filename, 'w') as f:
writer = csv.DictWriter(f, self.FOLLOW_LOG_FIELDS)
writer.writeheader()
for twitter_id, row in self._following.items():
row['twitter_id'] = twitter_id
writer.writerow(self._serialize_row(row))

os.rename(tmp_filename, self.FOLLOW_LOG)

def _get_or_create(self, twitter_id):
if twitter_id not in self._following:
self._following[twitter_id] = self._empty_row(twitter_id)
return self._following[twitter_id]

def save_follow(self, twitter_id, reason=None):
entry = self._get_or_create(twitter_id)

entry['follow_datetime'] = datetime.datetime.now()
entry['follow_reason'] = reason

def save_unfollow(self, twitter_id):
entry = self._get_or_create(twitter_id)
entry['unfollow_datetime'] = datetime.datetime.now()

def have_followed_before(self, twitter_id):
entry = self._following.get(twitter_id)
if entry is None: # no record of this twitter id.
return False

if entry['follow_datetime'] is not None:
return True

return False


def get_follow_log():
return FollowLog()


def search_tweets(q, count=100, result_type="recent"):
"""
Returns a list of tweets matching a certain phrase (hashtag, word, etc.)
Returns a list of tweets matching a certain phrase (hashtag, word, etc.)
"""

return t.search.tweets(q=q, result_type=result_type, count=count)
Expand Down Expand Up @@ -90,41 +190,25 @@ def auto_follow(q, count=100, result_type="recent"):
"""

result = search_tweets(q, count, result_type)
following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])

# make sure the "already followed" file exists
if not os.path.isfile(ALREADY_FOLLOWED_FILE):
with open(ALREADY_FOLLOWED_FILE, "w") as out_file:
out_file.write("")

# read in the list of user IDs that the bot has already followed in the
# past
do_not_follow = set()
dnf_list = []
with open(ALREADY_FOLLOWED_FILE) as in_file:
for line in in_file:
dnf_list.append(int(line))

do_not_follow.update(set(dnf_list))
del dnf_list
to_follow = set()

for tweet in result["statuses"]:
try:
if (tweet["user"]["screen_name"] != TWITTER_HANDLE and
tweet["user"]["id"] not in following and
tweet["user"]["id"] not in do_not_follow):
if tweet["user"]["screen_name"] == TWITTER_HANDLE:
continue
print('@{}:\n{}\n'.format(
tweet['user']['screen_name'],
tweet['text']))
to_follow.add(tweet['user']['id'])

t.friendships.create(user_id=tweet["user"]["id"], follow=True)
following.update(set([tweet["user"]["id"]]))
already_following = set(t.friends.ids(screen_name=TWITTER_HANDLE)["ids"])

print("followed %s" % (tweet["user"]["screen_name"]))

except TwitterHTTPError as e:
print("error: %s" % (str(e)))

# quit on error unless it's because someone blocked me
if "blocked" not in str(e).lower():
quit()
to_follow -= already_following
print("Following {} users".format(len(to_follow)))
with get_follow_log() as follow_log:
for twitter_id in to_follow:
_follow(follow_log, twitter_id,
'Tweet: `{}`'.format(tweet['text']))


def auto_follow_followers():
Expand Down Expand Up @@ -181,3 +265,18 @@ def auto_unfollow_nonfollowers():
if user_id not in users_keep_following:
t.friendships.destroy(user_id=user_id)
print("unfollowed %d" % (user_id))


def _follow(follow_log, twitter_id, reason=None):
print(twitter_id)
try:
t.friendships.create(user_id=twitter_id, follow=True)
except TwitterHTTPError as e:
if 'blocked' in str(e).lower(): # ignore block errors
# details: {"errors":[{"code":162,
# "message":"You have been blocked from following this account
# at the request of the user."}]}
logging.info('Ignoring "blocked" exception')
logging.exception(e)
else:
follow_log.save_follow(twitter_id, reason)