コード例 #1
0
ファイル: inbox.py プロジェクト: GrafeasGroup/tor
def process_reply(reply: Comment, cfg: Config) -> None:
    try:
        log.debug(f"Received reply from {reply.author.name}: {reply.body}")
        message = ""
        flair = None
        r_body = reply.body.lower()  # cache that thing

        if "image transcription" in r_body or is_comment_transcription(
                reply, cfg):
            message = i18n["responses"]["general"]["transcript_on_tor_post"]
        elif matches := [
                match.group() for match in
            [regex.search(reply.body) for regex in MOD_SUPPORT_PHRASES]
                if match
        ]:
            phrases = '"' + '", "'.join(matches) + '"'
            send_to_modchat(
                i18n["mod"]["intervention_needed"].format(
                    phrases=phrases,
                    link=reply.submission.shortlink,
                    author=reply.author.name,
                ),
                cfg,
            )
            message = i18n["responses"]["general"]["getting_help"]
        elif "thank" in r_body:  # trigger on "thanks" and "thank you"
            thumbs_up_gifs = i18n["urls"]["thumbs_up_gifs"]
            youre_welcome = i18n["responses"]["general"]["youre_welcome"]
            message = youre_welcome.format(random.choice(thumbs_up_gifs))
コード例 #2
0
ファイル: inbox.py プロジェクト: kschelonka/tor
def process_mod_intervention(post, cfg):
    """
    Triggers an alert in slack with a link to the comment if there is something
    offensive or in need of moderator intervention
    """
    if not isinstance(post, RedditComment):
        # Why are we here if it's not a comment?
        return

    # Collect all offenses (noted by the above regular expressions) from the
    # original
    phrases = []
    for regex in MOD_SUPPORT_PHRASES:
        matches = regex.search(post.body)
        if not matches:
            continue

        phrases.append(matches.group())

    if len(phrases) == 0:
        # Nothing offensive here, why did this function get triggered?
        return

    # Wrap each phrase in double-quotes (") and commas in between
    phrases = '"' + '", "'.join(phrases) + '"'

    send_to_modchat(
        f':rotating_light::rotating_light: Mod Intervention Needed '
        f':rotating_light::rotating_light: '
        f'\n\nDetected use of {phrases} {post.submission.shortlink}', cfg)
コード例 #3
0
ファイル: user_interaction.py プロジェクト: kschelonka/tor
def process_coc(post, cfg):
    """
    Adds the username of the redditor to the db as accepting the code of
    conduct.

    :param post: The Comment object containing the claim.
    :param cfg: the global config dict.
    :return: None.
    """
    result = cfg.redis.sadd('accepted_CoC', post.author.name)

    modchat_emote = random.choice([
        ':tada:', ':confetti_ball:', ':party-lexi:', ':party-parrot:', ':+1:',
        ':trophy:', ':heartpulse:', ':beers:', ':gold:', ':upvote:',
        ':coolio:', ':derp:', ':lenny1::lenny2:', ':panic:',
        ':fidget-spinner:', ':fb-like:'
    ])

    # Have they already been added? If 0, then just act like they said `claim`
    # instead. If they're actually new, then send a message to slack.
    if result == 1:
        send_to_modchat(
            f'<{reddit_url.format("/user/" + post.author.name)}|u/{post.author.name}>'
            f' has just'
            f' <{reddit_url.format(post.context)}|accepted the CoC!>'
            f' {modchat_emote}',
            cfg,
            channel='new_volunteers')
    process_claim(post, cfg, first_time=True)
コード例 #4
0
ファイル: inbox.py プロジェクト: kschelonka/tor
def check_inbox(cfg):
    """
    Goes through all the unread messages in the inbox. It deliberately
    leaves mail which does not fit into either category so that it can
    be read manually at a later point.

    :return: None.
    """
    # Sort inbox, then act on it
    # Invert the inbox so we're processing oldest first!
    for item in reversed(list(cfg.r.inbox.unread(limit=None))):
        # Very rarely we may actually get a message from Reddit itself.
        # In this case, there will be no author attribute.
        if item.author is None:
            send_to_modchat(
                f'We received a message without an author -- '
                f'*{item.subject}*:\n{item.body}', cfg)
            item.mark_read()

        elif item.author.name == 'transcribot':
            item.mark_read()

        elif item.author.name in cfg.redis.smembers('blacklist'):
            logging.info(
                f'Skipping inbox item from {item.author.name} who is on the '
                f'blacklist ')
            item.mark_read()
            continue

        elif item.subject == 'username mention':
            logging.info(f'Received mention! ID {item}')

            # noinspection PyUnresolvedReferences
            try:
                process_mention(item)
            except (AttributeError, RedditClientException):
                # apparently this crashes with an AttributeError if someone
                # calls the bot and immediately deletes their comment. This
                # should fix that.
                continue
            item.mark_read()

        elif item.subject in ('comment reply', 'post reply'):
            process_reply(item, cfg)

        elif item.subject[0] == '!':
            # Handle our special commands
            process_command(item, cfg)
            item.mark_read()
            continue

        elif isinstance(item, RedditMessage):
            process_message(item, cfg)
            item.mark_read()

        else:
            item.mark_read()
            forward_to_slack(item, cfg)
コード例 #5
0
ファイル: inbox.py プロジェクト: kschelonka/tor
def forward_to_slack(item, cfg):
    username = item.author.name

    send_to_modchat(
        f'<{reddit_url.format(item.context)}|Unhandled message>'
        f' by'
        f' <{reddit_url.format("/u/" + username)}|u/{username}> -- '
        f'*{item.subject}*:\n{item.body}', cfg)
    logging.info(
        f'Received unhandled inbox message from {username}. \n Subject: '
        f'{item.subject}\n\nBody: {item.body} ')
コード例 #6
0
ファイル: inbox.py プロジェクト: GrafeasGroup/tor
def forward_to_slack(item: InboxableMixin, cfg: Config) -> None:
    username = str(item.author.name)

    send_to_modchat(
        f'<{i18n["urls"]["reddit_url"].format(item.context)}|Unhandled message>'
        f" by"
        f' <{i18n["urls"]["reddit_url"].format("/u/" + username)}|u/{username}> -- '
        f"*{item.subject}*:\n{item.body}",
        cfg,
    )
    log.info(f"Received unhandled inbox message from {username}. \n Subject: "
             f"{item.subject}\n\nBody: {item.body} ")
コード例 #7
0
ファイル: user_interaction.py プロジェクト: kschelonka/tor
def process_message(message: RedditMessage, cfg):
    dm_subject = i18n['responses']['direct_message']['dm_subject']
    dm_body = i18n['responses']['direct_message']['dm_body']

    author = message.author
    username = author.name

    author.message(dm_subject, dm_body)

    send_to_modchat(
        f'DM from <{reddit_url.format("/u/" + username)}|u/{username}> -- '
        f'*{message.subject}*:\n{message.body}', cfg)

    logging.info(f'Received DM from {username}. \n Subject: '
                 f'{message.subject}\n\nBody: {message.body} ')
コード例 #8
0
ファイル: inbox.py プロジェクト: GrafeasGroup/tor
def check_inbox(cfg: Config) -> None:
    """
    Goes through all the unread messages in the inbox. It deliberately
    leaves mail which does not fit into either category so that it can
    be read manually at a later point.

    :return: None.
    """
    # Sort inbox, then act on it
    # Invert the inbox so we're processing oldest first!
    for item in reversed(list(cfg.r.inbox.unread(limit=None))):
        # Very rarely we may actually get a message from Reddit itself.
        # In this case, there will be no author attribute.
        author_name = item.author.name if item.author else None

        if author_name is None:
            send_to_modchat(
                f"We received a message without an author -- "
                f"*{item.subject}*:\n{item.body}",
                cfg,
            )
        elif author_name == "transcribot":
            # bot responses shouldn't trigger workflows in other bots
            log.info("Skipping response from our OCR bot")
        elif author_name == "blossom-app":
            log.info("Skipping response from Blossom")
        else:
            if isinstance(item, Comment):
                if is_our_subreddit(item.subreddit.name, cfg):
                    process_reply(item, cfg)
                else:
                    log.info(f"Received username mention! ID {item}")
                    process_mention(item)
            elif isinstance(item, Message):
                if item.subject[0] == "!":
                    process_command(item, cfg)
                else:
                    process_message(item, cfg)
            else:
                # We don't know what the heck this is, so just send it onto
                # slack for manual triage.
                forward_to_slack(item, cfg)
        # No matter what, we want to mark this as read so we don't re-process it.
        item.mark_read()
コード例 #9
0
ファイル: flair.py プロジェクト: kschelonka/tor
def set_meta_flair_on_other_posts(cfg):
    """
    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 cfg: the active config object.
    :return: None.
    """
    for post in cfg.tor.new(limit=10):

        if (post.author.name not in __BOT_NAMES__
                and post.author not in cfg.tor_mods
                and post.link_flair_text != flair.meta):
            logging.info(
                f'Flairing post {post.fullname} by author {post.author} with '
                f'Meta. ')
            flair_post(post, flair.meta)
            send_to_modchat(f'New meta post: <{post.shortlink}|{post.title}>',
                            cfg)
コード例 #10
0
ファイル: user_interaction.py プロジェクト: GrafeasGroup/tor
def process_coc(username: str, context: str, blossom_submission: Dict,
                cfg: Config) -> Tuple:
    """
    Process the acceptation of the CoC by the specified user.

    :param username: The name of the user accepting the CoC
    :param context: The context of the reply, to use as a link
    :param blossom_submission: The corresponding Submission in Blossom
    :param cfg: Config of tor
    """
    user_response = cfg.blossom.get_user(username=username)
    if user_response.status == BlossomStatus.ok:
        # The status codes of accepting the CoC are not checked because they are already
        # caught by getting the user.
        response = cfg.blossom.accept_coc(username=username)
        new_acceptance = response.status == BlossomStatus.ok
        if new_acceptance:
            emote = random.choice(MODCHAT_EMOTES)
            user_url = i18n["urls"]["reddit_url"].format(f"/u/{username}")
            post_url = i18n["urls"]["reddit_url"].format(context)
            send_to_modchat(
                f"<{user_url}|u/{username}> has just "
                f"<{post_url}|accepted the CoC!> {emote}",
                cfg,
                channel="CAZN8J078",
            )
        return process_claim(username,
                             blossom_submission,
                             cfg,
                             first_time=new_acceptance)
    elif user_response.status == BlossomStatus.not_found:
        cfg.blossom.create_user(username=username)
        return (
            i18n["responses"]["general"]["coc_not_accepted"].format(
                get_wiki_page("codeofconduct", cfg)),
            None,
        )
    else:
        return process_claim(username, blossom_submission, cfg)
コード例 #11
0
def set_meta_flair_on_other_posts(cfg: Config) -> None:
    """
    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 cfg: the active config object.
    :return: None.
    """
    for post in cfg.tor.new(limit=10):
        if str(post.author) in __BOT_NAMES__:
            continue
        if str(post.author) in cfg.tor_mods:
            continue
        if post.link_flair_text == flair.meta:
            continue

        log.info(
            f"Flairing post {post.fullname} by author {post.author} with Meta."
        )
        flair_post(post, flair.meta)
        send_to_modchat(f"New meta post: <{post.shortlink}|{post.title}>", cfg)
コード例 #12
0
ファイル: validation.py プロジェクト: kschelonka/tor
def verified_posted_transcript(post, cfg):
    """
    Because we're using basic gamification, we need to put in at least
    a few things to make it difficult to game the system. When a user
    says they've completed a post, we check the parent post for a top-level
    comment by the user who is attempting to complete the post and for the
    presence of the key. If it's all there, we update their flair and mark
    it complete. Otherwise, we ask them to please contact the mods.

    Process:
    Get source link, check all comments, look for a root level comment
    by the author of the post and verify that the key is in their post.
    Return True if found, False if not.

    :param post: The Comment object that contains the string 'done'.
    :param cfg: the global config object.
    :return: True if a post is found, False if not.
    """
    top_parent = get_parent_post_id(post, cfg.r)

    linked_resource = cfg.r.submission(top_parent.id_from_url(top_parent.url))
    # get rid of the "See More Comments" crap
    linked_resource.comments.replace_more(limit=0)
    for top_level_comment in linked_resource.comments.list():
        if (_author_check(post, top_level_comment)
                and _footer_check(top_level_comment, cfg)):
            return True

    # Did their transcript get flagged by the spam filter? Check their history.
    if _author_history_check(post, cfg):
        send_to_modchat(f'Found removed post: <{post.submission.shortlink}>',
                        cfg,
                        channel='#removed_posts')
        return True
    else:
        return False
コード例 #13
0
ファイル: user_interaction.py プロジェクト: GrafeasGroup/tor
def process_message(message: Message, cfg: Config) -> None:
    dm_subject = i18n["responses"]["direct_message"]["dm_subject"]
    dm_body = i18n["responses"]["direct_message"]["dm_body"]

    author = message.author
    username = author.name if author else None

    if username:
        author.message(dm_subject, dm_body)
        send_to_modchat(
            f'DM from <{i18n["urls"]["reddit_url"].format("/u/" + username)}|u/{username}> -- '
            f"*{message.subject}*:\n{message.body}",
            cfg,
        )
        log.info(
            f"Received DM from {username}. \n Subject: {message.subject}\n\nBody: {message.body}"
        )
    else:
        send_to_modchat(
            f"DM with no author -- "
            f"*{message.subject}*:\n{message.body}", cfg)
        log.info(
            f"Received DM with no author. \n Subject: {message.subject}\n\nBody: {message.body}"
        )
コード例 #14
0
ファイル: user_interaction.py プロジェクト: GrafeasGroup/tor
def process_done(
    user: Redditor,
    blossom_submission: Dict,
    comment: Comment,
    cfg: Config,
    override=False,
    alt_text_trigger=False,
) -> Tuple:
    """
    Handles comments where the user claims to have completed a post.

    This function sends a reply to the user depending on the responses received
    from Blossom.

    :param user: The user claiming his transcription is done
    :param blossom_submission: The relevant submission in Blossom
    :param comment: The comment of the user, used to retrieve the user's flair
    :param cfg: the global config object.
    :param override: whether the validation check should be skipped
    :param alt_text_trigger: whether there is an alternative to "done" that has
                             triggered this function.
    """
    return_flair = None
    done_messages = i18n["responses"]["done"]
    # This is explicitly missing the format call that adds the code of
    # conduct text because if we populate it here, we will fetch the wiki
    # page on _every single `done`_ and that's just silly. Only populate
    # it if it's necessary.
    coc_not_accepted = i18n["responses"]["general"]["coc_not_accepted"]

    blossom_user = cfg.blossom.get_user(username=user.name)
    if blossom_user.status != BlossomStatus.ok:
        # If we don't know who the volunteer is, then we don't have a record of
        # them and they need to go through the code of conduct process.
        return (
            coc_not_accepted.format(get_wiki_page("codeofconduct", cfg)),
            return_flair,
        )

    if not blossom_user.data["accepted_coc"]:
        # If the volunteer in question hasn't accepted the code of conduct,
        # eject early and return. Although the `create_transcription` endpoint
        # returns a code of conduct check, we only hit it when we create a
        # transcription, which requires that they wrote something. If a volunteer
        # just writes `done` without putting a transcription down, it will hit
        # this edge case.
        return (
            coc_not_accepted.format(get_wiki_page("codeofconduct", cfg)),
            return_flair,
        )

    transcription, is_visible = get_transcription(blossom_submission["url"],
                                                  user, cfg)

    message = done_messages["cannot_find_transcript"]  # default message

    if not transcription:
        # When the user replies `done` quickly after posting the transcription,
        # it might not be available on Reddit yet. Wait a bit and try again.
        time.sleep(1)
        transcription, is_visible = get_transcription(
            blossom_submission["url"], user, cfg)

    if transcription and not override:
        # Try to detect common formatting errors
        formatting_errors = check_for_formatting_issues(transcription.body)
        if len(formatting_errors) > 0:
            # Formatting issues found.  Reject the `done` and ask the
            # volunteer to fix them.
            issues = ", ".join([error.value for error in formatting_errors])
            # TODO: Re-evaluate if this is necessary
            # This is more of a temporary thing to see how the
            # volunteers react to the bot.
            send_to_modchat(
                i18n["mod"]["formatting_issues"].format(
                    author=user.name,
                    issues=issues,
                    link=f"https://reddit.com{comment.context}",
                ),
                cfg,
                "formatting-issues",
            )
            message = get_formatting_issue_message(formatting_errors)
            return message, return_flair

    if transcription:
        cfg.blossom.create_transcription(
            transcription.id,
            transcription.body,
            i18n["urls"]["reddit_url"].format(str(transcription.permalink)),
            transcription.author.name,
            blossom_submission["id"],
            not is_visible,
        )

    if transcription or override:
        # because we can enter this state with or without a transcription, it
        # makes sense to have this as a separate block.
        done_response = cfg.blossom.done(blossom_submission["id"], user.name,
                                         override)
        # Note that both the not_found and coc_not_accepted status are already
        # caught in the previous lines of code, hence these are not checked again.
        if done_response.status == BlossomStatus.ok:
            return_flair = flair.completed
            set_user_flair(user, comment, cfg)
            log.info(
                f'Done on Submission {blossom_submission["tor_url"]} by {user.name}'
                f" successful.")
            message = done_messages["completed_transcript"]
            transcription_count = blossom_user.data["gamma"] + 1

            if check_promotion(transcription_count):
                additional_message = generate_promotion_message(
                    transcription_count)
                message = f"{message}\n\n{additional_message}"

            if alt_text_trigger:
                message = f"I think you meant `done`, so here we go!\n\n{message}"

        elif done_response.status == BlossomStatus.already_completed:
            message = done_messages["already_completed"]

        elif done_response.status == BlossomStatus.missing_prerequisite:
            message = done_messages["not_claimed_by_user"]

        elif done_response.status == BlossomStatus.blacklisted:
            message = i18n["responses"]["general"]["blacklisted"]

    return message, return_flair
コード例 #15
0
def process_command(reply, cfg):
    """
    This function processes any commands send to the bot via PM with a subject
    that stars with a !. The basic flow is read JSON file, look for key with
    same subject, check if the caller is mod, or is in the list of allowed
    people, then reply with the results of pythonFunction.

    To add a new command: add an entry to commands.json, (look at the other
    commands already listed), and add your function to admin_commands.py.

    :param reply: Object, the message object that contains the requested
        command
    :param cfg: the global config object
    :return: None
    """

    # Trim off the ! from the start of the string
    requested_command = reply.subject[1:]

    with open('commands.json', newline='') as commands_file:
        commands = json.load(commands_file)
        logging.debug(
            f'Searching for command {requested_command}, '
            f'from {reply.author.name}.'
        )

        try:
            command = commands['commands'][requested_command]

        except KeyError:
            if from_moderator(reply, cfg):
                reply.reply(
                    "That command hasn't been implemented yet ):"
                    "\n\nMessage a dev to make your dream come true."
                )

            logging.warning(
                f"Error, command: {requested_command} not found!"
                f" (from {reply.author.name})"
            )

            return

        # command found
        logging.info(
            f'{reply.author.name} is attempting to run {requested_command}'
        )

        # Mods are allowed to do any command, and some people are whitelisted
        # per command to be able to use them
        if (
            reply.author.name not in command['allowedNames'] and
            not from_moderator(reply, cfg)
        ):
            logging.info(
                f"{reply.author.name} failed to run {requested_command},"
                f"because they aren't a mod, or aren't whitelisted to use this"
                f" command"
            )
            username = reply.author.name
            send_to_modchat(
                f":banhammer: Someone did something bad! "
                f"<https://reddit.com/user/{username}|u/{username}> tried to "
                f"run {requested_command}!", cfg
            )

            reply.reply(
                random.choice(commands['notAuthorizedResponses']).format(
                    random.choice(cfg.no_gifs)
                )
            )

            return

        logging.debug(
            f'Now executing command {requested_command},'
            f' by {reply.author.name}.'
        )

        result = globals()[command['pythonFunction']](reply, cfg)

        if result is not None:
            reply.reply(result)
コード例 #16
0
ファイル: user_interaction.py プロジェクト: kschelonka/tor
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