MadManoloz
MadManoloz

Reputation: 73

How can I execute a function that is inside a string from inside another function?

I'm trying to make a Reddit Bot in Python but I have encountered an issue. The objective of the bot is to read the comments of Reddit searching for "get_tweets". When it finds this it will read the whole comment that will look similar to this:

get_tweets("TWITTER_USERNAME",NUMBER_OF_TWEETS,"INCLUDE_RE_TWEETS","INCLUDE_REPLIES")

The comment will serve as a function and the 4 parameters will be determined by the user commenting. An example can be:

get_tweets("BarackObama",5,"no","yes")  

I think I have everything covered except for the fact that I can't execute the comment as a function because when I try it gives this error:

SyntaxError: unqualified exec is not allowed in function 'run_bot' it contains a nested function with free variables 

Here is the whole code (except for the authentication for twitter and reddit):

keywords = ['get_tweets']
cache = []
tweets_list = []

def get_tweets(user,num,rt,rp):
    tline = api.user_timeline(screen_name=user, count=num)
    tweets = []
    for t in tline:
        reply = t.in_reply_to_screen_name
        tweet = t.text
        if rt.lower() == 'no' and rp.lower() == 'no':
            if tweet[0:2] != 'RT' and reply == None:
                tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
        if rt.lower() == 'yes' and rp.lower() == 'no':
            if tweet[0:2] == 'RT' and reply == None:
            tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
        if rt.lower() == 'no' and rp.lower() == 'yes':
            if tweet[0:2] != 'RT' and reply != None:
                tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
        if rt.lower() == 'yes' and rp.lower() == 'yes':
            if tweet[0:2] == 'RT' and reply != None:
                tweets.append(tweet + ' | Date Tweeted: ' + str(t.created_at))
    tweets_list = tweets

def run_bot():
    subreddit = r.get_subreddit('test')
    print('Searching...')
    comments = subreddit.get_comments(limit=100)
    for comment in comments:
        comment_text = comment.body.lower()
        isMatch = any(string in comment_text for string in keywords)
        if comment.id not in cache and isMatch:
            print('Found comment: ' + comment_text)
            exec comment_text
            cache.append(comment.id)
            start = []
            end = []
            open_p = comment.index('(')
            text = ''
            for a in re.finditer(',', comment):
                start.append(a.start())
                end.append(a.end())
            num = comment_text[end[0]:start[1]]
            user = comment_text[open_p:start[0]]
            for tweet in tweets_list:
                text.append(' | ' + tweet + '\n\n')
            if num == 1:
                reply = 'Here is the latest tweet from ' + user + ':\n\n' + text + '\n\n***\nI am a bot.'
            else:
                reply = 'Here are the last ' + num + ' tweets from ' + user + ':\n\n' + text + '\n\n***\nI am a bot.'
            comment.reply(reply)         
run_bot()

Upvotes: 3

Views: 119

Answers (2)

MadManoloz
MadManoloz

Reputation: 73

I figured it out guys! I used eval() instead of exec
Thanks to @jwodder for warning me of a very BAD mistake I made and for the rest of you who assisted me in fixing my code.

EDIT: Don't do this. The other guy has a much better solution.

Upvotes: 1

swenzel
swenzel

Reputation: 7173

Using exec or eval is a huge security problem! The best you can try is to extract the parameters from the string using regex or simply decompose it using the , and then call your function with that. Luckily you only need strings and numbers and therefore no dangerous parsing is necessary.
One possible solution would be:

import re

def get_tweets(user, num, rt, rp):
    num = int(num)
    print user, num, rt, rp

comment_text = 'get_tweets("BarackObama",5,"no","yes")'

# capture 5 comma separated parts between '(' and ')'
pattern = r'get_tweets\(([^,]*),([^,]*),([^,]*),([^,]*)\)'
param_parts = re.match(pattern,comment_text).groups()

# strip away surrounding ticks and spaces
user,num,rt,rp = map(lambda x:x.strip('\'" '), param_parts)

# parse the number
num = int(num)

# call get tweets
get_tweets(user,num,rt,rp)

prints:

BarackObama 5 no yes

Disadvantage:

  • Only works if the username does not contain a comma or begin/end with ' or ", which I guess can be assumed (correct me if I'm wrong here).

Advantages:

  • Since the splitting is done at , you can also get rid of the ticks entirely which makes get_tweets(BarackObama,5,no,yes) valid as well.
  • We're using regex which means that the comment may contain additional text and we extract only what we need.
  • If anyone wanted to inject code, you would just get a weird username, or a wrong number of arguments or an int which is not parsable or an invalid argument for rt/rp... which would all lead to an exception or no tweets at all.
  • You can actually return a value from get_tweets and don't need to use a global variable.

Upvotes: 3

Related Questions