Skip to content

Commit

Permalink
Init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeremy Simpson authored and Jeremy Simpson committed Jan 20, 2015
0 parents commit 94ddc77
Show file tree
Hide file tree
Showing 22 changed files with 1,247 additions and 0 deletions.
32 changes: 32 additions & 0 deletions .gitignore
@@ -0,0 +1,32 @@
# My stuff
.DS_Store
.idea/
local_settings.py

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# Installer logs
pip-log.txt
pip-delete-this-directory.txt
15 changes: 15 additions & 0 deletions base/Makefile
@@ -0,0 +1,15 @@
.PHONY: install

install:
make clean
python setup.py install
make clean

develop:
make clean
python setup.py develop
rm -rf build/ dist/

clean:
rm -rf build/ dist/ *.egg-info
find . -name '*.pyc' -delete
4 changes: 4 additions & 0 deletions base/install/requirements.txt
@@ -0,0 +1,4 @@
gevent>=1.0.1
praw>=2.1.19
pylru>=1.0.6
requests>=2.5.1
1 change: 1 addition & 0 deletions base/redditbot/__init__.py
@@ -0,0 +1 @@
__import__("pkg_resources").declare_namespace(__name__)
4 changes: 4 additions & 0 deletions base/redditbot/base/__init__.py
@@ -0,0 +1,4 @@
def patch_all():
print 'Monkey patching...'
from gevent import monkey
monkey.patch_all()
210 changes: 210 additions & 0 deletions base/redditbot/base/handlers.py
@@ -0,0 +1,210 @@
import time
import logging

import gevent
import praw
import pylru
import requests
import requests.auth

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

OAUTH_ACCESS_TOKEN_URL = 'https://www.reddit.com/api/v1/access_token'
OAUTH_SCOPES = {'edit', 'identity', 'modconfig', 'modflair', 'modlog', 'modposts',
'mysubreddits', 'privatemessages', 'read', 'submit', 'subscribe', 'vote'}


class MultiBotHandler(object):
def __init__(self, handlers):
self.handlers = handlers

def run(self):
greenlets = []
for handler in self.handlers:
greenlets.append(gevent.spawn(handler.run))
gevent.joinall(greenlets)


class BotHandler(object):
def __init__(self, user_agent, auth, delay, fetch_limit, cache_size=0):
self.user_agent = user_agent
self.auth = auth
self.delay = delay
self.fetch_limit = fetch_limit
self.cache_size = cache_size
self.cache = pylru.lrucache(self.cache_size) if self.cache_size > 0 else None
self.api_request_delay = 1.0 if self.__is_oauth() else 2.0
self.r = praw.Reddit(self.user_agent, cache_timeout=0, api_request_delay=self.api_request_delay)
self.expires = -1
self.__auth()

def _get_content(self):
raise NotImplementedError()

def _check(self, obj):
raise NotImplementedError()

def _do(self, obj):
raise NotImplementedError()

def __is_oauth(self):
return 'client_id' in self.auth and 'secret' in self.auth

def __update_access_credentials(self):
# Fetch access token
client_auth = requests.auth.HTTPBasicAuth(self.auth['client_id'], self.auth['secret'])
response = requests.post(OAUTH_ACCESS_TOKEN_URL, auth=client_auth, data={
'grant_type': 'password',
'username': self.auth['username'],
'password': self.auth['password']
}, headers={
'User-Agent': self.user_agent
})

# Check response
if response.ok:
response = response.json()
else:
logger.error('Could not retrieve access creds: Status {status}'.format(status=response.status_code))
return

# Update
if 'error' in response:
logger.error('Could not retrieve access creds: Json error: {status}'.format(status=response['error']))
else:
self.r.set_access_credentials(scope=OAUTH_SCOPES, access_token=response['access_token'])
self.expires = time.time() + int(response['expires_in']) * 0.9

def __auth(self):
if 'username' not in self.auth or 'password' not in self.auth:
raise Exception("Must provide username and password in auth")

if self.__is_oauth():
self.r.set_oauth_app_info(client_id='a', client_secret='a', redirect_uri='a')
self.__update_access_credentials()
else:
self.r.login(self.auth['username'], self.auth['password'])

def __main(self):
# Check if we need to update access token
if time.time() > self.expires > 0:
self.__update_access_credentials()

# Get the content
content = self._get_content()
if not content:
logger.warn('Bad content object: skipping...')
return

hits = 0
misses = 0

# Process all content
for obj in content:
# Check if it's in the cache
if self.cache is not None:
if obj.id in self.cache:
hits += 1
continue
misses += 1
self.cache[obj.id] = 0

# Process the object, sandbox exceptions
try:
if not self._check(obj):
continue
logger.info('Found valid object: {id} by {name}.'.format(id=obj.id,
name=obj.author.name if obj.author else '[deleted]'))
if not self._do(obj):
logger.info('Failed to process object {id}.'.format(id=obj.id))
except Exception as e:
logger.exception('Exception while processing object {id}'.format(id=obj.id))

if self.cache is not None:
logger.info('Cache hits/misses/total: {hits} / {misses} / {total}'.format(hits=hits, misses=misses,
total=hits + misses))

def run(self):
logger.info('Bot started!')

while True:
start_time = time.time()

try:
self.__main()
except Exception as e:
logger.exception('Exception while processing content generator')

# Sleep at least self.delay per cycle
time_delta = time.time() - start_time
sleep_time = self.delay - time_delta
logger.info('Processing/Sleeping for: {p:.2f}s / {s:.2f}s'.format(p=time_delta, s=max(0, sleep_time)))
logger.info('Finished processing round for {name}'.format(name=self.user_agent))
if sleep_time > 0:
time.sleep(sleep_time)

logger.info('Bot finished! Exiting gracefully.')


class UserCommentsVoteTriggeredBot(BotHandler):
def __init__(self, *args, **kwargs):
self.monitored_user = kwargs.pop('monitored_user')
self.score_threshold_max = kwargs.pop('score_threshold_max', None)
self.score_threshold_min = kwargs.pop('score_threshold_min', None)
if self.score_threshold_max is None and self.score_threshold_min is None:
raise Exception("score_threshold_max or score_threshold_min should be set")

super(UserCommentsVoteTriggeredBot, self).__init__(*args, **kwargs)

def _get_content(self):
return self.r.get_redditor(self.monitored_user).get_comments(limit=self.fetch_limit)

def _check(self, comment):
# Check vote score min
if self.score_threshold_min is not None and comment.score < self.score_threshold_min:
return True

# Check vote score max
if self.score_threshold_max is not None and comment.score > self.score_threshold_max:
return True

return False


class MailTriggeredBot(BotHandler):
def __init__(self, *args, **kwargs):
super(MailTriggeredBot, self).__init__(*args, **kwargs)

def _get_content(self):
return self.r.get_unread(limit=self.fetch_limit)

def is_private_message(self, message):
return not message.was_comment

def is_comment_reply(self, message):
return message.was_comment and message.subject == 'comment reply'

def is_post_reply(self, message):
return message.was_comment and message.subject == 'post reply'

def is_username_mention(self, message):
return message.was_comment and message.subject == 'username mention'


class SubredditCommentTriggeredBot(BotHandler):
def __init__(self, *args, **kwargs):
self.subreddit = kwargs.pop('subreddit')
super(SubredditCommentTriggeredBot, self).__init__(*args, **kwargs)

def _get_content(self):
return self.r.get_comments(self.subreddit, limit=self.fetch_limit)


class SubredditSubmissionTriggeredBot(BotHandler):
def __init__(self, *args, **kwargs):
self.subreddit = kwargs.pop('subreddit')
super(SubredditSubmissionTriggeredBot, self).__init__(*args, **kwargs)

def _get_content(self):
return self.r.get_subreddit(self.subreddit).get_new(limit=self.fetch_limit)
94 changes: 94 additions & 0 deletions base/redditbot/base/utils.py
@@ -0,0 +1,94 @@
import logging

import praw

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


def has_replied(praw_object, username):
"""
Returns True if the specified user has a comment in the top level replies of the given submission/comment/message,
and False otherwise.
For comments, submissions, messages ONLY.
"""
if type(praw_object) == praw.objects.Message:
# TODO: Fix this to actually check properly
# If it's not the first message in the PM thread, we replied previously.
# This is not the best method, and it is a bit flakey,
# but good enough for most cases
if praw_object.first_message is not None:
return True
return False
elif type(praw_object) == praw.objects.Submission:
praw_object.replace_more_comments(limit=None)
replies = praw_object.comments
elif type(praw_object) == praw.objects.Comment:
replies = praw_object.replies
else:
raise Exception("Object must be an instance of praw.objects.Comment/Submission/Message")

if not replies:
return False

# Check each reply if the username matches
username = username.lower()
for reply in replies:
if reply.author and reply.author.name.lower() == username:
return True

return False


def is_comment_owner(praw_comment, username):
"""
Returns True if the specified comment belongs to the user,
otherwise False.
"""
return praw_comment.author and praw_comment.author.name.lower() == username.lower()


def send_reply(praw_object, reply_msg):
"""
Returns the reply object if the message was sent successfully, otherwise None.
For comments, submissions, messages ONLY.
"""
try:
if type(praw_object) == praw.objects.Submission:
reply_obj = praw_object.add_comment(reply_msg)
else:
reply_obj = praw_object.reply(reply_msg)
except Exception as e:
logger.exception('Exception while replying')
return None

logger.info(' => Reply Sent!')
return reply_obj


def edit_reply(praw_comment, reply_msg):
"""
Returns True if the comment was edited successfully, and False otherwise.
For comments ONLY.
"""
try:
praw_comment.edit(reply_msg)
except Exception as e:
logger.exception('Exception while editing')
return False

logger.info(' => Edit was made!')
return True


def has_chain(praw_r, praw_comment, username):
"""
Returns True if the parent was made by username.
Returns False otherwise.
"""
if not hasattr(praw_comment, 'parent_id'):
return False
parent = praw_r.get_info(thing_id=praw_comment.parent_id)
if not parent or type(parent) != praw.objects.Comment:
return False
return is_comment_owner(parent, username)
21 changes: 21 additions & 0 deletions base/setup.py
@@ -0,0 +1,21 @@
import setuptools
import pip

if pip.main(['install', '-r', 'install/requirements.txt']) != 0:
raise Exception('Pip install requirements failed')

setuptools.setup(
name='redditbot.base',
version='1.0.0',
author='Jeremy Simpson',
description='Redditbot framework',
license='MIT',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.7'
],
packages=setuptools.find_packages(),
namespace_packages=['redditbot'],
)

0 comments on commit 94ddc77

Please sign in to comment.