From b1373c629c3386bddd7acea71021a789e49b5c0d Mon Sep 17 00:00:00 2001 From: moneromooo Date: Mon, 19 Jan 2015 08:41:49 +0000 Subject: [PATCH] Add a reddit network --- tipbot/config.py.example | 8 ++ tipbot/modules/reddit.py | 256 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 tipbot/modules/reddit.py diff --git a/tipbot/config.py.example b/tipbot/config.py.example index f95dfc4..525142e 100644 --- a/tipbot/config.py.example +++ b/tipbot/config.py.example @@ -52,6 +52,14 @@ network_config = { 'timeout_seconds': 600, 'channels': ['#txtptest000'], }, + 'reddit': { + 'subreddits': ['test'], + 'login': 'testbx', + 'keyword': '/u/testbx', + 'user_agent': tipbot_name, + 'update_period': 90, + 'load_limit': 100, + } } dice_min_multiplier=1.1 diff --git a/tipbot/modules/reddit.py b/tipbot/modules/reddit.py new file mode 100644 index 0000000..19279dd --- /dev/null +++ b/tipbot/modules/reddit.py @@ -0,0 +1,256 @@ +#!/bin/python +# +# Cryptonote tipbot - Reddit +# Copyright 2015 moneromooo +# +# The Cryptonote tipbot is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation; either version 2, or (at your option) +# any later version. +# + +import sys +import string +import time +import threading +import re +import praw +import tipbot.config as config +from tipbot.log import log_error, log_warn, log_info, log_log +from tipbot.utils import * +from tipbot.command_manager import * +from tipbot.network import * + +#import logging +#logging.basicConfig(level=logging.DEBUG) + +class RedditNetwork(Network): + def __init__(self,name): + Network.__init__(self,name) + self.last_update_time=0 + self.logged_in=False + self.last_seen_ids=None + self.thread=None + + def is_identified(self,link): + # all reddit users are identified + return True + + def connect(self): + if self.thread: + return False + try: + cfg=config.network_config[self.name] + self.login=cfg['login'] + password=GetPassword(self.name) + self.subreddits=cfg['subreddits'] + user_agent=cfg['user_agent'] + self.update_period=cfg['update_period'] + self.load_limit=cfg['load_limit'] + self.keyword=cfg['keyword'] + + self.reddit=praw.Reddit(user_agent=user_agent) + self.reddit.login(self.login,password) + self.items_cache=dict() + + self.stop = False + self.thread = threading.Thread(target=self.run) + self.thread.start() + self.logged_in=True + + except Exception,e: + log_error('Failed to login to reddit: %s' % str(e)) + return False + return True + + def disconnect(self): + log_info('Reddit disconnect') + if not self.thread: + return + log_info('Shutting down Reddit thread') + self.stop = True + self.thread.join() + self.thread = None + self.items_cache=None + self.reddit = None + + def send_group(self,group,msg,data=None): + item=data + if not item: + log_error('RedditNetwork: no item found in send_group: cannot send %s' % (msg)) + return + self._schedule_reply(item,None,msg) + + def send_user(self,user,msg,data=None): + item=data + if not item: + # new PM + self._schedule_reply(None,user.nick,msg) + else: + # reply to PM + self._schedule_reply(item,None,msg) + + def is_acceptable_command_prefix(self,s): + s=s.strip() + if s=="": + return True + if s.lower() == self.keyword.lower(): + return True + return False + + def _parse(self,item,is_pm): + author=self.canonicalize(item.author.name) if hasattr(item,'author') else None + if author==self.canonicalize(self.login): + return + + if item.id in self.last_seen_ids: + #log_log('Already seen %s %.1f hours ago by %s: %s (%s), skipping' % (item.id,age/3600,str(author),repr(title),repr(item.body))) + return + + age=time.time()-item.created_utc + ts=long(float(item.created_utc)) + title=item.link_title if hasattr(item,'link_title') else None + + log_log('Parsing new item %s from %.1f hours ago by %s: %s (%s)' % (item.id,age/3600,str(author),repr(title),repr(item.body))) + self.last_seen_ids.add(item.id) + redis_sadd('reddit:last_seen_ids',item.id) + + if item.body.lower().find(self.keyword.lower()) >= 0: + group=None + #if not is_pm and hasattr(item,'subreddit'): + # group=Group(self,item.subreddit.display_name) + group = None + self.items_cache[item.fullname]=item + link=Link(self,User(self,author),group,item) + log_info('Found keyword %s from %s' % (self.keyword,link.identity())) + for line in item.body.split('\n'): + if is_pm: + exidx=line.find('!') + if exidx!=-1 and len(line)>exidx+1 and line[exidx+1] in string.ascii_letters and self.is_acceptable_command_prefix(line[:exidx]): + cmd=line[exidx+1:].split(' ') + cmd[0] = cmd[0].strip(' \t\n\r') + log_info('Found command from %s: %s' % (link.identity(), str(cmd))) + if self.on_command: + self.on_command(link,cmd) + + else: + # reddit special: +x as a reply means tip + if not is_pm and not item.is_root and hasattr(item,'parent_id'): + line=line.replace(self.keyword,'').strip() + if re.match("\+[0-9]*\.[0-9]*",line): + if self.on_command: + try: + parent_item=self.reddit.get_info(thing_id=item.parent_id) + author=parent_item.author.name + synthetic_cmd=['tip',author,line.replace('+','')] + log_log('Running synthetic command: %s' % (str(synthetic_cmd))) + self.on_command(link,synthetic_cmd) + except Exception,e: + log_error('Failed to tip %s\'s parent: %s' % (item.id,str(e))) + + def _schedule_reply(self,item,recipient,text): + log_log('scheduling reply to %s:%s: %s' % (item.id if item else '""',recipient or '""',text)) + if item: + ndata = redis_llen('reddit:replies') + if ndata > 0: + prev_item = redis_lindex('reddit:replies',ndata-1) + if prev_item: + prev_parts=prev_item.split(':',2) + prev_fullname=prev_parts[0] + prev_recipient=prev_parts[1] + prev_text=prev_parts[2] + if prev_fullname==item.fullname: + log_log('Appending to previous item, also for the same fullname') + new_text=prev_text+"\n\n"+text + redis_lset('reddit:replies',ndata-1,(item.fullname if item else "")+":"+(recipient or "")+":"+new_text) + return + + redis_rpush('reddit:replies',(item.fullname if item else "")+":"+(recipient or "")+":"+text) + + def _post_next_reply(self): + data=redis_lindex('reddit:replies',0) + if not data: + return False + parts=data.split(':',2) + fullname=parts[0] + recipient=parts[1] + text=parts[2] + + text = text.replace('\n\n','\n\n   \n\n').replace('\n',' \n') + + try: + if recipient: + # PM + self.reddit.send_message(recipient,"Reply from %s"%self.login,text,raise_captcha_exception=True) + log_info('Posted message to %s: %s' % (recipient,text)) + else: + # subreddit or reply to PM + item = None + log_info('looking for "%s" in %s' % (str(fullname),str(self.items_cache))) + if fullname in self.items_cache: + item=self.items_cache[fullname] + if not item: + item=self.reddit.get_info(thing_id=fullname) + if not item: + log_error('Failed to find item %s to post %s' % (fullname,text)) + redis_lpop('reddit:replies') + return True + reply_item=item.reply(text) + log_info('Posted reply to %s: %s' % (fullname,text)) + if reply_item and hasattr(reply_item,'id'): + redis_sadd('reddit:last_seen_ids',reply_item.id) + + redis_lpop('reddit:replies') + + except praw.errors.RateLimitExceeded,e: + log_info('Rate limited trying to send %s, will retry: %s' % (data,str(e))) + return False + except Exception,e: + log_error('Error sending %s, will retry: %s' % (data,str(e))) + return False + + return True + + def canonicalize(self,name): + return name.lower() + + def update(self): + return True + + def _check(self): + now=time.time() + if now-self.last_update_time < self.update_period: + return True + self.last_update_time=now + + if not self.last_seen_ids: + self.last_seen_ids=redis_smembers('reddit:last_seen_ids') + log_log('loaded last seen ids: %s ' % str(self.last_seen_ids)) + + if self.logged_in: + # get_unread doesn't seem to work + messages=self.reddit.get_inbox() + for message in messages: + if not message.was_comment: + self._parse(message,True) + + sr=self.reddit.get_subreddit("+".join(self.subreddits)) + comments=sr.get_comments(limit=self.load_limit) + for comment in comments: + self._parse(comment,False) + + while self._post_next_reply(): + pass + + log_log('RedditNetwork: update done in %.1f seconds' % float(time.time()-self.last_update_time)) + return True + + def run(self): + while not self.stop: + try: + self._check() + except Exception,e: + log_error('Exception in RedditNetwork:_check: %s' % str(e)) + time.sleep(1) + +RegisterNetwork("reddit",RedditNetwork)