def remove_users(reddit: Reddit, users, subreddit, c, conn): moderator_relationship = subreddit.moderator() to_delete = list() moderators = list() for moderator in moderator_relationship: moderators.append(moderator) for user in users: try: in_subreddit = False redditor: Redditor = reddit.redditor(user) today = datetime.date.today() join_date = get_user_join_date(user, c, conn) delta_time = today - join_date if delta_time.days <= 7: continue in_subreddit = iterate_over_days(redditor.comments.new( limit=1000), subreddit, config.removal_time) if not in_subreddit: in_subreddit = iterate_over_days(redditor.submissions.new( limit=1000), subreddit, config.removal_time) if not in_subreddit: to_delete.append(user) continue except Exception: continue for user in to_delete: redditor = reddit.redditor(user) subreddit.contributor.remove(redditor) c.execute('DELETE FROM Users WHERE Username=?', (user,)) conn.commit() print(user) print('Amount of users deleted: ', len(to_delete)) print('') return to_delete
class Reddit: def __init__(self, client_id=None, client_secret=None, user_agent=None): self.client_id = client_id or os.getenv('REDDIT_CLIENT_ID') self.client_secret = client_secret or os.getenv('REDDIT_CLIENT_SECRET') self.user_agent = user_agent or os.getenv('REDDIT_USER_AGENT') self.api = PrawReddit( client_id=self.client_id, client_secret=self.client_secret, user_agent=self.user_agent, read_only=True, ) def get_comments_submissions(self, username, num=5): """Return max `num` of comments and submissions by `username`.""" coms = [ dict( title=comment.link_title, text=comment.body_html, subreddit=comment.subreddit_name_prefixed, url=comment.link_url, created=datetime.fromtimestamp(comment.created_utc, pytz.utc), ) for comment in self.api.redditor(username).comments.new(limit=num) ] subs = [ dict( title=submission.title, text=submission.selftext_html, subreddit=submission.subreddit_name_prefixed, url=submission.url, created=datetime.fromtimestamp(submission.created_utc, pytz.utc), ) for submission in self.api.redditor(username).submissions.new( limit=num) ] return coms + subs if len(coms + subs) < num else (coms + subs)[:num] def profile_image_url(self, username): """Return URL of user's avatar image.""" try: return self.api.redditor(username).icon_img except NotFound: logger.exception('Failed to fetch Reddit profile image of %s', username) return None @staticmethod def profile_url(username): """Return URL of user's profile.""" return 'https://www.reddit.com/user/%s' % username
def run_user_search(r: Reddit, comments_replied_to): """ Runs a stream of new comments from a designated Redditor. Replies to new comments with a message. todo link this with some api to make funny comment responses. Update time to be based off time at start of program. :param r: an instance of Reddit :param comments_replied_to: the archive of comment IDs for comments this bot has replied to already """ # Define a search user, create a list to track all comments search_user = config.HiddenUser # Track comments replied to comments_replied_to = [] # Track time of newest submission and submission in stream new_message_time_utc = 0 current_comment_time_utc = 0 # Finds the most recently posted comment newest_comment = r.redditor(search_user).comments.new(limit=1).next() new_message_time_utc = newest_comment.created_utc print(f'The most recent comment was {newest_comment.body!r}') # Streams new comments in for comment in r.redditor(search_user).stream.comments(): current_comment_time_utc = comment.created_utc # If new comment is found, reply and update if (current_comment_time_utc > new_message_time_utc): print(f'I found a new comment {comment.body!r}') comment.reply(commands.HELSER) comments_replied_to.append(comment.id) # Opens comment tracking archive and trims it to maximum 25 # comment IDs with open("comments_replied_to.txt", "r") as fread: comment_id_archive = fread.read().splitlines(True) comment_id_archive = comment_id_archive[-24:] # Adds the newest comment we've found and replied to as the 25 # comment ID, at the end of the # list with open("comments_replied_to.txt", "w") as fout: for comment_id in comment_id_archive: fout.write(comment_id) fout.write(comment.id) # Track time of most recent message. new_message_time_utc = current_comment_time_utc
def _set_redditor_karma(api: praw.Reddit, name): red = api.redditor(name) return """ MATCH (n {id: "%s"}) WITH n SET n.comment_karma = %s, n.link_karma = %s """ % (red.id, red.comment_karma, red.link_karma)
def respond(config, debug=False): setup_logging(debug) logger = logging.getLogger('Responder') logger.info('Initialized') reddit = Reddit(config['Bot']) with open(config['Track file']) as f: last_response = float(f.read()) logger.debug('Last comment read in') comments = list( filter( lambda c: c.subreddit.display_name.lower() in config['Subreddits']. lower(), reddit.redditor(config['Target']).comments.new(limit=50))) logger.debug('Fetched latest comments. Count: %i', len(comments)) for comment in comments: if last_response < comment.created_utc: logger.debug('Target comment found') response = comment.reply(config['Message']) if response: logger.info( 'Responded to comment. Response URL: https://reddit.com%s', response.permalink) with open(config['Track file'], 'w') as f: f.write(str(comment.created_utc)) break else: logger.warning('Unable to respond to comment: %s', comment.link_url) logger.debug('Exiting')
def migrate_friends(origin_account: praw.Reddit, destination_account: praw.Reddit, friends: list, verbose: bool = True): """ Migrates a friend list from one reddit account to another """ if utils.is_null_or_empty(friends): print("Friends list is empty or was not found.") return for friend in friends: redditor_name = friend.name try: # Add to destination account destination_account.redditor(redditor_name).friend() # Remove from origin account origin_account.redditor(redditor_name).unfriend() except Exception as ex: log.error(ex, f"An error occurred while migrating the redditor '{redditor_name}'.")
def send_message_to_staff( reddit: Reddit, winners_list_path: str, staff_recipients: List[str], version_string: str, gold_coin_reward: int, ): """ Sends the winners list results to a list of recipients via Private Message (PM) This function must be called only after the winners_list_path file exists! Attributes: reddit: the PRAW Reddit instance winners_list_path: the file path to read from for the winners list + potential winners list staff_recipients: a list of staff member recipients for the PM version_string: the version of the patch notes gold_coin_reward: the number of Gold Coins intended for the reward """ with open(winners_list_path, "r") as winners_list_file: winners_list_text = ( f"The following Reddit users have won {str(gold_coin_reward)} Gold Coins from the Reddit Patch Notes game:\n\n" + winners_list_file.read()) subject_line = ( f"{version_string} - Winners for the HoN Patch Notes Guessing Game" ) for recipient in staff_recipients: try: reddit.redditor(recipient).message(subject=subject_line, message=winners_list_text) except RedditAPIException as redditError: tprint(f"RedditAPIException encountered: {redditError}") tprint( f"{recipient} was not sent a message, continuing to next recipient" ) continue except Exception as error: tprint(f"General Exception encountered: {error}") tprint( f"{recipient} was not sent a message, continuing to next recipient" ) continue
def decay(r: praw.Reddit, user: str, db: Database) -> None: """ Trigger point decay on a user """ for submission in r.redditor(user).new(limit=1): if (time.time() - submission.created_utc < 259200): return usero = db.lookup(user) usero.score *= .96 journal("User " + user + "'s score decayed to " + str(user.score)) db.add(usero)
def main(): config = configparser.ConfigParser() config.read(f'{HOME_DIR}/.config/praw.ini') bots = config['DEFAULT']['bots'].split(',') for bot in bots: reddit = Reddit(bot, config_interpolation='basic') bot_name = reddit.config.custom['bot_name'] redditor = reddit.redditor(bot_name) get_bot_stat(redditor).to_csv( f'{CURRENT_DIR}/{bot_name}.tsv', sep='\t', index=False, ) print(f'{bot} done')
def is_sub_mod_praw(sub_name: Text, useranme: Text, reddit: Reddit) -> bool: """ Check if a given username is a moderator on a given sub :rtype: bool :param subreddit: Praw SubReddit obj :param user: username :return: bool """ user = reddit.redditor(useranme) if not user: log.error('Failed to locate redditor %s', useranme) return False for sub in user.moderated(): if sub.display_name.lower() == sub_name.lower(): return True return False
def test_get_user_submissions(test_user: str, limit: int, downloader_mock: MagicMock, reddit_instance: praw.Reddit): downloader_mock.args.limit = limit downloader_mock._determine_sort_function.return_value = praw.models.Subreddit.hot downloader_mock.sort_filter = RedditTypes.SortType.HOT downloader_mock.args.submitted = True downloader_mock.args.user = test_user downloader_mock.authenticated = False downloader_mock.reddit_instance = reddit_instance downloader_mock._create_filtered_listing_generator.return_value = \ RedditDownloader._create_filtered_listing_generator( downloader_mock, reddit_instance.redditor(test_user).submissions, ) results = RedditDownloader._get_user_data(downloader_mock) results = assert_all_results_are_submissions(limit, results) assert all([res.author.name == test_user for res in results])
def get_fruit_vectors(fruit_vector_client: praw.Reddit, recent: bool, start_limit=0, end_limit=-1): posts = fruit_vector_client.redditor("ErmineDev").submissions.new(limit=300) fruit_vectors = [] for current_post in posts: if current_post.subreddit_id in whitelisted_subreddit_ids: # The ID of the first non-fruit vector post, stop here if this is also the current post ID if current_post.id != "eos9jv": fruit_vectors.append(current_post) else: break if start_limit > len(fruit_vectors): start_limit = 0 if end_limit < start_limit: end_limit = len(fruit_vectors) if recent: return fruit_vectors[start_limit:end_limit][::-1] elif not recent: return fruit_vectors[::-1][start_limit:end_limit]
class RedditInstance(): ''' This class will take care of the Reddit instance especially with regards to posting. ''' def __init__(self, site_name='', user_agent=''): self.site_name = site_name self.user_agent = user_agent self.Reddit = Reddit(site_name=self.site_name, user_agent=self.user_agent) self.post_title = '' self.post_body = [] def append_body(self, message, end='\n'): if isinstance(message, str): self.post_body.append(message + end) elif isinstance(message, list): self.post_body += message def extend_body_last(self, message, end='\n'): self.post_body[-1] += message + end @property def body(self): return ''.join(self.post_body) def post(self, subreddit): self.Reddit.subreddit(subreddit).submit(self.post_title, self.body) @property def latest_post(self): return self.Reddit.redditor(self.site_name).submissions.new(limit=1) def comment_on_post(self, comment): self.latest_post.reply(comment)
def save_user( db, reddit: praw.Reddit, username: str, post_reload_sec: int, comment_reload_sec: int, ) -> None: user = reddit.redditor(username) latest_post_utc = latest_from_user_utc(db=db, table_name="posts", username=username) get_since = latest_post_utc and (latest_post_utc - post_reload_sec) LOGGER.info(f"Getting posts by {username} since timestamp {get_since}") _takewhile = partial(created_since, target_sec_utc=get_since) db["posts"].upsert_all( (saveable(s) for s in takewhile(_takewhile, user.submissions.new(limit=LIMIT))), pk="id", alter=True, ) latest_comment_utc = latest_from_user_utc(db=db, table_name="comments", username=username) get_since = latest_post_utc and (latest_post_utc - comment_reload_sec) LOGGER.info(f"Getting comments by {username} since timestamp {get_since}") _takewhile = partial(created_since, target_sec_utc=get_since) db["comments"].upsert_all( (saveable(s) for s in takewhile(_takewhile, user.comments.new(limit=LIMIT))), pk="id", alter=True, )
class reddit_bot(configuration_manager,Reddit): _COMMENT_MAX_LEN = 10000 def __init__(self,configuration_json_file, configuation_json_item): self.bot = None self.bot_call = None self.bot_subreddit = None self.bot_database = None self.bot_name = None self.bot_objects = [] self.configuation_json_item = configuation_json_item self.initialize_bot(configuration_json_file) self.subscribed_subreddits = self.get_subscribed_subreddits() def initialize_bot(self,PRAW_bot_json_file): PRAW_json_item = ','.join([self._firstItem,self.configuation_json_item]) connection_string_list = self.config_arguments(PRAW_bot_json_file,PRAW_json_item) self.bot = Reddit( user_agent=connection_string_list['user_agent'], client_id=connection_string_list['client_id'], client_secret=connection_string_list['client_secret'], username=connection_string_list['username'], password=connection_string_list['password'] ) self.bot_name=connection_string_list['username'] self.bot_call=connection_string_list['bot_call'] self.bot_subreddit=self.bot.subreddit(connection_string_list['bot_subreddit']) self.bot_database=connection_string_list['source_database'] def get_subscribed_subreddits(self): subreddit_list = [] for subreddit in list(self.bot.user.subreddits(limit=None)): subreddit_list.append(subreddit.display_name) return '+'.join(subreddit_list) def get_reddit_obj_type(self,object_id): try: return str(self.bot.redditor(object_id[0:3])) except: print('didn\'t work') def get_reddit_obj(self,object_id): try: return self.bot.redditor(object_id) except: print('didn\'t work') def reddit_object_redditor_reply(self,object_id,source_redditor=None,return_comment_id=False): if source_redditor is None: source_redditor = self.bot_name if self.get_reddit_obj_type(object_id)==REDDIT_COMMENT: object_id=object_id.replace(REDDIT_COMMENT,'') calling_object = self.bot.comment(object_id) calling_object.comment_sort = 'new' elif self.get_reddit_obj_type(object_id)==REDDIT_LINK: object_id=object_id.replace(REDDIT_LINK,'') calling_object = self.bot.submission(object_id) calling_object.comment_sort = 'new' calling_object.reply_sort = 'new' try: replies = calling_object.comments for r in replies: if r.author == source_redditor: if return_comment_id: return r else: return True return False except: return False
class RedditBot(): def __init__(self, praw_bot_name: str, is_silent: bool = True, test_submission: bool = True, manual: bool = False) -> None: self.reddit = Reddit(praw_bot_name, config_interpolation='basic') self.model = Model(self.reddit.config.custom['model_name']) self.is_silent = is_silent self.test_submission = test_submission self.manual = manual logger.debug(f'model {self.reddit.config.custom["model_name"]}') def _get_subreddits(self) -> List[Subreddit]: subreddits = [] for subreddit_name in self.reddit.config.custom['subreddits'].split( ','): subreddit = self.reddit.subreddit(subreddit_name) subreddits.append(subreddit) return subreddits def _get_replied_submissions(self) -> Set[str]: redditor = self.reddit.redditor(self.reddit.config.custom['bot_name']) summissions_id = set() for comment in redditor.comments.new(limit=None): summissions_id.add(comment.submission.id) return summissions_id def _get_submissions(self, subreddits: List[Subreddit], check_hot_count: int = 150, max_submission_count: int = 5) -> List[Submission]: replied_submissions_id = self._get_replied_submissions() submissions = [] for subreddit in subreddits: for submission in subreddit.hot(limit=check_hot_count): if submission.id in replied_submissions_id: continue if contains_stop_words(submission.title): continue submissions.append(submission) if len(submissions) == 0: logger.debug(f'no submissions found') submissions = sorted(submissions, key=lambda x: x.created_utc, reverse=True) submissions = submissions[:max_submission_count * 4] submissions = random.sample( submissions, min(len(submissions), max_submission_count)) logger.debug(f'found submissions: {submissions}') return submissions def _prepare_reply(self, title: str, text: str, num_return_sequences: int = 1) -> [str]: replies = self.model.generate_text(title, text, num_return_sequences) processed_replies = [process_output(reply) for reply in replies] return processed_replies, replies def _make_reply(self, submission: Submission, reply_text: str = None) -> None: url = submission.url title = submission.title selftext = submission.selftext if reply_text is None: reply_text, generated_text = self._prepare_reply(title, selftext) reply_text, generated_text = reply_text[0], generated_text[0] if not reply_text or reply_text == ' ': logger.debug( f'FAIL: generated reply to submission={title} was very bad ({generated_text})' ) return if self.test_submission: submission = self.reddit.submission( self.reddit.config.custom['test_submission']) if not self.is_silent: submission.reply( f'{url}\n\n{title}\n\n{selftext}\n\n\nREPLY:\n\n' + reply_text) else: if not self.is_silent: submission.reply(reply_text) logger.debug(f'made reply: submission={title} reply={reply_text}') def _choose_reply_manual(self, submission: Submission, reply_texts: str) -> str: url = submission.url title = submission.title selftext = submission.selftext print('SUBMISSION DATA:') print(f'{url}\n{title}\n{selftext}') print('Possible options:') for i, reply_text in enumerate(reply_texts): print(f'{i}) {reply_text}') print('-----') reply_index = input('Your choice:') try: reply_index = int(reply_index) if reply_index < 0 or reply_index >= len(reply_texts): return None print(f'selected option: {reply_index}') return reply_texts[reply_index] except ValueError: return None def _make_replies(self, submissions: List[Submission]) -> None: if not self.manual: for submission in submissions: self._make_reply(submission) time.sleep(SLEEP_BETWEEN_REPLIES) else: reply_texts_list = [] for submission in submissions: reply_text, _ = self._prepare_reply(submission.title, submission.selftext, 10) reply_texts_list.append(reply_text) replies = [] for submission, reply_texts in zip(submissions, reply_texts_list): replies.append( self._choose_reply_manual(submission, reply_texts)) for submission, reply_text in zip(submissions, replies): if reply_text is None: continue self._make_reply(submission, reply_text) time.sleep(SLEEP_BETWEEN_REPLIES) def run(self) -> None: subreddits = self._get_subreddits() if self.manual: submissions = self._get_submissions(subreddits, max_submission_count=10) else: submissions = self._get_submissions(subreddits) self._make_replies(submissions)
def get_comments(reddit: praw.Reddit, user_name: str): user = reddit.redditor(user_name) comments = user.comments.new() yield from comments
def processinbox(r: praw.Reddit, accountsdb) : for message in r.inbox.unread() : r.inbox.mark_read([message]) body = message.body.split() body[0] = body[0].lower() user = '' if "!bal" in body[0] : if len(body) == 2 and (not config.privatebalances or message.author.name in config.mods): user = body[1] else : user = message.author.name balance = database.balance(user, accountsdb) message.reply("User " + user + " has " + str(balance) + " " + config.currencyname + config.signature) log(message.author.name + " queried " + user +"'s balance, which is " + str(balance)) continue if "!newa" in body[0] or "!createa" in body[0]: if database.balance(message.author.name) == None : database.createaccount(message.author.name, accountsdb, config.startingbalance) message.reply("Account created. Your starting balance is " + str(config.startingbalance) + " " + config.currencyname + config.signature) log(message.author.name + " created a new account") else : message.reply("You already have an account. Your balance is " + str(database.balance(message.author.name)) + " " + config.currencyname + config.signature) continue if ("!delete" in body[0] or "!close" in body[0]) and len(body) == 1 : if database.balance(message.author.name) == None : message.reply("You don't have an account." + config.signature) else : database.deleteaccount(message.author.name, accountsdb) message.reply("Your account has been deleted." + config.signature) log(message.author.name + " deleted their account") continue if "!trans" in body[0] or "!send" in body[0]: if len(body) < 3 : message.reply("Error: command `!transfer` takes 2 arguments, not " + str(len(body) - 1) + config.signature) continue if database.balance(message.author.name, accountsdb) == None : message.reply("You currently don't have an account. Run !newacc to create an account." + config.signature) continue if database.balance(body[1], accountsdb) == None : message.reply("The target user, " + body[1] + " does not have an account." + config.signature) continue try : amt = int(body[2]) if amt <= 0 : message.reply("Error: amount cannot be negative or zero" + config.signature) continue if amt > database.balance(message.author.name, accountsdb) : message.reply("Error: amount is greater than your available balance, which is " + str(database.balance(message.author.name, accountsdb))) continue database.change(message.author.name, amt * -1, accountsdb) database.change(body[1], amt, accountsdb) message.reply("Transfer successful! Your available balance is now " + str(database.balance(message.author.name, accountsdb)) + " " + config.currencyname + config.signature) r.redditor(body[1]).message("You received a transfer", "You were sent " + str(amt) + " " + config.currencyname + " from u/" + message.author.name + config.signature) log(message.author.name + " transferred " + str(amt) + " to " + body[1] + "; new balance = " + str(database.balance(message.author.name, accountsdb))) continue except ValueError : message.reply("Error: amount must be an integer" + config.signature) continue mc = modcommand(message, accountsdb) if "!" in body[0] and not mc: message.reply("Command \"" + body[0] + "\" not found." + config.signature) if config.markread : continue if not mc : r.inbox.mark_unread([message]) time.sleep(config.sleeptime)
if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--db_path", type=str, required=True, help="Path to the database file") parser.add_argument( "--n_hot", type=int, required=True, help="Number of hot submissions and comments to fetch for each user" ) parser.add_argument("--reddit_client_id", type=str, required=True, help="Client ID to use with Reddit's API") parser.add_argument( "--reddit_client_secret", type=str, required=True, help="Client secret to use with Reddit's API" ) args = parser.parse_args() logger.info("Create connection to '{}'".format(args.db_path)) db_connection = create_connection(db_path=args.db_path) logger.info("Connect to Reddit API") reddit = Reddit( user_agent="mbti-type-from-text:v0.0.1 (by /u/gcoter)", client_id=args.reddit_client_id, client_secret=args.reddit_client_secret, ) logger.info("Start iterating over users") all_user_names = get_all_user_names(db_connection=db_connection) for n, user_name in enumerate(all_user_names): logger.info("Handle user '{}' ({}/{})".format(user_name, n + 1, len(all_user_names))) user = reddit.redditor(name=user_name) insert_or_update_user_submissions(user=user, n_hot=args.n_hot, db_connection=db_connection) insert_or_update_user_comments(user=user, n_hot=args.n_hot, db_connection=db_connection)
class SearchEngine: __rd_socket = None __ps_api = None def __init__(self, user_agent=False, use_praw=False): rd_credential = (RedditCredential.objects.get(user_agent=user_agent) if user_agent else RedditCredential.objects.first()) self.__rd_socket = Reddit(client_id=rd_credential.client_id, client_secret=rd_credential.client_secret, user_agent=rd_credential.user_agent) self.__ps_api = PushshiftAPI(self.__rd_socket if use_praw else None) def new_credential(client_id, client_secret, user_agent, domain): rd_credential = RedditCredential( client_id=client_id, client_secret=client_secret, user_agent=user_agent, domain=domain, ) rd_credential.save() def most_commented_submissions(self, subreddit=None, before=None, after=None, limit=10): return [ { 'id': submission.id, 'title': submission.title, 'body': submission.selftext, 'url': submission.url, 'subreddit': submission.subreddit, 'author': submission.author, 'num_comments': submission.num_comments, 'created_at': datetime.utcfromtimestamp( submission.created_utc), 'retrieved_on': datetime.utcfromtimestamp( submission.retrieved_on) } for submission in self.__ps_api.search_submissions( before=datetime.strptime(before, "%d-%m-%Y" ) if before else None, after=datetime.strptime(after, "%d-%m-%Y") if after else None, subreddit=subreddit, sort_type='num_comments', limit=limit) ] or [None] def retrive_submission_by_id(self, submission_id): submission = self.__rd_socket.submission(id=submission_id) submission = { 'id': submission.id, 'title': submission.title, 'body': submission.selftext, 'url': submission.url, 'subreddit': submission.subreddit.display_name, 'author': submission.author.name, 'score': submission.score, 'num_comments': submission.num_comments, 'created_at': datetime.utcfromtimestamp(submission.created_utc), 'retrieved_on': datetime.now() } return submission def retrive_submission_comments(self, submission_id, before=None, after=None, method='psaw'): comments = None if method == 'psaw': comments = [{ 'id': comment.id, 'author_id': comment.author, 'body': comment.body, 'created_at': datetime.utcfromtimestamp(comment.created_utc), 'submission_id': comment.link_id.split('_')[1], 'parent': comment.parent_id.split('_')[1], 'retrieved_on': datetime.utcfromtimestamp(comment.retrieved_on) } for comment in self.__ps_api.search_comments( link_id=submission_id, after=datetime.strptime(after, "%d-%m-%Y") if after else None, before=datetime.strptime(before, "%d-%m-%Y" ) if before else None, )] if method == 'praw' or not comments: comments = [{ 'id': comment.id, 'author_id': comment.author.name if comment.author else None, 'body': comment.body, 'score': comment.score, 'created_at': datetime.utcfromtimestamp(comment.created_utc), 'submission_id': comment._submission.id, 'parent': comment.parent_id.split('_')[1], 'retrieved_on': datetime.now() } for comment in self.__rd_socket.submission( id=submission_id).comments.replace_more(limit=0)] return comments def retrive_redditor_submissions(self, redditor, domain=None, before=None, after=None, method='psaw'): submission = None if method == 'psaw': submissions = [{ 'id': submission.id, 'title': submission.title, 'body': submission.selftext, 'url': submission.url, 'subreddit': submission.subreddit, 'author': submission.author, 'num_comments': submission.num_comments, 'created_at': datetime.utcfromtimestamp(submission.created_utc), 'retrieved_on': datetime.utcfromtimestamp(submission.retrieved_on) } for submission in self.__ps_api.search_submissions( author=redditor, domain=domain, before=datetime.strptime(before, "%d-%m-%Y" ) if before else None, after=datetime.strptime(after, "%d-%m-%Y") if after else None)] if method == 'praw' or not submissions: submissions = [ { 'id': submission.id, 'title': submission.title, 'body': submission.selftext, 'url': submission.url, 'subreddit': submission.subreddit.display_name, 'author': submission.author.name, 'score': submission.score, 'num_comments': submission.num_comments, 'created_at': datetime.utcfromtimestamp(submission.created_utc), 'retrieved_on': datetime.now() } for submission in self.__rd_socket.redditor( redditor).submissions.new() if not domain or submission.subreddit.display_name in domain ] return submissions def redditor_info(self, redditor): redditor = self.__rd_socket.redditor(redditor) return { 'name': getattr(redditor, 'name'), 'submissions_karma': getattr(redditor, 'link_karma'), 'comments_karma': getattr(redditor, 'comment_karma'), 'created_at': datetime.utcfromtimestamp(getattr(redditor, 'created_utc')) } def subreddit_info(self, subreddit): subreddit = self.__rd_socket.subreddit(subreddit) return { 'name': subreddit.display_name, 'description': subreddit.description[:5000], 'short_description': subreddit.public_description, 'num_subscribers': subreddit.subscribers, 'created_at': datetime.utcfromtimestamp(subreddit.created_utc), 'last_update': datetime.now() } def update_comments_score(self, comments_id): num_comments = len(comments_id) for i, comment_id in enumerate(comments_id): try: comment = Comment.objects.get(id=comment_id) score = self.__rd_socket.comment(id=comment_id).score comment.score = int(score) comment.save() except Exception as ex: print(ex) pass if i % 500 == 0: print(f'{i+1}/{num_comments}') print(f'{i+1}/{num_comments}') def update_submissions_score(self, submissions_id): num_submissions = len(submissions_id) for i, submission_id in enumerate(submissions_id): submission = Submission.objects.get(id=submission_id) score = self.__rd_socket.submission(id=submission_id).score submission.score = score submission.save() if i % 500 == 0: print(f'{i+1}/{num_submissions}') print(f'{i+1}/{num_submissions}') def update_submissions_comments(self, submissions_ids=None): comments_ids = {comment.id for comment in Comment.objects.all()} authors_names = {author.name for author in RedditUser.objects.all()} submissions = Submission.objects.filter( id__in=submissions_ids ) if submissions_ids else Submission.objects.all() for i, submission in enumerate(submissions): print(f'{i}/{len(submissions)} - {submission.id}') comments = [ comment for comment in self.retrive_submission_comments(submission.id) if comment['id'] not in comments_ids ] authors = [ comment['author_id'] for comment in comments if comment['author_id'] not in authors_names ] authors_bulk = [] for author in authors: try: authors_bulk.append( RedditUser(**self.redditor_info(author))) except Exception: authors_bulk.append(RedditUser(name=author)) RedditUser.objects.bulk_create(authors_bulk, ignore_conflicts=True) comments = [Comment(**comment) for comment in comments] Comment.objects.bulk_create(comments, ignore_conflicts=True)
class Reddit(metaclass=Singleton): def __init__(self): self.api = RedditApi( # username=REDDIT_USERNAME, # password=REDDIT_PASSWORD, client_id=REDDIT_CLIENTID, client_secret=REDDIT_CLIENTSECRET, user_agent=REDDIT_USERAGENT, refresh_token=REDDIT_REFRESH_TOKEN, redirect_uri=REDDIT_REDIRECT_URL, ) if REDDIT_REFRESH_TOKEN: print(f'Logged in with: {self.api.user.me()}') return if not REDDIT_REFRESH_CODE: url = self.api.auth.url(['*'], 2205, 'permanent') print( f'Missing refresh token, go with {REDDIT_USERNAME} to the url:\n{url}' ) else: refresh_token = self.api.auth.authorize(REDDIT_REFRESH_CODE) print(f'Code validated. Refresh token: {refresh_token}') sleep(15) exit() def inbox(self): return [ message for message in self.api.inbox.unread() if not message.was_comment ][:5] def send(self, username, title, content): try: LOG.info(f'Sending to {username}: {title}') self.api.redditor(username).message(title, content) sleep(5) except: LOG.error(f'Error sending to {username}: {title}') def reply(self, message, content): try: LOG.info(f'Replying to {message.author.name}: {message.subject}') message.reply(content) message.mark_read() sleep(5) except Exception as e: LOG.error( f'Error replying to {message.author.name}: {message.subject} > {e}' ) def usable(self, sub, country=None): if not sub: return False now = datetime.utcnow() try: submission = self.api.submission(id=sub.submission_id) if not submission.author or submission.author.name != REDDIT_USERNAME: LOG.info(f'Submission was deleted: {sub}') return False else: LOG.info(f'Submission wasnt deleted: {sub}') if submission.stickied and (now - sub.created_at).days < 170: LOG.info(f'Submission is stickied and not expired: {sub}') return True else: LOG.info(f'Submission isnt stickied or expired: {sub}') if now > sub.expires_at: LOG.info(f'Submission expired: {sub}') return False else: LOG.info(f'Submission is active: {sub}') if country: LOG.info( f'Submission is for a country, it will be reused: {sub}' ) return True if now.today().weekday() not in [0, 3]: # now is not monday/thursday LOG.info( f'Submission will be reused (not monday/thursday yet): {sub}' ) return True else: LOG.info( f'Submission might be replaced (monday/thursday): {sub}') if now.hour < 17: LOG.info( f'Submission will be reused (not 17:00 UTC yet): {sub}') return True else: LOG.info(f'Submission might be replaced (17:00 UTC): {sub}') if sub.created_at.day != now.day: LOG.info( f'Submission will be replaced (it\'s monday/thursday, my dudes): {sub}' ) return False else: LOG.info( f'Submission wont be replaced (already created one today): {sub}' ) except Exception as e: LOG.error( f'Submission shows error {str(e)}, will be replaced: {sub}') return False LOG.info(f'Submission will be reused: {sub}') return True def create(self, subreddit, title, content): submission = self.api\ .subreddit(subreddit)\ .submit(title, selftext=content) # submission.disable_inbox_replies() self.update_flair(submission, subreddit) return submission.id def edit(self, sub, content): submission = self.api.submission(id=sub.submission_id) if not submission: return submission.edit(content) LOG.info(f'Submission updated: {sub}') def nsfw(self, sub): if not sub: return submission = self.api.submission(id=sub.submission_id) if not submission: return try: submission.mod.nsfw() LOG.info(f'Submission marked as NSFW: {sub}') except: LOG.error(f'Submission can\'t be marked as NSFW: {sub}') def update_flair(self, submission, subreddit): try: flair_text = FLAIRS.get(subreddit) if not flair_text: return else: flair_text = flair_text.lower() for flair in submission.flair.choices(): if flair['flair_text'].lower() == flair_text: submission.flair.select(flair['flair_template_id']) LOG.info(f'Flair "{flair_text}" applied') break else: LOG.warning(f'Flair "{flair_text}" not found for {subreddit}') except Exception as e: LOG.error(f'Flair for "{subreddit}" can\'t applied: {str(e)}') def submit(self, system, subreddit, title, content, country=None): subreddit = subreddit.lower() reddit_db = RedditDatabase() key = f'{system}/{country if country else subreddit}' sub = reddit_db.load(key) now = datetime.utcnow() if not self.usable(sub, country): self.nsfw(sub) sub = Submission(_id=key, submission_id=self.create(subreddit, title, content), subreddit=subreddit, system=system, title=title, days_to_expire=60 if country else 165) LOG.info(f'Submission created: {sub}') else: self.edit(sub, content) sub.updated_at = now sub.length = len(content) reddit_db.save(sub) sleep(5) return sub
def send_message_to_winners( # noqa: C901 reddit: Reddit, winners_list: List[str], reward_codes_list: List[str], version_string: str, gold_coin_reward: int, ): """ Sends the winners list results to a list of recipients via Private Message (PM). This function uses recursion to send messages to failed recipients. This function also frequently encounters Reddit API Exceptions due to rate limits. To sleep for the appropriate duration without wasting time, the rate limit error is parsed: Test strings for regex capture: RATELIMIT: "Looks like you've been doing that a lot. Take a break for 4 minutes before trying again." on field 'ratelimit' RATELIMIT: "Looks like you've been doing that a lot. Take a break for 47 seconds before trying again." on field 'ratelimit' RATELIMIT: "Looks like you've been doing that a lot. Take a break for 4 minutes 47 seconds before trying again." on field 'ratelimit' RATELIMIT: "Looks like you've been doing that a lot. Take a break for 1 minute before trying again." on field 'ratelimit' Attributes: reddit: the PRAW Reddit instance winners_list: a list of winning recipients for the PM reward_codes_list: a list of reward codes for each winner version_string: the version of the patch notes gold_coin_reward: the number of Gold Coins intended for the reward """ subject_line = f"Winner for the {version_string} Patch Notes Guessing Game" failed_recipients_list = [] for recipient in winners_list: reward_code = ( "N/A - all possible reward codes have been used up.\n\n" f"Please contact {STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} for a code to be issued manually." ) if len(reward_codes_list) > 0: reward_code = reward_codes_list[0] # TODO: Add this back if reward codes generator works again # message = ( # f"Congratulations {recipient}!\n\n" # f"You have been chosen by the bot as a winner for the {version_string} Patch Notes Guessing Game!\n\n" # f"Your reward code for {str(gold_coin_reward)} Gold Coins is: **{reward_code}**\n\n" # "You can redeem your reward code here: https://www.heroesofnewerth.com/redeem/\n\n" # f"Please contact {STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} if any issues arise.\n\n" # "Thank you for participating in the game! =)" # ) message = ( f"Congratulations {recipient}!\n\n" f"You have been chosen by the bot as a winner for the {version_string} Patch Notes Guessing Game!\n\n" f"Please contact /u/{STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} via the Reddit Messaging system to obtain your code.\n\n" "Please include your In-Game Username in your message.\n\n" "Thank you for participating in the game! =)") try: reddit.redditor(recipient).message(subject=subject_line, message=message) tprint( f"Winner message sent to {recipient}, with code: {reward_code}" ) # Pop reward code from list only if the message was sent successfully if len(reward_codes_list) > 0: reward_codes_list.pop(0) # Reddit API Exception except RedditAPIException as redditException: tprint(f"Full Reddit Exception: {redditException}\n\n") for subException in redditException.items: # Rate limit error handling if subException.error_type == "RATELIMIT": failed_recipients_list.append(recipient) tprint( f"{redditException}\n{recipient} was not sent a message (added to retry list), " "continuing to next recipient") tprint(f"Subexception: {subException}\n\n") # Sleep for the rate limit duration by parsing the minute and seconds count from # the message into named groups regex_capture = re.search( r"\s+((?P<minutes>\d+) minutes?)?\s?((?P<seconds>\d+) seconds)?\s+", subException.message, ) if regex_capture is None: print( "Invalid regex detected. Sleeping for 60 seconds..." ) time.sleep(60) break else: # Use named groups from regex capture and assign them to a dictionary sleep_time_regex_groups = regex_capture.groupdict( default=0) # Add 1 extra second to account for millisecond-precision checking secondsToSleep = ( 60 * int( sleep_time_regex_groups.get( "minutes") # type: ignore ) + int( sleep_time_regex_groups.get( "seconds") # type: ignore ) + 1) # type: ignore print(f"Sleeping for {str(secondsToSleep)} seconds") time.sleep(secondsToSleep) break continue except Exception as error: tprint( f"{error}\n{recipient} was not sent a message (will not retry), continuing to next recipient" ) continue # At the end of the function, recurse this function to re-send messages to failed recipients # Recurse only if failed_recipients_list has content in it # Prevents infinite loops by ensuring that the failed recipients count # gradually progresses towards the end condition. failed_recipients = len(failed_recipients_list) if failed_recipients > 0 and failed_recipients < len(winners_list): send_message_to_winners( reddit, failed_recipients_list, reward_codes_list, version_string, gold_coin_reward, )