Esempio n. 1
0
def process_override(reply, cfg):
    """
    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 cfg: 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, cfg):
        reply.reply(_(random.choice(cfg.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 = cfg.r.comment(id=clean_id(reply.parent_id))
    parents_parent = cfg.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, cfg, override=True
        )
Esempio n. 2
0
def process_wrong_post_location(post):
    transcript_on_tor_post = i18n['responses']['general'][
        'transcript_on_tor_post']
    try:
        post.reply(_(transcript_on_tor_post))
    except praw.exceptions.APIException:
        logging.debug(
            'Something went wrong with asking about a misplaced post; '
            'ignoring.')
Esempio n. 3
0
def process_thanks(post, cfg):
    thumbs_up_gifs = i18n['urls']['thumbs_up_gifs']
    youre_welcome = i18n['responses']['general']['youre_welcome']
    try:
        post.reply(_(youre_welcome.format(random.choice(thumbs_up_gifs))))
    except praw.exceptions.APIException as e:
        if e.error_type == 'DELETED_COMMENT':
            logging.debug('Comment requiring thanks was deleted')
            return
        raise
Esempio n. 4
0
def process_mention(mention):
    """
    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.
    :return: None.
    """
    i18n = translation()
    pm_subject = i18n['responses']['direct_message']['subject']
    pm_body = i18n['responses']['direct_message']['body']

    # message format is subject, then body
    mention.author.message(pm_subject, _(pm_body))
    logging.info(f'Message sent to {mention.author.name}!')
Esempio n. 5
0
def process_debug(user: Redditor, blossom_submission: Dict,
                  cfg) -> Tuple[str, None]:
    # TODO: turn this into a decorator
    # 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 is_moderator(user.name, cfg):
        logging.info(f"{user.name} just tried to override. Lolno.")
        return _(random.choice(cfg.no_gifs)), None

    # format for reddit by putting four spaces at the beginning of each line
    message = "\n".join([
        "    {}".format(i)
        for i in json.dumps(blossom_submission, indent=4).split("\n")
    ])
    return message, None
Esempio n. 6
0
def request_transcription(post: PostSummary, content_type: str,
                          content_format: str, cfg: Config) -> None:
    """Request a transcription by posting the provided post to our subreddit."""
    title = i18n["posts"]["discovered_submit_title"].format(
        sub=str(post["subreddit"]),
        type=content_type.title(),
        title=truncate_title(cleanup_post_title(str(post["title"]))),
    )
    permalink = i18n["urls"]["reddit_url"].format(str(post["permalink"]))
    submission = cfg.tor.submit(title=title, url=permalink)
    intro = i18n["posts"]["rules_comment"].format(
        post_type=content_type,
        formatting=content_format,
        header=cfg.header,
    )
    submission.reply(_(intro))
    flair_post(submission, flair.unclaimed)
    create_blossom_submission(post, submission, cfg)
Esempio n. 7
0
def process_mention(mention: Comment) -> None:
    """
    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.
    :return: None.
    """
    try:
        pm_subject = i18n["responses"]["direct_message"]["subject"]
        pm_body = i18n["responses"]["direct_message"]["body"]

        # message format is subject, then body
        mention.author.message(pm_subject, _(pm_body))
        log.info(f"Message sent to {mention.author.name}!")
    except (ClientException, AttributeError):
        # apparently this crashes with an AttributeError if someone
        # calls the bot and immediately deletes their comment. This
        # should fix that.
        pass
Esempio n. 8
0
def process_override(user: Redditor, blossom_submission: Dict, parent_id: str,
                     cfg):
    """
    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 user: The user requesting the override
    :param blossom_submission: The relevant Submission of Blossom
    :param parent_id: The ID of the parent comment of the override
    :param cfg: the global config object.
    """

    # TODO: turn this into a decorator
    # 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 is_moderator(user.name, cfg):
        logging.info(f"{user.name} just tried to override. Lolno.")
        return _(random.choice(cfg.no_gifs)), None

    # 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 = cfg.r.comment(id=clean_id(parent_id))
    grandparent = cfg.r.comment(id=clean_id(reply_parent.parent_id))
    if grandparent.body.lower() in (CLAIM_PHRASES + DONE_PHRASES):
        logging.info(
            f"Starting validation override for post {grandparent.fullname}, "
            f"approved by {user.name}")
        return process_done(grandparent.author,
                            blossom_submission,
                            grandparent,
                            cfg,
                            override=True)
    return "Cannot process - no target comment found.", None
Esempio n. 9
0
def process_claim(post, cfg, first_time=False):
    """
    Handles comment replies containing the word 'claim' and routes
    based on a basic decision tree.

    :param post: The Comment object containing the claim.
    :param cfg: the global config dict.
    :return: None.
    """
    top_parent = get_parent_post_id(post, cfg.r)

    already_claimed = i18n['responses']['claim']['already_claimed']
    claim_already_complete = i18n['responses']['claim']['already_complete']
    please_accept_coc = i18n['responses']['general']['coc_not_accepted']

    if first_time:
        claim_success = i18n['responses']['claim']['first_claim_success']
    else:
        claim_success = i18n['responses']['claim']['success']

    # WAIT! Do we actually own this post?
    if top_parent.author.name not in __BOT_NAMES__:
        logging.debug('Received `claim` on post we do not own. Ignoring.')
        return

    try:
        if not coc_accepted(post, cfg):
            # do not cache this page. We want to get it every time.
            post.reply(
                _(please_accept_coc.format(get_wiki_page('codeofconduct',
                                                         cfg))))
            return

        # this can be either '' or None depending on how the API is feeling
        # today
        if top_parent.link_flair_text in ['', None]:
            # There exists the very small possibility that the post was
            # malformed and doesn't actually have flair on it. In that case,
            # let's set something so the next part doesn't crash.
            flair_post(top_parent, flair.unclaimed)

        if flair.unclaimed in top_parent.link_flair_text:
            # need to get that "Summoned - Unclaimed" in there too
            post.reply(_(claim_success))

            flair_post(top_parent, flair.in_progress)
            logging.info(
                f'Claim on ID {top_parent.fullname} by {post.author} successful'
            )

        # can't claim something that's already claimed
        elif top_parent.link_flair_text == flair.in_progress:
            post.reply(_(already_claimed))
        elif top_parent.link_flair_text == flair.completed:
            post.reply(_(claim_already_complete))

    except praw.exceptions.APIException as e:
        if e.error_type == 'DELETED_COMMENT':
            logging.info(
                f'Comment attempting to claim ID {top_parent.fullname} has '
                f'been deleted. Back up for grabs! ')
            return
        raise  # Re-raise exception if not
Esempio n. 10
0
def process_unclaim(post, cfg):
    # Sometimes people need to unclaim things. Usually this happens because of
    # an issue with the post itself, like it's been locked or deleted. Either
    # way, we should probably be able to handle it.

    # Process:
    # If the post has been reported, then remove it. No checks, just do it.
    # If the post has not been reported, attempt to load the linked post.
    #   If the linked post is still up, then reset the flair on ToR's side
    #    and reply to the user.
    #   If the linked post has been taken down or deleted, then remove the post
    #    on ToR's side and reply to the user.

    top_parent = post.submission

    unclaim_failure_post_already_completed = i18n['responses']['unclaim'][
        'post_already_completed']
    unclaim_still_unclaimed = i18n['responses']['unclaim']['still_unclaimed']
    unclaim_success = i18n['responses']['unclaim']['success']
    unclaim_success_with_report = i18n['responses']['unclaim'][
        'success_with_report']
    unclaim_success_without_report = i18n['responses']['unclaim'][
        'success_without_report']

    # WAIT! Do we actually own this post?
    if top_parent.author.name not in __BOT_NAMES__:
        logging.info('Received `unclaim` on post we do not own. Ignoring.')
        return

    if flair.unclaimed in top_parent.link_flair_text:
        post.reply(_(unclaim_still_unclaimed))
        return

    for item in top_parent.user_reports:
        if (reports.original_post_deleted_or_locked in item[0]
                or reports.post_violates_rules in item[0]):
            top_parent.mod.remove()
            send_to_modchat(
                'Removed the following reported post in response to an '
                '`unclaim`: {}'.format(top_parent.shortlink),
                cfg,
                channel='removed_posts')
            post.reply(_(unclaim_success_with_report))
            return

    # Okay, so they commented with unclaim, but they didn't report it.
    # Time to check to see if they should have.
    linked_resource = cfg.r.submission(top_parent.id_from_url(top_parent.url))
    if is_removed(linked_resource):
        top_parent.mod.remove()
        send_to_modchat(
            'Received `unclaim` on an unreported post, but it looks like it '
            'was removed on the parent sub. I removed ours here: {}'
            ''.format(top_parent.shortlink),
            cfg,
            channel='removed_posts')
        post.reply(_(unclaim_success_without_report))
        return

    # Finally, if none of the other options apply, we'll reset the flair and
    # continue on as normal.
    if top_parent.link_flair_text == flair.completed:
        post.reply(_(unclaim_failure_post_already_completed))
        return

    if top_parent.link_flair_text == flair.in_progress:
        flair_post(top_parent, flair.unclaimed)
        post.reply(_(unclaim_success))
        return
Esempio n. 11
0
def process_done(post, cfg, 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 cfg: 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, cfg.r)

    done_cannot_find_transcript = i18n['responses']['done'][
        'cannot_find_transcript']
    done_completed_transcript = i18n['responses']['done'][
        'completed_transcript']
    done_still_unclaimed = i18n['responses']['done']['still_unclaimed']

    # WAIT! Do we actually own this post?
    if top_parent.author.name not in __BOT_NAMES__:
        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, cfg):
                # 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'
                          f'{done_completed_transcript}'))
                else:
                    post.reply(_(done_completed_transcript))
                update_user_flair(post, cfg)
                logging.info(
                    f'Post {top_parent.fullname} completed by {post.author}!')
                # get that information saved for the user
                author = User(str(post.author), cfg.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)

            cfg.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
Esempio n. 12
0
def process_post(new_post, cfg):
    """
    After a valid post has been discovered, this handles the formatting
    and posting of those calls as workable jobs to ToR.

    :param new_post: Submission object that needs to be posted.
    :param cfg: the config object.
    :return: None.
    """
    id_already_handled_in_db = i18n['debug']['id_already_handled_in_db']
    discovered_submit_title = i18n['posts']['discovered_submit_title']
    rules_comment = i18n['posts']['rules_comment']
    yt_already_has_transcripts = i18n['posts']['yt_already_has_transcripts']

    if new_post['subreddit'] in cfg.upvote_filter_subs:
        # ignore posts if they don't meet the threshold for karma and the sub
        # is in our list of upvoted filtered ones
        if new_post['ups'] < cfg.upvote_filter_subs[new_post['subreddit']]:
            return

    if not is_valid(new_post['name'], cfg):
        logging.debug(id_already_handled_in_db.format(new_post['name']))
        return

    if new_post['archived']:
        return

    if new_post['author'] is None:
        # we don't want to handle deleted posts, that's just silly
        return

    logging.info(
        f'Posting call for transcription on ID {new_post["name"]} posted by '
        f'{new_post["author"]}'
    )

    if new_post['domain'] in cfg.image_domains:
        content_type = 'image'
        content_format = cfg.image_formatting

    elif new_post['domain'] in cfg.audio_domains:
        content_type = 'audio'
        content_format = cfg.audio_formatting

    elif new_post['domain'] in cfg.video_domains:
        if 'youtu' in new_post['domain']:
            if not valid_youtube_video(new_post['url']):
                add_complete_post_id(new_post['name'], cfg)
                return
            if get_yt_transcript(new_post['url']):
                np = cfg.r.submission(id=new_post['name'])
                np.reply(_(
                    yt_already_has_transcripts
                ))
                add_complete_post_id(new_post['name'], cfg)
                logging.info(
                    f'Found YouTube video, {get_yt_video_id(new_post["url"])},'
                    f' with good transcripts.'
                )
                return
        content_type = 'video'
        content_format = cfg.video_formatting
    else:
        # This means we pulled from a subreddit bypassing the filters.
        content_type = 'Other'
        content_format = cfg.other_formatting

    # Truncate a post title if it exceeds 250 characters, so the added
    # formatting still fits in Reddit's 300 char limit for post titles
    post_title = new_post['title']
    max_title_length = 250
    if len(post_title) > max_title_length:
        post_title = post_title[:max_title_length - 3] + '...'

    # noinspection PyBroadException
    try:
        result = cfg.tor.submit(
            title=discovered_submit_title.format(
                sub=new_post['subreddit'],
                type=content_type.title(),
                title=post_title
            ),
            url=reddit_url.format(new_post['permalink'])
        )
        result.reply(
            _(
                rules_comment.format(
                    post_type=content_type,
                    formatting=content_format,
                    header=cfg.header
                )
            )
        )
        flair_post(result, flair.unclaimed)

        add_complete_post_id(new_post['name'], cfg)
        cfg.redis.incr('total_posted', amount=1)

        if cfg.OCR and content_type == 'image':
            # hook for OCR bot; in order to avoid race conditions, we add the
            # key / value pair that the bot isn't looking for before adding
            # to the set that it's monitoring.
            cfg.redis.set(new_post['name'], result.fullname)
            cfg.redis.rpush('ocr_ids', new_post['name'])

        cfg.redis.incr('total_new', amount=1)

    # The only errors that happen here are on Reddit's side -- pretty much
    # exclusively 503s and 403s that arbitrarily resolve themselves. A missed
    # post or two is not the end of the world.
    except Exception as e:
        logging.error(
            f'{e} - unable to post content.\nID: {new_post["name"]}\n '
            f'Title: {new_post["title"]}\n Subreddit: '
            f'{new_post["subreddit"]}'
        )