<reponame>plygrnd/snoowatch
import time
import json

from datetime import datetime

from boto3.session import Session
from botocore.exceptions import ClientError
from praw import Reddit
from prawcore import exceptions

from log import log_generator

logger = log_generator(__name__)


def get_reddit_api_creds(reddit_api_secret_name, aws_region):
    """
    This was mostly copied from the Secrets Manager-generated sample in the console.
    Variable names have been adjusted to suit this use case.

    This is a `script` application (more information in PRAW's documentation [here](https://praw.readthedocs.io/en/latest/getting_started/authentication.html#application-only-client-credentials).)
    As such, you do _not_ need to pass your Reddit username/password to the API; it requires your `user_agent`, `client_id` and `client_secret`.

    Secret value MUST be formatted as follows:

    {
        'client_id': 'xxxxxx',
        'client_secret': 'yyyyyy',
        'user_agent': 'SnooWatch 0.1 by u/beefythecat'
    }

    :param reddit_api_secret_name: The name of your Secrets Manager secret.
    :param aws_region: The AWS region within which your secret is stored
    :returns: JSON-formatted string containing required credentials.
    """

    # Create a Secrets Manager client
    session = Session()
    client = session.client(service_name="secretsmanager", region_name=aws_region)

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=reddit_api_secret_name
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "DecryptionFailureException":
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InternalServiceErrorException":
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InvalidParameterException":
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InvalidRequestException":
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "ResourceNotFoundException":
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
    else:
        # Decrypts secret using the associated KMS CMK.
        secret = get_secret_value_response["SecretString"]

        return json.loads(secret)


class Subreddit(Reddit):
    def __init__(self, reddit_api_secret_name, aws_region, subreddit=None):
        """
        Initializes an instance of Reddit.
        :param subreddit: OPTIONAL. Supply a subreddit name if you're working with subreddits.
        :returns: praw.Reddit
        """

        # Required: user_agent, client_id, client_secret
        reddit_creds = get_reddit_api_creds(reddit_api_secret_name, aws_region)

        if reddit_creds:
            super().__init__(**reddit_creds)
        else:
            raise KeyError("You did not specify reddit credentials, please get real and try again.")

        if subreddit:
            self.sub = self.subreddit(subreddit)

    def get_mod_log(self, since):
        """
        Gets mod actions

        :param since: Date from which to pull data, in Unix epoch format
        :returns: List of mod actions
        """

        actions = []

        for action in self.sub.mod.log(limit=since):
            actions.append(
                {
                    "Name": action.mod.name,
                    "Action": action.action,
                    "Time": datetime.utcfromtimestamp(int(action.created_utc)).strftime(
                        "%Y-%m-%d %H:%M:%S"
                    ),
                    "Details": action.details,
                    "Post": action.target_permalink,
                    "Post Title": action.target_title,
                    "Post Body": action.target_body,
                }
            )

        return actions

    def get_single_submission_by_id(self, post_id):
        """
        Fetch a single post by ID.
        :param post_id: Base-36 encoded post ID.
        :returns: Single submission
        """

        logger.info(f"Fetching post {post_id}")

        post = self.submission(id=post_id)

        submission = {
            "title": post.title,
            "link": post.permalink,
            "url": post.url,
            "text": post.selftext,
            "author": self.get_redditor(post.author.name).name,
        }

        return submission

    def get_redditor(self, redditor):
        """
        Gets a reddit account or returns None if one is unavailable

        :param redditor: Reddit username
        :returns: self.redditor or None
        """
        logger.info(f"Getting account status for u/{redditor} .")
        # We need to check if an account was deleted or shadowbanned
        # (both return prawcore.exceptions.NotFound)
        try:
            redditor = self.redditor(redditor)
            hasattr(redditor, "fullname")
            logger.info(f"u/{redditor.name} is a valid reddit account.")
        except exceptions.NotFound:
            logger.info(f"u/{redditor.name} either shadowbanned or deleted.")
            return

        # https://redd.it/3sbs31
        # Accounts created after 2015/11/10 will have the is_suspended attribute
        # All others will return nothing.
        has_suspended_attr = hasattr(redditor, "is_suspended")
        if has_suspended_attr:
            logger.debug("is_suspended available.")
            is_suspended = getattr(redditor, "is_suspended")
            if is_suspended:
                logger.info(f"u/{redditor} has been banned.")
                return
            else:
                logger.info(f"u/{redditor} has an active Reddit account")
            return redditor
        else:
            logger.debug("is_suspended unavailable")
            logger.info(f"An account exists for u/{redditor}")
            return redditor

    def get_submissions(self, since, until):
        """
        Creates a list of submission objects.

        :param since: Date from which to pull data, in Unix epoch format
        :param until: Date until which to pull data, in unix epoch format
        :returns: List of submission objects
        """
        since = time.mktime(datetime.strptime(since, "%Y/%m/%d").timetuple())
        until = time.mktime(datetime.strptime(until, "%Y/%m/%d").timetuple())

        logger.info("Fetching submissions from Reddit.")
        submissions = [item for item in self.sub.submissions(since, until)]
        logger.info(f"Fetched {len(submissions)} submissions from Reddit.")

        return submissions

    @staticmethod
    def parse_submissions(submissions):
        """
        Parses out useful data from submissions

        :param submissions: name of submissions variable to parse
        :returns: JSON-formatted list of submissions
        """

        metrics = []

        for post in submissions:

            data = {
                "id": post.id,
                "url": post.url,
                "created": datetime.utcfromtimestamp(int(post.created_utc)).strftime(
                    "%Y-%m-%d %H:%M:%S"
                ),
                "title": post.title,
                "flair": post.link_flair_text,
                "views": post.view_count,
                "comment_count": post.num_comments,
                "submission_text": post.selftext,
                "domain": post.domain,
                "removed": post.removed,
                "author": {
                    "author_name": str(post.author),
                    "account_created": None,
                    "account_age": None,
                    "is_banned": None,
                },
                "karma": post.score,
                "upvotes": post.ups,
                "downvotes": post.downs,
            }

            # TODO: figure out how to do this quicker.
            # https://github.com/plygrnd/tinkerbell/issues/3
            if not post.author.name:
                data["author"]["author_name"] = "[deleted]"
            elif hasattr(post.author, "is_suspended"):
                if getattr(post.author, "is_suspended"):
                    data["author"]["account_name"] = post.author.name
                    data["author"]["is_banned"] = True
            else:
                data["author"]["author_name"] = post.author.name
                account_created = datetime.utcfromtimestamp(
                    int(post.author.created_utc)
                )
                data["author"]["account_created"] = str(account_created)
                data["author"]["account_age"] = abs(
                    (datetime.now() - account_created).days
                )
                data["author"]["is_banned"] = False

            logger.debug(data)
            metrics.append(data)

        return metrics

import time
import json

from datetime import datetime

from boto3.session import Session
from botocore.exceptions import ClientError
from praw import Reddit
from prawcore import exceptions

from log import log_generator

logger = log_generator(__name__)


def get_reddit_api_creds(reddit_api_secret_name, aws_region):
    """
    This was mostly copied from the Secrets Manager-generated sample in the console.
    Variable names have been adjusted to suit this use case.

    This is a `script` application (more information in PRAW's documentation [here](https://praw.readthedocs.io/en/latest/getting_started/authentication.html#application-only-client-credentials).)
    As such, you do _not_ need to pass your Reddit username/password to the API; it requires your `user_agent`, `client_id` and `client_secret`.

    Secret value MUST be formatted as follows:

    {
        'client_id': 'xxxxxx',
        'client_secret': 'yyyyyy',
        'user_agent': 'SnooWatch 0.1 by u/beefythecat'
    }

    :param reddit_api_secret_name: The name of your Secrets Manager secret.
    :param aws_region: The AWS region within which your secret is stored
    :returns: JSON-formatted string containing required credentials.
    """

    # Create a Secrets Manager client
    session = Session()
    client = session.client(service_name="secretsmanager", region_name=aws_region)

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=reddit_api_secret_name
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "DecryptionFailureException":
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InternalServiceErrorException":
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InvalidParameterException":
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "InvalidRequestException":
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response["Error"]["Code"] == "ResourceNotFoundException":
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
    else:
        # Decrypts secret using the associated KMS CMK.
        secret = get_secret_value_response["SecretString"]

        return json.loads(secret)


class Subreddit(Reddit):
    def __init__(self, reddit_api_secret_name, aws_region, subreddit=None):
        """
        Initializes an instance of Reddit.
        :param subreddit: OPTIONAL. Supply a subreddit name if you're working with subreddits.
        :returns: praw.Reddit
        """

        # Required: user_agent, client_id, client_secret
        reddit_creds = get_reddit_api_creds(reddit_api_secret_name, aws_region)

        if reddit_creds:
            super().__init__(**reddit_creds)
        else:
            raise KeyError("You did not specify reddit credentials, please get real and try again.")

        if subreddit:
            self.sub = self.subreddit(subreddit)

    def get_mod_log(self, since):
        """
        Gets mod actions

        :param since: Date from which to pull data, in Unix epoch format
        :returns: List of mod actions
        """

        actions = []

        for action in self.sub.mod.log(limit=since):
            actions.append(
                {
                    "Name": action.mod.name,
                    "Action": action.action,
                    "Time": datetime.utcfromtimestamp(int(action.created_utc)).strftime(
                        "%Y-%m-%d %H:%M:%S"
                    ),
                    "Details": action.details,
                    "Post": action.target_permalink,
                    "Post Title": action.target_title,
                    "Post Body": action.target_body,
                }
            )

        return actions

    def get_single_submission_by_id(self, post_id):
        """
        Fetch a single post by ID.
        :param post_id: Base-36 encoded post ID.
        :returns: Single submission
        """

        logger.info(f"Fetching post {post_id}")

        post = self.submission(id=post_id)

        submission = {
            "title": post.title,
            "link": post.permalink,
            "url": post.url,
            "text": post.selftext,
            "author": self.get_redditor(post.author.name).name,
        }

        return submission

    def get_redditor(self, redditor):
        """
        Gets a reddit account or returns None if one is unavailable

        :param redditor: Reddit username
        :returns: self.redditor or None
        """
        logger.info(f"Getting account status for u/{redditor} .")
        # We need to check if an account was deleted or shadowbanned
        # (both return prawcore.exceptions.NotFound)
        try:
            redditor = self.redditor(redditor)
            hasattr(redditor, "fullname")
            logger.info(f"u/{redditor.name} is a valid reddit account.")
        except exceptions.NotFound:
            logger.info(f"u/{redditor.name} either shadowbanned or deleted.")
            return

        # https://redd.it/3sbs31
        # Accounts created after 2015/11/10 will have the is_suspended attribute
        # All others will return nothing.
        has_suspended_attr = hasattr(redditor, "is_suspended")
        if has_suspended_attr:
            logger.debug("is_suspended available.")
            is_suspended = getattr(redditor, "is_suspended")
            if is_suspended:
                logger.info(f"u/{redditor} has been banned.")
                return
            else:
                logger.info(f"u/{redditor} has an active Reddit account")
            return redditor
        else:
            logger.debug("is_suspended unavailable")
            logger.info(f"An account exists for u/{redditor}")
            return redditor

    def get_submissions(self, since, until):
        """
        Creates a list of submission objects.

        :param since: Date from which to pull data, in Unix epoch format
        :param until: Date until which to pull data, in unix epoch format
        :returns: List of submission objects
        """
        since = time.mktime(datetime.strptime(since, "%Y/%m/%d").timetuple())
        until = time.mktime(datetime.strptime(until, "%Y/%m/%d").timetuple())

        logger.info("Fetching submissions from Reddit.")
        submissions = [item for item in self.sub.submissions(since, until)]
        logger.info(f"Fetched {len(submissions)} submissions from Reddit.")

        return submissions

    @staticmethod
    def parse_submissions(submissions):
        """
        Parses out useful data from submissions

        :param submissions: name of submissions variable to parse
        :returns: JSON-formatted list of submissions
        """

        metrics = []

        for post in submissions:

            data = {
                "id": post.id,
                "url": post.url,
                "created": datetime.utcfromtimestamp(int(post.created_utc)).strftime(
                    "%Y-%m-%d %H:%M:%S"
                ),
                "title": post.title,
                "flair": post.link_flair_text,
                "views": post.view_count,
                "comment_count": post.num_comments,
                "submission_text": post.selftext,
                "domain": post.domain,
                "removed": post.removed,
                "author": {
                    "author_name": str(post.author),
                    "account_created": None,
                    "account_age": None,
                    "is_banned": None,
                },
                "karma": post.score,
                "upvotes": post.ups,
                "downvotes": post.downs,
            }

            # TODO: figure out how to do this quicker.
            # https://github.com/plygrnd/tinkerbell/issues/3
            if not post.author.name:
                data["author"]["author_name"] = "[deleted]"
            elif hasattr(post.author, "is_suspended"):
                if getattr(post.author, "is_suspended"):
                    data["author"]["account_name"] = post.author.name
                    data["author"]["is_banned"] = True
            else:
                data["author"]["author_name"] = post.author.name
                account_created = datetime.utcfromtimestamp(
                    int(post.author.created_utc)
                )
                data["author"]["account_created"] = str(account_created)
                data["author"]["account_age"] = abs(
                    (datetime.now() - account_created).days
                )
                data["author"]["is_banned"] = False

            logger.debug(data)
            metrics.append(data)

        return metrics
This was mostly copied from the Secrets Manager-generated sample in the console.     Variable names have been adjusted to suit this use case.      This is a `script` application (more information in PRAW's documentation [here](https://praw.readthedocs.io/en/latest/getting_started/authentication.html#application-only-client-credentials).)     As such, you do _not_ need to pass your Reddit username/password to the API; it requires your `user_agent`, `client_id` and `client_secret`.      Secret value MUST be formatted as follows:      {         'client_id': 'xxxxxx',         'client_secret': 'yyyyyy',         'user_agent': 'SnooWatch 0.1 by u/beefythecat'     }      :param reddit_api_secret_name: The name of your Secrets Manager secret.     :param aws_region: The AWS region within which your secret is stored     :returns: JSON-formatted string containing required credentials. # Create a Secrets Manager client # Secrets Manager can't decrypt the protected secret text using the provided KMS key. # Deal with the exception here, and/or rethrow at your discretion. # An error occurred on the server side. # Deal with the exception here, and/or rethrow at your discretion. # You provided an invalid value for a parameter. # Deal with the exception here, and/or rethrow at your discretion. # You provided a parameter value that is not valid for the current state of the resource. # Deal with the exception here, and/or rethrow at your discretion. # We can't find the resource that you asked for. # Deal with the exception here, and/or rethrow at your discretion. # Decrypts secret using the associated KMS CMK. Initializes an instance of Reddit.         :param subreddit: OPTIONAL. Supply a subreddit name if you're working with subreddits.         :returns: praw.Reddit # Required: user_agent, client_id, client_secret Gets mod actions          :param since: Date from which to pull data, in Unix epoch format         :returns: List of mod actions Fetch a single post by ID.         :param post_id: Base-36 encoded post ID.         :returns: Single submission Gets a reddit account or returns None if one is unavailable          :param redditor: Reddit username         :returns: self.redditor or None # We need to check if an account was deleted or shadowbanned # (both return prawcore.exceptions.NotFound) # https://redd.it/3sbs31 # Accounts created after 2015/11/10 will have the is_suspended attribute # All others will return nothing. Creates a list of submission objects.          :param since: Date from which to pull data, in Unix epoch format         :param until: Date until which to pull data, in unix epoch format         :returns: List of submission objects Parses out useful data from submissions          :param submissions: name of submissions variable to parse         :returns: JSON-formatted list of submissions # TODO: figure out how to do this quicker. # https://github.com/plygrnd/tinkerbell/issues/3