Ejemplo n.º 1
0
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)
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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))
Ejemplo n.º 7
0
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