Skip to content

Commit

Permalink
Feature/issue 24 use reg ex to parse commands (#26)
Browse files Browse the repository at this point in the history
* Add env cli parameter to easily switch between active deploy and testing

* Add env parameter to run script

* Remove need for commands and major refactor 

* Fixed bugs. Bot appears to be working now

* Update HELP text and add version # to TAIL

* Move/Remove unused functions

Co-authored-by: Alex Burkey <[email protected]>
  • Loading branch information
AlexBurkey and Alex Burkey authored Jul 10, 2020
1 parent 2665e2c commit 072aa47
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 166 deletions.
326 changes: 168 additions & 158 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,101 @@
#!/usr/bin/env python3
import re
import os
import sys
import praw
import json
import sqlite3
import requests
from string import Template
from dotenv import load_dotenv
import my_strings as ms
import helpers as h


# References praw-ini file
UA = 'MFAImageBot'
DB_FILE = 'mfa.db'
BATSIGNAL = '!mfaimagebot'
SUBREDDIT_NAME = "malefashionadvice"
IMGUR_ALBUM_API_URL = 'https://api.imgur.com/3/album/${album_hash}/images'
IMGUR_GALLERY_API_URL = 'https://api.imgur.com/3/gallery/album/${gallery_hash}'
DIRECT_LINK_TEMPLATE = '[Direct link to image #${index}](${image_link}) \nImage number ${index} from album ${album_link}'

TODO_TEXT = "Sorry, this function has not been implemented yet.\n\n"
HELP_TEXT = ("Usage: I respond to comments starting with `!MFAImageBot`. \n"
"`!MFAImageBot help`: Print this help message. \n"
"`!MFAImageBot link <album-link> <number>`: Attempts to directly link the <number> image from <album-link> \n"
"`!MFAImageBot op <number>`: Attempts to directly link the <number> image from the album in the submission \n"
)
TAIL = ("\n\n---\nI am a bot! If you've found a bug you can open an issue "
"[here.](https://github.com/AlexBurkey/MFAImageBot/issues/new?template=bug_report.md) \n"
"If you have an idea for a feature, you can submit the idea "
"[here](https://github.com/AlexBurkey/MFAImageBot/issues/new?template=feature_request.md)")
def run():
r = praw.Reddit(USER_AGENT)
load_dotenv() # Used for imgur auth
# TODO: verify that the db path is valid.
# A single file is fine but dirs are not created if they don't exist
print("Looking for comments...")
for comment in r.subreddit(SUBREDDIT_NAME).stream.comments():
if check_batsignal(comment.body) and not check_has_responded(comment):
response = ms.HELP_TEXT
tokens = h.get_and_split_first_line(comment.body)
# More than 100 tokens on the first line.
# I'm not dealing with that
if len(tokens) > 100:
db_obj = h.reply_and_upvote(comment, response=ms.HELP_TEXT, respond=RESPOND)
add_comment_to_db(db_obj)
continue

# Check for help
if parse_comment(tokens)['help']:
db_obj = h.reply_and_upvote(comment, response=ms.HELP_TEXT, respond=RESPOND)
add_comment_to_db(db_obj)
continue

index = parse_comment(tokens)['index']
if index == -1:
db_obj = h.reply_and_upvote(comment, response=ms.HELP_TEXT, respond=RESPOND)
add_comment_to_db(db_obj)
continue

# TODO: Wrap/deal with possible exceptions from parsing imgur url
comment_imgur_url = parse_comment(tokens)['imgur_url']

# Check/parse imgur
album_link_map = None
album_link = None
if comment_imgur_url is not None:
album_link_map = parse_imgur_url(comment_imgur_url)
album_link = comment_imgur_url
elif h.is_imgur_url(comment.submission.url):
album_link_map = parse_imgur_url(comment.submission.url)
album_link = comment.submission.url
else:
# No imgur link found. Respond
response = 'Sorry, no imgur link found in your comment or the link of the OP.'
db_obj = h.reply_and_upvote(comment, response=response, respond=RESPOND)
add_comment_to_db(db_obj)
continue

album_link_type = album_link_map['type']
album_link_id = album_link_map['id']

# TODO: Wrap in try/except
request_url = build_request_url(album_link_type, album_link_id)

r = send_imgur_api_request(request_url)
if r is not None and r.status_code == 200:
response = None
image_link = None
try:
# Parse request and set response text
image_link = get_direct_image_link(r.json(), album_link_type, index)
s = Template(DIRECT_LINK_TEMPLATE)
response = s.substitute(index=index, image_link=image_link, album_link=album_link)
except IndexError:
response = 'Sorry that index is out of bounds.'
db_obj = h.reply_and_upvote(comment, response=response, respond=RESPOND)
add_comment_to_db(db_obj)
print(f"Request url: {request_url}")
print(f'Image link: {image_link}')
continue
else: # Status code not 200
# TODO: Deal with status codes differently, like if imgur is down or I don't have the env configured
print(f'Status Code: {r.status_code}')
response = f'Sorry, {album_link} is probably not an existing imgur album, or Imgur is down.'
db_obj = h.reply_and_upvote(comment, response=response, respond=RESPOND)
add_comment_to_db(db_obj)
print(f"Request url: {request_url}")
continue


def check_batsignal(comment_body):
Expand Down Expand Up @@ -69,151 +138,67 @@ def check_has_responded(comment):
return val


def bot_action(c, verbose=True, respond=False):
response_text = 'bot_action text'
response_type = None
tokens = c.body.split()
# If there's a command in the comment parse and react
# Break this out into a "parse_comment_tokens" function
# Alternatively "set_response_text" or something
if len(tokens) > 1:
response_type = tokens[1].lower()
if response_type == 'help':
response_text = HELP_TEXT
elif response_type == 'link' or response_type == 'op':
# TODO: Wrapping the whole thing in a try-catch is a code smell
try:
link_index_album = get_direct_image_link(c, tokens[:4])
image_link = link_index_album['image_link']
index = link_index_album['index']
album_link = link_index_album['album_link']
s = Template(DIRECT_LINK_TEMPLATE)
response_text = s.substitute(index=index, image_link=image_link, album_link=album_link)
except ValueError as e:
print(str(e))
response_text = str(e)
except IndexError:
response_text = 'Sorry that index is out of bounds.'
else:
response_text = TODO_TEXT + HELP_TEXT
# Otherwise print the help text
else:
response_text = HELP_TEXT

if respond:
c.reply(response_text + TAIL)
c.upvote()

# Adding everything to the DB
# TODO: Make "responded" more dependent on whether we were actually able to respond to the comment.
# and not just what the bot is being told to do.
db_obj = {'hash': c.id, 'has_responded': respond, 'response_type': response_type}
print(f"Hash: {db_obj['hash']}")
print(f"Has responded: {db_obj['has_responded']}")
print(f"Response type: {db_obj['response_type']}")
add_comment_to_db(db_obj)

# Logging
if verbose:
tokens = c.body.encode("UTF-8").split()
# print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print("Comment Body: ")
print(c.body.encode("UTF-8"))
print(f"Tokens: {tokens}")
print("Response comment body: ")
print(response_text.encode("UTF-8"))
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
def parse_comment(tokens):
pairs = {'index': -1, 'imgur_url': None, 'help': False}
# Find the numbers in the tokens
for s in tokens:
# Look for help
if s.lower() == 'help':
pairs['help'] = True
break
# Find the numbers in the tokens
if h.isInt(s):
pairs['index'] = int(s)
# Find imgur URL in the tokens
if h.is_imgur_url(s):
pairs['imgur_url'] = s
return pairs


def get_direct_image_link(comment, tokens):
def build_request_url(imgur_link_type, imgur_link_id):
"""
Gets the direct link to an image from an imgur album based on index.
Tokens should look like:
['!MFAImageBot', 'link', '<imgur-link>', '<index>']
or
['!MFAImageBot', 'op', '<index>']
Create the Imgur Request URL.
Imgur Galleries and Albums have different endpoints.
Any other resource type hasn't been implemented/might not exist.
TODO: doctests
"""
imgur_url = None
index = None
image_link = ''
if len(tokens) >= 4 and tokens[1] == 'link':
# Correct num parameters for album provided
imgur_url = tokens[2]
index = get_index_from_string(tokens[3])
elif len(tokens) >= 3 and tokens[1] == 'op':
# Correct num parametrs for album in OP
# This should generate a 404 or something if the post isn't a link to imgur.
imgur_url = comment.submission.url
index = get_index_from_string(tokens[2])
if imgur_link_type == 'album':
s = Template(IMGUR_ALBUM_API_URL)
return s.substitute(album_hash=imgur_link_id)
elif imgur_link_type == 'gallery':
s = Template(IMGUR_GALLERY_API_URL)
return s.substitute(gallery_hash=imgur_link_id)
else:
print('Looks like a malformed `link` or `op` command')
raise ValueError(f'Malformed `{tokens[1]}`` command.')
raise ValueError('Sorry, that imgur resource hasn\'t been implemented yet.')

# This can raise an exception which is fine
link_type_and_id = parse_imgur_url(imgur_url)
imgur_resource_type = link_type_and_id['type']
resource_id = link_type_and_id['id']
r = None
if link_type_and_id is not None:
# Galleries and Albums have different URL endpoints
url = ''
if imgur_resource_type == 'album':
s = Template(IMGUR_ALBUM_API_URL)
url = s.substitute(album_hash=resource_id)
elif imgur_resource_type == 'gallery':
s = Template(IMGUR_GALLERY_API_URL)
url = s.substitute(gallery_hash=resource_id)
else:
raise ValueError('Sorry, that imgur resource hasn\'t been implemented yet.')

# Send the request
client_id = os.getenv('IMGUR_CLIENT_ID')
headers = {'Authorization': f'Client-ID {client_id}'}
print(f"Request url: {url}")
r = requests.get(url, headers=headers)

# Since I thought 2 lines would make sense here this is probably a good place to separate methods
if r is not None and r.status_code == 200:
# happy path for now
r_json = r.json()
image_link = ''
if imgur_resource_type == 'gallery':
# Gallery: g_response.data.images[index].link
image_link = r_json['data']['images'][index-1]['link']
elif imgur_resource_type == 'album':
# Album: a_response.data[index].link
image_link = r_json['data'][index-1]['link']
else:
raise ValueError('This should be unreachable. Please respond to this comment or open an issue so I see it.')
print(f'Image link: {image_link}')
return {'image_link': image_link, 'index': index, 'album_link': imgur_url}
else: # Status code not 200
# TODO: Deal with status codes differently, like if imgur is down or I don't have the env configured
print(f'Status Code: {r.status_code}')
raise ValueError(f'Sorry, {imgur_url} is probably not an existing imgur album.')
def send_imgur_api_request(request_url):
"""
Send API request to Imgur
TODO: doctests
"""
# Send the request
client_id = os.getenv('IMGUR_CLIENT_ID')
headers = {'Authorization': f'Client-ID {client_id}'}
return requests.get(request_url, headers=headers)


def get_index_from_string(str):
def get_direct_image_link(r_json, imgur_link_type, index):
"""
Wrap this in a try-except because I don't like the error message
Parse Imgur API request response JSON for the direct image link.
If the index is out of bounds will throw an IndexError.
>>> get_index_from_string('1')
1
>>> get_index_from_string('1.1')
Traceback (most recent call last):
...
ValueError: Sorry, "1.1" doesn't look like an integer to me.
>>> get_index_from_string('notAnInt')
Traceback (most recent call last):
...
ValueError: Sorry, "notAnInt" doesn't look like an integer to me.
TODO: Tests for this are sorely needed. Unsure what happens with all of this refactoring.
I _think_ only an IndexError is thrown now.
"""
index = None
try:
index = int(str)
except ValueError:
raise ValueError(f'Sorry, "{str}" doesn\'t look like an integer to me.')
return index
if imgur_link_type == 'gallery':
# Gallery: g_response.data.images[index].link
return r_json['data']['images'][index-1]['link']
elif imgur_link_type == 'album':
# Album: a_response.data[index].link
return r_json['data'][index-1]['link']
else:
raise ValueError('This should be unreachable. Please respond to this comment or open an issue so I see it.')


# Lol yanked this whole thing from SE
Expand Down Expand Up @@ -259,10 +244,17 @@ def parse_imgur_url(url):


def add_comment_to_db(db_dict):
"""
Adds the comment and its info to the database
"""
# print(f"Hash: {db_dict['hash']}")
print(f"Has responded: {db_dict['has_responded']}")
print(f"Response text: ")
print(f"{db_dict['response_text']}")
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
# https://stackoverflow.com/questions/19337029/insert-if-not-exists-statement-in-sqlite
cur.execute('INSERT OR REPLACE INTO comments VALUES (:hash, :has_responded, :response_type)', db_dict)
cur.execute('INSERT OR REPLACE INTO comments VALUES (:hash, :has_responded, :response_text)', db_dict)
conn.commit()
conn.close()

Expand All @@ -274,7 +266,7 @@ def db_setup(db_file):
cur.execute('''CREATE TABLE IF NOT EXISTS comments (
comment_hash TEXT NOT NULL UNIQUE,
has_responded INTEGER DEFAULT 0,
response_type TEXT DEFAULT NULL,
response_text TEXT DEFAULT NULL,
CHECK(has_responded = 0 OR has_responded = 1)
)''')
conn.commit()
Expand All @@ -283,14 +275,32 @@ def db_setup(db_file):


if __name__ == '__main__':
r = praw.Reddit(UA)
load_dotenv() # Used for imgur auth
# TODO: verify that the db path is valid.
# A single file is fine but dirs are not created if they don't exist
env = sys.argv[1]
USER_AGENT = ''
DB_FILE = ''
SUBREDDIT_NAME = ''
RESPOND = False
print(f"Running bot in env: {env}")

if env == 'test':
USER_AGENT = 'MFAImageBotTest'
DB_FILE = 'test.db'
SUBREDDIT_NAME = 'mybottestenvironment'
RESPOND = False
elif env == 'prod':
USER_AGENT = ms.USER_AGENT
DB_FILE = ms.DB_FILE
SUBREDDIT_NAME = ms.SUBREDDIT_NAME
RESPOND = True
else:
print("Not a valid environment: test or prod.")
print("Exiting...")
sys.exit()
# file-scope vars are set above
print(f"User agent: {USER_AGENT}")
print(f"DB file: {DB_FILE}")
print(f"Subreddit name: {SUBREDDIT_NAME}")
print(f"Respond: {RESPOND}")
print("~~~~~~~~~~")
db_setup(DB_FILE) # TODO: set db file path as CLI parameter
print("Looking for comments...")
for comment in r.subreddit(SUBREDDIT_NAME).stream.comments():
if check_batsignal(comment.body) and not check_has_responded(comment):
print(f"Comment hash: {comment}")
# TODO: Set respond bool as CLI input value
bot_action(comment, respond=True)
run()
Loading

0 comments on commit 072aa47

Please sign in to comment.