示例#1
0
def process_claim(post, config):
    """
    Handles comment replies containing the word 'claim' and routes
    based on a basic decision tree.

    :param post: The Comment object containing the claim.
    :param config: the global config dict.
    :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.debug('Received `claim` on post we do not own. Ignoring.')
        return

    try:
        if not coc_accepted(post, config):
            # do not cache this page. We want to get it every time.
            post.reply(
                _(
                    please_accept_coc.format(
                        get_wiki_page('codeofconduct', config))))
            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
示例#2
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)
示例#3
0
def set_meta_flair_on_other_posts(r, tor, config):
    """
    Loops through the 10 newest posts on ToR and sets the flair to
    'Meta' for any post that is not authored by the bot or any of
    the moderators.

    :param r: Active reddit object
    :param tor: The Subreddit object for ToR.
    :param config: the active config object.
    :return: None.
    """
    for post in tor.new(limit=10):

        if (post.author != r.redditor('transcribersofreddit')
                and post.author not in config.tor_mods
                and post.link_flair_text != flair.meta):
            logging.info('Flairing post {} by author {} with Meta.'.format(
                post.fullname, post.author))
            flair_post(post, flair.meta)
def process_claim(post, config):
    """
    Handles comment replies containing the word 'claim' and routes
    based on a basic decision tree.

    :param post: The Comment object containing the claim.
    :param config: the global config dict.
    :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.debug('Received `claim` on post we do not own. Ignoring.')
        return

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

    if top_parent.link_flair_text is 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(
            'Claim on ID {} by {} successful'.format(
                top_parent.fullname, post.author
            )
        )
    # 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))
示例#5
0
def process_post(new_post, tor, config):
    """
    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 tor: TranscribersOfReddit subreddit instance.
    :param config: the config object.
    :return: None.
    """

    subreddit = new_post.subreddit

    if subreddit.display_name in config.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 < config.upvote_filter_subs[subreddit.display_name]:
            return

    if not is_valid(new_post.fullname, config):
        logging.debug(id_already_handled_in_db.format(new_post.fullname))
        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('Posting call for transcription on ID {} posted by {}'.format(
        new_post.fullname, new_post.author.name))

    if new_post.domain in config.image_domains:
        content_type = 'image'
        content_format = config.image_formatting

    elif new_post.domain in config.audio_domains:
        content_type = 'audio'
        content_format = config.audio_formatting

    elif new_post.domain in config.video_domains:
        if 'youtu' in new_post.domain:
            if not valid_youtube_video(new_post.url):
                return
            if get_yt_transcript(new_post.url):
                new_post.reply(_(yt_already_has_transcripts))
                add_complete_post_id(new_post.fullname, config)
                logging.info('Found YouTube video, {}, with good transcripts.'
                             ''.format(get_yt_video_id(new_post.url)))
                return
        content_type = 'video'
        content_format = config.video_formatting
    else:
        # how could we get here without fulfilling one of the above
        # criteria? Just remember: the users will find a way.
        content_type = 'Unknown'
        content_format = 'Formatting? I think something went wrong here...'

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

        add_complete_post_id(new_post.fullname, config)
        config.redis.incr('total_posted', amount=1)

        if config.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.
            config.redis.set(new_post.fullname, result.fullname)
            config.redis.rpush('ocr_ids', new_post.fullname)

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

    # I need to figure out what errors can happen here
    except Exception as e:
        logging.error('{} - unable to post content.\n'
                      'ID: {id}\n'
                      'Title: {title}\n'
                      'Subreddit: {sub}'.format(
                          e,
                          id=new_post.fullname,
                          title=new_post.title,
                          sub=new_post.subreddit.display_name))
示例#6
0
文件: inbox.py 项目: GrafeasGroup/tor
                    alt_text_trigger=alt_text,
                )
            elif "!override" in r_body:
                message, flair = process_override(reply.author,
                                                  blossom_submission,
                                                  reply.parent_id, cfg)
            elif "!debug" in r_body:
                message, flair = process_debug(reply.author,
                                               blossom_submission, cfg)
            else:
                # If we made it this far, it's something we can't process automatically
                forward_to_slack(reply, cfg)
        if message:
            send_reddit_reply(reply, message)
        if flair:
            flair_post(reply.submission, flair)

    except (ClientException, AttributeError) as e:
        # the only way we should hit this is if somebody comments and then
        # deletes their comment before the bot finished processing. It's
        # uncommon, but common enough that this is necessary.
        log.warning(e)
        log.warning(f"Unable to process comment {reply.submission.shortlink} "
                    f"by {reply.author}")


@beeline.traced(name="process_mention")
def process_mention(mention: Comment) -> None:
    """
    Handles username mentions and handles the formatting and posting of
    those calls as workable jobs to ToR.
示例#7
0
def process_done(post, config, override=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.
    :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:
                post.reply(_(done_completed_transcript))
                update_user_flair(post, config)
                logging.info(
                    f'Post {top_parent.fullname} completed by {post.author}!'
                )
            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
示例#8
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))
示例#9
0
def process_post(new_post, config):
    """
    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 config: the config object.
    :return: None.
    """

    if new_post['subreddit'] in config.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'] < config.upvote_filter_subs[new_post['subreddit']]:
            return

    if not is_valid(new_post['name'], config):
        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 config.image_domains:
        content_type = 'image'
        content_format = config.image_formatting

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

    elif new_post['domain'] in config.video_domains:
        if 'youtu' in new_post['domain']:
            if not valid_youtube_video(new_post['url']):
                add_complete_post_id(new_post['name'], config)
                return
            if get_yt_transcript(new_post['url']):
                np = config.r.submission(id=new_post['name'])
                np.reply(_(yt_already_has_transcripts))
                add_complete_post_id(new_post['name'], config)
                logging.info(
                    f'Found YouTube video, {get_yt_video_id(new_post["url"])},'
                    f' with good transcripts.')
                return
        content_type = 'video'
        content_format = config.video_formatting
    else:
        # This means we pulled from a subreddit bypassing the filters.
        content_type = 'Other'
        content_format = config.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 = config.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=config.header)))
        flair_post(result, flair.unclaimed)

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

        if config.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.
            config.redis.set(new_post['name'], result.fullname)
            config.redis.rpush('ocr_ids', new_post['name'])

        config.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"]}')
示例#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