Python Twitter Bot

Just created a twitter bot with python and tweepy (a python library to interact with twitter, http://www.tweepy.org/) which is following a defined set of users. This users are gathered from defined user’s followers. After a defined lease of days this followers are became unfollowed if they did not followed back during a new execution of the script.

This script also handles the max API calls per execution. –> normally the max limit is set to 300/15min.
All data will be stored and updated in a local CSV.

Basic steps:

1. Create an twitter application and describe the purpose and details. (https://developer.twitter.com/en/apps/)

App Name: Choose a unique name
Description: Description of your application
Sign in with Twitter: Enabled
Callback URL: http://127.0.0.1
App usage: Purpose and description of your application

2. Get the Keys and Tokens

Get the API keys from tab: “Keys and tokens” and store them in a different file

# Variables that contain the user credentaials to access Tiwtter APIself.
ACCESS_TOKEN = "1...i"
ACCESS_TOKEN_SECRET = "d...6"
CONSUMER_KEY = "M...L"
CONSUMER_SECRET = "6...X"
3. Explore basic functions and API docs

Tweepy docs: http://docs.tweepy.org/en/v3.5.0/

Authentication
import tweepy        
from tweepy import OAuthHandler      
import twitter_creds         
auth = OAuthHandler(twitter_creds.CONSUMER_KEY, twitter_creds.CONSUMER_SECRET)       
auth.set_access_token(twitter_creds.ACCESS_TOKEN, twitter_creds.ACCESS_TOKEN_SECRET)
Create a Tweet
tweet = api.update_status(‘Hello World!’)
Delete a Tweet
tweet = api.update_status('something')   
api.destroy_status(tweet.id_str)
Follow
api.create_friendship('@vmwarecloudaws')
Unfollow
api.destroy_friendship('@vmwarecloudaws')

Structure of CSV:

id,screen_name,scannedOn,scannedFrom,followedOn,followedBackOn,unfollowedOn,state,recordState
2466userId6,screen_name,2019-03-18 11:49:30,screen_name,2019-03-23 11:49:30,2019-03-21 11:49:30,,bidirectional,True

Python script:

import csv #interact with CSV
import sys #import sys for terminating script
import datetime
import time
import random #to pick a user to get followers from - defined in list "toGetFollowersFrom"
import tweepy
from tweepy import OAuthHandler
from tweepy.parsers import RawParser
import twitter_creds

#global list with user objects imported from CSV
twitters = list()
users = list()
apiInteractions = 1 #starts on 1 because of the authentication

#parameters
maxAllowedApiInteractions = 200 #script will end after this number is reached
ownScreen_Name = 'raphi_gassmann' #without (at)
daysForFollowersToFollowBack = 7
toGetFollowersFrom = ["user1", "user2"]
#csv filepath --> first row needs to be headers: id, screen_name, scannedOn, scannedFrom, followedOn, followedBackOn, unfollowedOn, state, recordState
csvFilepath = '/Users/rgassmann/Desktop/twitter-bot/twitterBot.csv'

#authentication - https://developer.twitter.com/en/apps/
auth = OAuthHandler(twitter_creds.CONSUMER_KEY, twitter_creds.CONSUMER_SECRET)
auth.set_access_token(twitter_creds.ACCESS_TOKEN, twitter_creds.ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)
#api = tweepy.API(auth_handler = auth, parser = rawParser)

# ========================== classes ==========================
class User:
    def __init__(self, id, screen_name, scannedOn, scannedFrom, followedOn, followedBackOn, unfollowedOn, state, recordState):
        self.id = id
        self.screen_name = screen_name
        self.scannedOn = scannedOn
        self.scannedFrom = scannedFrom
        self.followedOn = followedOn
        self.followedBackOn = followedBackOn
        self.unfollowedOn = unfollowedOn
        self.state = state
        self.recordState = recordState

    def follow(self):
        '''
        will create follow friendship with defined user
        '''
        global apiInteractions
        try:
            apiInteractionAllowed()
            apiUser = api.create_friendship(self.id) #add as follower
            apiInteractions += 1 #increase apiInteraction
            print 'Followed user: %s, userId: %s' % (apiUser.screen_name, apiUser.id)
            now = datetime.datetime.now()
            global newUser
            newUser = User(apiUser.id, apiUser.screen_name, self.scannedOn, self.scannedFrom, now.strftime('%Y-%m-%d %H:%M:%S'), self.followedBackOn, self.unfollowedOn, 'followed', True)
            #update CSV
            setRowToDeleted(self.id, csvFilepath)
            newUserToCsv(newUser, csvFilepath)
            twitters.append(newUser)
        except tweepy.TweepError as e:
            if (e.message[0]['code'] == 160): #already requested to follow
                print 'Alredy requested to follow user: %s, updating CSV...' % (self.screen_name)
                now = datetime.datetime.now()
                newUser = User(self.id, self.screen_name, self.scannedOn, self.scannedFrom, now.strftime('%Y-%m-%d %H:%M:%S'), self.followedBackOn, self.unfollowedOn, 'followed', True)
                #update CSV
                setRowToDeleted(self.id, csvFilepath)
                newUserToCsv(newUser, csvFilepath)
                twitters.append(newUser)
            if (e.message[0]['code'] == 89): #expired ACCESS_TOKEN
                args = ('Invalid or expired token. Failed to unfollow user: %s, id: %s') % (self.screen_name, self.id)
                sys.exit(args) #end script
            if (e.meassage[0]['code'] == 161): #followed more than 1000 this day
                args = ('The limit to follow more than 1000 account this day is reached. More information here: https://help.twitter.com/en/using-twitter/twitter-follow-limit')
                sys.exit(args) #end script
            if (e.message[0]['code'] == 34) or (e.message[0]['code'] == 108): #page does not exist, err108=cannot find specific user
                #delete user, seems is not existing any more
                setRowToDeleted(self.id, csvFilepath)
            else:
                print 'Following failed for user.screen_name: %s, user.id: %s' % (self.screen_name, self.id)
                print e
            pass

    def unfollow(self):
        '''
        will destroy / unfollow defined user
        '''
        global apiInteractions
        try:
            apiInteractionAllowed()
            apiUser = api.destroy_friendship(self.id) #remove as followed
            apiInteractions += 1 #increase apiInteraction
            print 'Unfollowed user: %s, userId: %s' % (apiUser.screen_name, apiUser.id)
            now = datetime.datetime.now()
            global newUser
            newUser = User(apiUser.id, apiUser.screen_name, self.scannedOn, self.scannedFrom, self.followedOn, self.followedBackOn, now.strftime('%Y-%m-%d %H:%M:%S'), 'unfollowed', True)
            #update CSV
            setRowToDeleted(self.id, csvFilepath)
            newUserToCsv(newUser, csvFilepath)
            twitters.append(newUser)
        except tweepy.TweepError as e:
            if (e.message[0]['code'] == 89): #expired ACCESS_TOKEN
                args = ('Invalid or expired token. Failed to unfollow user: %s, id: %s') % (self.screen_name, self.id)
                sys.exit(args) #end script
            if (e.message[0]['code'] == 34) or (e.message[0]['code'] == 108): #page does not exist, err108=cannot find specific user
                #delete user, seems is not existing any more
                setRowToDeleted(self.id, csvFilepath)
            else:
                print e
            pass

# ========================== pre checks ==========================
def followedCleanUpNeeded(daysSinceFollowedOn):
    """
    Is checking CSV if there are user in state=followed but not yet confirmed to followed back
    AND if they are followed more than 7 days ago
    AND if there are more than 10 users to perform the action on
    returning list of users which did not followed back
    """
    print ' ====== CheckCleanUpNeeded ====== '
    global apiInteractions
    apiInteractions += 1
    apiInteractionAllowed()
    followers = tweepy.Cursor(api.followers, screen_name=ownScreen_Name, count=200).items()

    while True:
        try:
            follower = next(followers)
        except tweepy.TweepError as e:
            apiInteractionAllowed()
            print 'request of followers of %s timed out' % (ownScreen_Name)
            print e
            apiInteractions += 1
            time.sleep(60*15)
            follower = next(followers)
        except StopIteration:
            break

        for user in twitters:
            if (str(user.id) == str(follower.id) and user.recordState == True and user.state != 'bidirectional'):
                now = datetime.datetime.now()
                newUser = User(follower.id, follower.screen_name, user.scannedOn, user.scannedFrom, user.followedOn, now.strftime('%Y-%m-%d %H:%M:%S'), user.unfollowedOn, 'bidirectional', True)
                setRowToDeleted(str(follower.id), csvFilepath)
                newUserToCsv(newUser, csvFilepath)
                print 'new follower found and updated - screen_name: %s' % (follower.screen_name)

    compareDate = datetime.datetime.now()+datetime.timedelta(days=-daysSinceFollowedOn, hours=0)     #today minus daysSinceFollowedOn pararmeter
    cleanUpUsers = list()
    print 'compareDate: %s' % (compareDate)
    for user in twitters:
        if (user.followedOn != '' and user.state == 'followed' and user.followedBackOn == '' and user.recordState == True): #check first if followedOn has content
            followedOnDatetime = datetime.datetime.strptime(user.followedOn, '%Y-%m-%d %H:%M:%S')
            if (followedOnDatetime < compareDate):
                cleanUpUsers.append(user)
    print 'CleanUp needed for %s users' % (len(cleanUpUsers))
    return cleanUpUsers

def newUsersToFollow():
    """
    Is checking CSV if there are user in state=new to followed
    AND if there are more than threshold parameter defined users to perform the action on
    returning list of users objects
    """
    print ' ====== CheckIfNewUsersToFollowExists ====== '
    counter = 0
    toFollow = list()
    for user in twitters:
        if (user.state == 'new' and user.recordState == True and user.followedOn == ''):
            toFollow.append(user)
    return toFollow

def apiInteractionAllowed():
    global apiInteractions
    global maxAllowedApiInteractions
    if (apiInteractions>=maxAllowedApiInteractions):
        args = ('Script terminated, because of: max API calls reached, API interactions: %s') % (apiInteractions)
        sys.exit(args) #end script

# ========================== CSV interactions ==========================
def readCsv(filepath):
    '''
    reads defined CSV and creates user objects
    '''
    file = open(filepath)
    global users
    users = list(csv.reader(file, delimiter=','))
    #checking headers row
    if (users[0][0] == 'id' and users[0][1] == 'screen_name' and users[0][2] == 'scannedOn' and users[0][3] == 'scannedFrom' and users[0][4] == 'followedOn' and users[0][5] == 'followedBackOn' and users[0][6] == 'unfollowedOn' and users[0][7] == 'state' and users[0][8] == 'recordState'):
        #slicing data rows
        #users = users[1:][:] # not needed
        print 'valid CSV provided as input'
    else:
        print 'invalid CSV provided as input - please check headers row'
        pass

    for idx, val in enumerate(users):
        #check if record is set as active
        if (str_to_bool(users[idx][8]) == True):
            if (users[idx][0] != 'id' and users[idx][1] != 'screen_name'): # exclude headers
                #create user object
                user = User(users[idx][0], users[idx][1], users[idx][2], users[idx][3], users[idx][4], users[idx][5], users[idx][6], users[idx][7], str_to_bool(users[idx][8]))
                #append user to global twitters list
                twitters.append(user)
        else:
            #record is set as deleted, nothing to do
            pass

def setRowToDeleted(userid, filepath):
    '''
    sets a user record csv to inactive / deleted
    '''
    #update object in twitters list
    for user in twitters:
        if (user.id == userid):
            user.recordState = False
    #update CSV
    for idx, val in enumerate(users):
        if (users[idx][0]==userid and str_to_bool(users[idx][8]) == True):
            users[idx][8] = 'False'
            writer = csv.writer(open(filepath, 'w'))
            writer.writerows(users)

def newUserToCsv(user, filepath):
    '''
    writes a new user object to CSV
    '''
    newUser = []
    newUser.append(user.id)
    newUser.append(user.screen_name)
    newUser.append(user.scannedOn)
    newUser.append(user.scannedFrom)
    newUser.append(user.followedOn)
    newUser.append(user.followedBackOn)
    newUser.append(user.unfollowedOn)
    newUser.append(user.state)
    newUser.append(user.recordState)

    users.append(newUser)

    writer = csv.writer(open(filepath, 'w'))
    writer.writerows(users)

def str_to_bool(s):
    '''
    converts a string to a boolean and retuning it
    '''
    if s.upper() == 'TRUE':
         return True
    elif s.upper() == 'FALSE':
         return False
    else:
         return s

# ========================== gathering new followers from ... ==========================
def gatheringUsersToFollow(username):
    global apiInteractions
    apiInteractions += 1

    followers = tweepy.Cursor(api.followers, screen_name=username, count=200).items()

    while True:
        try:
            follower = next(followers)
        except tweepy.TweepError:
            apiInteractionAllowed()
            apiInteractions += 1
            time.sleep(60*15)
            follower = next(followers)
        except StopIteration:
            break

        userAlreadyExist = bool(False)
        for user in twitters:
            if (str(user.id)==str(follower.id)):
                userAlreadyExist = bool(True)

        if not userAlreadyExist:
            now = datetime.datetime.now()
            newUser = User(follower.id, follower.screen_name, now.strftime('%Y-%m-%d %H:%M:%S'), username, '', '', '', 'new', True)
            newUserToCsv(newUser, csvFilepath)
            print 'new user added as state: new to follow - screen_name: %s' % (follower.screen_name)

# ========================== main ==========================
if __name__ == '__main__':
    #open CSV
    readCsv(csvFilepath)

    # PRIO 1: CleanUp
    toUnfollow = followedCleanUpNeeded(daysForFollowersToFollowBack)
    if (len(toUnfollow)>0):
        print 'CleanUp needed! --> running CleanUp'
        for user in toUnfollow:
            print 'user for cleanup: %s' % (user.screen_name)
            user.unfollow() # --> just be sure !!!!sys.exit('just check')
    # PRIO 2: follow
    toFollow = newUsersToFollow()
    if len(toFollow)>20:
        print 'Following needed --> found %s users to follow' % (len(toFollow))
        for user in toFollow:
            user.follow()
    # PRIO 3: get followers of
    gatheringUsersToFollow(random.choice(toGetFollowersFrom))

    args = ('Script terminated, because of: All actions performed (no limit reached), API interactions: %s') % (apiInteractions)
    sys.exit(args) #end script
Bot Schema