Newer
Older
"""This is the XKCD Alt Text Bot.
This bot checks once a minute for new Tweets from @xkcdComic. If one is found, it accesses the
linked comic, extracts the image alt text, and Tweets it as a reply."""
import time # Program sleeping
import os # API keys & access tokens
import requests # Accessing API
from requests_oauthlib import OAuth1 # OAuth
from bs4 import BeautifulSoup # HTML searching
class Twitter():
"""This class handles all API requests to Twitter."""
def __init__(self, auth):
"""This class constructor collects the OAuth keys for the class."""
self.auth = auth
def get(self):
"""This function determines if a new comic has been posted, and returns it.
Mentions of 'comic' refer to @xkcdComic, mentions of 'alt' refer to @XKCDAltTextBot."""
# Build payloads for comic bot and alt text bot search
comic_payload = {'q': 'from:xkcdComic', 'result_type': 'recent', 'count': '1'}
alt_payload = {'q': 'from:XKCDAltTextBot', 'result_type': 'recent', 'count': '1'}
# Retrieve data from Twitter searches
for attempt in range(6):
if attempt == 5: # Too many attempts
print('Twitter search failed ({}), see below response.'.format(str(comic_raw.status_code)))
print('Twitter error message:\n\n{}'.format(comic_raw.json()))
del alt_payload, comic_payload
return 'crash' # Enter log protection mode
print('Searching for new comics...')
alt_raw = requests.get('https://api.twitter.com/1.1/search/tweets.json',
params=alt_payload, auth=self.auth)
comic_raw = requests.get('https://api.twitter.com/1.1/search/tweets.json',
params=comic_payload, auth=self.auth)
if comic_raw.status_code == 200: # Good request
pass
elif comic_raw.status_code >= 429 or comic_raw.status_code == 420:
# Twitter issue or rate limiting
print('Twitter search failed ({})'.format(comic_raw.status_code))
print('Reattempting in 5 minutes...')
time.sleep(300) # sleep for 5 minutes and reattempt
continue
else: # Other problem in code
print('Twitter search failed ({}), see below '.format(str(comic_raw.status_code)) +
'response.')
print('Twitter error message:\n\n{}'.format(comic_raw.json()))
del alt_payload, comic_payload, alt_raw, comic_raw
return 'crash' # Enter log protection mode
# Convert to JSON
alt = alt_raw.json()
comic = comic_raw.json()
if alt['statuses'][0]['id'] is None or \
comic['statuses'][0]['in_reply_to_status_id'] is None:
print('Twitter search failed: No Tweet found')
del alt_payload, comic_payload, alt_raw, comic_raw, alt, comic
return 'crash' # Enter log protection mode
if alt['statuses'][0]['id'] == comic['statuses'][0]['in_reply_to_status_id']:
# This tweet has already been replied to
del alt_payload, comic_payload, alt_raw, comic_raw, alt, comic
return None # Sleep for 60 seconds
else:
# This tweet has not been replied to
del alt_payload, comic_payload, alt_raw, comic_raw, alt
return comic['statuses'][0] # Return comic Tweet
def post(self, tweet, reply):
"""This function Tweets the alt (title) text as a reply to @xkcdComic."""
print('Tweeting...')
tweet_payload = {'status': tweet, 'in_reply_to_status_id': reply,
'auto_populate_reply_metadata': 'true'}
# POST Tweet
for attempt in range(6):
if attempt == 5: # Too many attempts
print('Tweeting failed ({}), see below response.'.format(str(tweet.status_code)))
print('Twitter error message:\n\n{}'.format(tweet.json()))
del tweet_payload
return 'crash' # Enter log protection mode
tweet = requests.post('https://api.twitter.com/1.1/statuses/update.json', json=tweet_payload,
auth=self.auth)
if tweet.status_code == 200: # Good request
print('Successfully Tweeted:\n\n{}'.format(tweet.json()))
del tweet, tweet_payload
return None
elif tweet.status_code >= 429 or tweet.status_code == 420 or \
tweet.status_code == 403:
# Twitter issue or rate limiting
print('Tweeting failed ({})'.format(tweet.status_code))
print('Reattempting in 5 minutes...')
time.sleep(300) # sleep for 5 minutes and reattempt
continue
else: # Other problem in code
print('Tweeting failed ({}), see below response.'.format(str(tweet.status_code)))
print('Twitter error message:\n\n{}'.format(tweet.json()))
del tweet, tweet_payload
return 'crash' # Enter log protection mode
def get_auth():
"""This function retrieves the API keys and access tokens from environmental variables."""
print("Building OAuth header...")
key = [os.environ.get('XKCD_API_KEY', None),
os.environ.get('XKCD_API_SECRET_KEY', None),
os.environ.get('XKCD_ACCESS_TOKEN', None),
os.environ.get('XKCD_ACCESS_SECRET_TOKEN', None)]
for i in key:
if i is None: # Verify keys were loaded
print("OAuth initiation failed: Environmental variable not found")
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
del key
return 'crash' # Enter log protection mode
auth = OAuth1(key[0], key[1], key[2], key[3])
print('OAuth initiation successful!')
del key
return auth
def retrieve_text(site):
"""This retrieves the HTML of the website, isolates the image title text, and formats it for the
Tweet."""
for attempt in range(11):
print('Accessing {} (attempt {} of 11'.format(site, attempt+1))
html_raw = requests.get(site) # Retrieving raw HTML data
if html_raw.status_code != 200: # Data not successfully retrieved
if attempt < 6:
print('Could not access XKCD ({}). '.format(html_raw.status_code) +
'Trying again in 10 seconds...')
time.sleep(10) # Make 6 attempts with 10 second delays
elif attempt < 10:
print('Could not access XKCD ({}). '.format(html_raw.status_code) +
'Trying again in 60 seconds...')
time.sleep(60) # Make 4 attempts with 60 seconds delays
else:
print('XKCD retrieval failed: could not access {}'.format(site))
return 'crash' # Enter log protection mode
html = BeautifulSoup(html_raw, 'html.parser')
comic = html.find('img', title=True) # Locates the only image with title text (the comic)
if comic is None:
print('Title extraction failed: image not found')
return 'crash' # Enter log protection mode
title = comic['title'] # Extracts the title text
tweet = 'Alt/title text: "{}"'.format(title) # Construct the main Tweet body
print('Tweet constructed')
del html_raw, html, comic, title
return tweet
def crash():
"""This function protects logs by pinging google.com every 20 minutes."""
print('Entering log protection mode.')
while True:
a = requests.get('https://google.com') # Ping Google
del a
time.sleep(1200)
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
continue
# Main program
# All mentions of 'crash' mean the program has, and is entering log protection mode
auth = get_auth() # Build authentication header
if auth == 'crash':
crash()
twitter = Twitter(auth)
while True: # Initialize main account loop
original_tweet = twitter.get() # Check for new comics
if original_tweet == 'crash':
crash()
elif original_tweet is None:
print('No new comics found. Sleeping for 60 seconds...')
time.sleep(60)
continue
else:
body = retrieve_text(original_tweet['entities']['urls'][0]['expanded_url']) # Build Tweet
if body == 'crash':
crash()
result = twitter.post(body, original_tweet['id_str']) # Post Tweet
if result == 'crash':
crash()
elif result is None: # Successful Tweet
print('Sleeping for 60 seconds...')
time.sleep(60)
continue