def process_override(reply, config): """ This process is for moderators of ToR to force u/transcribersofreddit to mark a post as complete and award flair when the bot refutes a `done` claim. The comment containing "!override" must be in response to the bot's comment saying that it cannot find the transcript. :param reply: the comment reply object from the moderator. :param config: the global config object. :return: None. """ # don't remove this check, it's not covered like other admin_commands # because it's used in reply to people, not as a PM if not from_moderator(reply, config): reply.reply(_(random.choice(config.no_gifs))) logging.info(f'{reply.author.name} just tried to override. Lolno.') return # okay, so the parent of the reply should be the bot's comment # saying it can't find it. In that case, we need the parent's # parent. That should be the comment with the `done` call in it. reply_parent = config.r.comment(id=clean_id(reply.parent_id)) parents_parent = config.r.comment(id=clean_id(reply_parent.parent_id)) if 'done' in parents_parent.body.lower(): logging.info( f'Starting validation override for post {parents_parent.fullname}, ' f'approved by {reply.author.name}') process_done(parents_parent, config, override=True)
def process_override(reply, config): """ This process is for moderators of ToR to force u/transcribersofreddit to mark a post as complete and award flair when the bot refutes a `done` claim. The comment containing "!override" must be in response to the bot's comment saying that it cannot find the transcript. :param reply: the comment reply object from the moderator. :param config: the global config object. :return: None. """ # first we verify that this comment comes from a moderator and that # we can work on it. if not from_moderator(reply, config): reply.reply(_(random.choice(config.no_gifs))) logging.info('{} just tried to override. Lolno.'.format( reply.author.name)) return # okay, so the parent of the reply should be the bot's comment # saying it can't find it. In that case, we need the parent's # parent. That should be the comment with the `done` call in it. reply_parent = config.r.comment(id=clean_id(reply.parent_id)) parents_parent = config.r.comment(id=clean_id(reply_parent.parent_id)) if 'done' in parents_parent.body.lower(): logging.info('Starting validation override for post {}' ', approved by {}'.format(parents_parent.fullname, reply.author.name)) process_done(parents_parent, config, override=True)
def run(config): time.sleep(config.ocr_delay) new_post = config.redis.lpop('ocr_ids') if new_post is None: logging.debug('No post found. Sleeping.') # nothing new in the queue. Wait and try again. # Yes, I know this is outside a loop. It will be run inside a loop # by tor_core. return # We got something! new_post = new_post.decode('utf-8') logging.info(f'Found a new post, ID {new_post}') url = config.r.submission(id=clean_id(new_post)).url try: result = process_image(url) except OCRError as e: logging.warning('There was an OCR Error: ' + str(e)) return logging.debug(f'result: {result}') if not result: logging.info('Result was none! Skipping!') # we don't want orphan entries config.redis.delete(new_post) return tor_post_id = config.redis.get(new_post).decode('utf-8') logging.info( f'posting transcription attempt for {new_post} on {tor_post_id}') tor_post = config.r.submission(id=clean_id(tor_post_id)) thing_to_reply_to = tor_post.reply( _(base_comment.format(result['process_time_in_ms'] / 1000))) for chunk in chunks(result['text'], 9000): # end goal: if something is over 9000 characters long, we # should post a top level comment, then keep replying to # the comments we make until we run out of chunks. chunk = chunk.replace('\r\n', '\n\n').replace('/u/', '\\/u/').replace( '/r/', '\\/r/').replace(' u/', ' \\/u/').replace(' r/', ' \\/r/').replace('>>', '\>\>') thing_to_reply_to = thing_to_reply_to.reply(_(chunk)) config.redis.delete(new_post)
def update_user_flair(post, config): """ On a successful transcription, this takes the user's current flair, increments the counter by one, and stores it back to the subreddit. If the user is past 50 transcriptions, select the appropriate flair class and write that back too. :param post: The post which holds the author information. :param config: The global config instance. :return: None. """ flair_text = '{} Γ - Beta Tester' post_author = User(str(post.author), config.redis) current_transcription_count = post_author.get('transcriptions', 0) try: # The post object is technically an inbox mention, even though it's # a Comment object. In order to get the flair, we have to take the # ID of our post object and re-request it from Reddit in order to # get the *actual* object, even though they have the same ID. It's # weird. user_flair = config.r.comment( id=clean_id(post.fullname) ).author_flair_text except AttributeError: user_flair = flair_text if user_flair in ['', None]: # HOLD ON. Do we have one saved? Maybe Reddit's screwing up. if current_transcription_count != 0: # we have a user object for them and shouldn't have landed here. user_flair = flair_text.format(current_transcription_count) else: user_flair = flair_text.format('0') if 'Γ' in user_flair: new_count, flair_css = _parse_existing_flair(user_flair) # if there's anything special in their flair string, let's save it additional_flair_text = user_flair[user_flair.index('Γ') + 1:] user_flair = f'{new_count} Γ' # add in that special flair bit back in to keep their flair intact user_flair += additional_flair_text config.tor.flair.set(post.author, text=user_flair, css_class=flair_css) logging.info(f'Setting flair for {post.author}') post_author.update('transcriptions', current_transcription_count + 1) post_author.save() else: # they're bot or a mod and have custom flair. Leave it alone. return
def update_user_flair(post, config): """ On a successful transcription, this takes the user's current flair, increments the counter by one, and stores it back to the subreddit. :param post: The post which holds the author information. :param config: The global config instance. :return: None. """ flair_text = '0 Γ - Beta Tester' try: # The post object is technically an inbox mention, even though it's # a Comment object. In order to get the flair, we have to take the # ID of our post object and re-request it from Reddit in order to # get the *actual* object, even though they have the same ID. It's # weird. user_flair = config.r.comment(id=clean_id(post.fullname)).author_flair_text except AttributeError: user_flair = flair_text if user_flair is None: user_flair = flair_text if 'Γ' in user_flair: # take their current flair and add one to it new_flair_count = int(user_flair[:user_flair.index('Γ') - 1]) # if there's anything special in their flair string, let's save it additional_flair_text = user_flair[user_flair.index('Γ') + 1:] user_flair = '{} Γ'.format(new_flair_count + 1) # add in that special flair bit back in to keep their flair intact user_flair += additional_flair_text config.tor.flair.set(post.author, text=user_flair, css_class='grafeas') logging.info('Setting flair for {}'.format(post.author)) else: # they're bot or a mod and have custom flair. Leave it alone. return
def process_mention(mention, config): """ Handles username mentions and handles the formatting and posting of those calls as workable jobs to ToR. :param mention: the Comment object containing the username mention. :param config: the global config dict :return: None. """ # We have to do this entire parent / parent_permalink thing twice because # the method for calling a permalink changes for each object. Laaaame. if not mention.is_root: # this comment is in reply to something. Let's grab a comment object. parent = config.r.comment(id=clean_id(mention.parent_id)) parent_permalink = parent.permalink() # a comment does not have a title attribute. Let's fake one by giving # it something to work with. parent.title = 'Unknown Content' else: # this is a post. parent = config.r.submission(id=clean_id(mention.link_id)) parent_permalink = parent.permalink # format that sucker so it looks right in the template. parent.title = '"' + parent.title + '"' # Ignore requests made by the OP of content or the OP of the submission if mention.author == parent.author: logging.info('Ignoring mention by OP u/{} on ID {}'.format( mention.author, mention.parent_id)) return logging.info('Posting call for transcription on ID {}'.format( mention.parent_id)) if is_valid(parent.fullname, config): # we're only doing this if we haven't seen this one before. # noinspection PyBroadException try: result = config.tor.submit(title=summoned_submit_title.format( sub=mention.subreddit.display_name, commentorpost=parent.__class__.__name__.lower(), title=parent.title), url=reddit_url.format(parent_permalink)) result.reply( _(rules_comment_unknown_format.format(header=config.header))) result.reply( _( summoned_by_comment.format( reddit_url.format( config.r.comment(clean_id( mention.fullname)).permalink())))) flair_post(result, flair.summoned_unclaimed) logging.debug( 'Posting success message in response to caller, u/{}'.format( mention.author)) mention.reply( _('The transcribers have been summoned! Please be patient ' 'and we\'ll be along as quickly as we can.')) add_complete_post_id(parent.fullname, config) # I need to figure out what errors can happen here except Exception as e: logging.error( '{} - Posting failure message in response to caller, ' 'u/{}'.format(e, mention.author)) mention.reply(_(something_went_wrong))
def process_done(post, config, override=False, alt_text_trigger=False): """ Handles comments where the user says they've completed a post. Also includes a basic decision tree to enable verification of the posts to try and make sure they actually posted a transcription. :param post: the Comment object which contains the string 'done'. :param config: the global config object. :param override: A parameter that can only come from process_override() and skips the validation check. :param alt_text_trigger: a trigger that adds an extra piece of text onto the response. Just something to help ease the number of false-positives. :return: None. """ top_parent = get_parent_post_id(post, config.r) # WAIT! Do we actually own this post? if top_parent.author.name != 'transcribersofreddit': logging.info('Received `done` on post we do not own. Ignoring.') return try: if flair.unclaimed in top_parent.link_flair_text: post.reply(_(done_still_unclaimed)) elif top_parent.link_flair_text == flair.in_progress: if not override and not verified_posted_transcript(post, config): # we need to double-check these things to keep people # from gaming the system logging.info( f'Post {top_parent.fullname} does not appear to have a ' f'post by claimant {post.author}. Hrm... ') # noinspection PyUnresolvedReferences try: post.reply(_(done_cannot_find_transcript)) except praw.exceptions.ClientException as e: # We've run into an issue where someone has commented and # then deleted the comment between when the bot pulls mail # and when it processes comments. This should catch that. # Possibly should look into subclassing praw.Comment.reply # to include some basic error handling of this so that # we can fix it throughout the application. logging.warning(e) return # Control flow: # If we have an override, we end up here to complete. # If there is no override, we go into the validation above. # If the validation fails, post the apology and return. # If the validation succeeds, come down here. if override: logging.info('Moderator override starting!') # noinspection PyUnresolvedReferences try: if alt_text_trigger: post.reply( _('I think you meant `done`, so here we go!\n\n' + done_completed_transcript)) else: post.reply(_(done_completed_transcript)) update_user_flair(post, config) logging.info( f'Post {top_parent.fullname} completed by {post.author}!') # get that information saved for the user author = User(str(post.author), config.redis) author.list_update('posts_completed', clean_id(post.fullname)) author.save() except praw.exceptions.ClientException: # If the butt deleted their comment and we're already this # far into validation, just mark it as done. Clearly they # already passed. logging.info(f'Attempted to mark post {top_parent.fullname} ' f'as done... hit ClientException.') flair_post(top_parent, flair.completed) config.redis.incr('total_completed', amount=1) except praw.exceptions.APIException as e: if e.error_type == 'DELETED_COMMENT': logging.info( f'Comment attempting to mark ID {top_parent.fullname} ' f'as done has been deleted') return raise # Re-raise exception if not