Exemple #1
0
def gameday_roster_update(game):
    """ Gets the gameday rosters from the live feed endpoint.
        This is needed because in some instances a player is not included
        on the /teams/{id}/roster page for some reason.

    Args:
        game: Current Game object

    Returns:
        None
    """

    args = arguments.get_arguments()

    home_team = game.home_team
    away_team = game.away_team

    logging.info("Getting gameday rosters from Live Feed endpoint.")

    try:
        gameday_roster = livefeed.get_livefeed(game.game_id)
        all_players = gameday_roster.get("gameData").get("players")
        for player_id, player in all_players.items():
            try:
                team = player.get("currentTeam").get("name")
                if team == home_team.team_name:
                    home_team.gameday_roster[player_id] = player
                else:
                    away_team.gameday_roster[player_id] = player
            except Exception as e:
                logging.error("%s doesn't have a team - skipping.",
                              player["fullName"])
    except Exception as e:
        logging.error("Unable to get all players.")
        logging.error(e)
Exemple #2
0
def get_api():
    """
    Returns an Authorized session of the Tweepy API.

    Input:
        None

    Output:
        tweepy_session - authorized twitter session that can send a tweet.
    """
    args = arguments.get_arguments()

    twitterenv = "debug" if args.debugsocial else "prod"
    twitter_config = utils.load_config()["twitter"][twitterenv]

    consumer_key = twitter_config["consumer_key"]
    consumer_secret = twitter_config["consumer_secret"]
    access_token = twitter_config["access_token"]
    access_secret = twitter_config["access_secret"]

    auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
    auth.set_access_token(access_token, access_secret)

    tweepy_session = tweepy.API(auth)
    return tweepy_session
Exemple #3
0
def get_twython_api():
    """
    Returns an Authorized session of the Twython API.
    This is only used until Tweepy supports Chunked Video uploads.

    Input:
        None

    Output:
        twython_session - authorized twitter session that can send a tweet.
    """
    args = arguments.get_arguments()

    twitterenv = "debug" if args.debugsocial else "prod"
    twitter_config = utils.load_config()["twitter"][twitterenv]

    consumer_key = twitter_config["consumer_key"]
    consumer_secret = twitter_config["consumer_secret"]
    access_token = twitter_config["access_token"]
    access_secret = twitter_config["access_secret"]

    twython_session = Twython(consumer_key, consumer_secret, access_token,
                              access_secret)

    return twython_session
def spawn_another_process():
    """Spawns a second process of the hockeygamebot (for split squad games)."""
    args = arguments.get_arguments()

    if args.date is not None:
        python_exec = sys.executable
        script_path = os.path.join(PROJECT_ROOT, "app.py")
        spawn_args = " ".join(sys.argv[1:])
        full_exec = [
            "{python} {script} --split {args}".format(python=python_exec,
                                                      script=script_path,
                                                      args=spawn_args)
        ]

        logging.debug("Spawning Process: %s", full_exec)
        Popen(full_exec, shell=True)
    else:
        python_exec = sys.executable
        script_path = os.path.join(PROJECT_ROOT, "app.py")
        spawn_args = " ".join(sys.argv[1:])
        full_exec = [
            "{python} {script} --split {args}".format(python=python_exec,
                                                      script=script_path,
                                                      args=spawn_args)
        ]
        logging.debug("Spawning Process: %s", full_exec)
        # Popen(["nohup python3 " + dirname + "/hockey_twitter_bot.py --split &"], shell=True)
        Popen(full_exec, shell=True)
Exemple #5
0
    def wrapper_social_timeout(*args, **kwargs):
        parsed_args = arguments.get_arguments()
        config = load_config()

        # If notweets is specified, always run the social methods
        if parsed_args.notweets:
            return func(*args, **kwargs)

        # Check for a force-send flag & if not False, force the message through
        if kwargs.get("force_send"):
            return func(*args, **kwargs)

        try:
            event = kwargs.get("event")
            event_time = dateutil.parser.parse(event.date_time)
            timeout = config["script"]["event_timeout"]
            utcnow = datetime.now(timezone.utc)
            time_since_event = (utcnow - event_time).total_seconds()
            if time_since_event < timeout:
                return func(*args, **kwargs)
            else:
                logging.info(
                    "Event #%s (%s) occurred %s second(s) in the past - older than our social timeout.",
                    event.event_idx,
                    event.event_type,
                    time_since_event,
                )
                # return False
                return {"twitter": None, "discord": None, "slack": None}
        except Exception as e:
            logging.warning("Timeout function should contain an event key:value. %s", e)
            return func(*args, **kwargs)
Exemple #6
0
def setup_logging():
    """Configures application logging and prints the first three log lines."""
    # pylint: disable=line-too-long
    # logger = logging.getLogger(__name__)

    # Create logs directory if not present
    if not os.path.exists(LOGS_PATH):
        os.makedirs(LOGS_PATH)

    args = arguments.get_arguments()

    # Reset root handler to default so BasicConfig is respected
    root = logging.getLogger()
    if root.handlers:
        for handler in root.handlers:
            root.removeHandler(handler)

    log_file_name = datetime.now().strftime(
        load_config()["script"]["log_file_name"] + "-%Y%m%d%H%M%S.log")
    log_file = os.path.join(LOGS_PATH, log_file_name)
    if args.console and args.debug:
        logging.basicConfig(
            level=logging.DEBUG,
            datefmt="%Y-%m-%d %H:%M:%S",
            format=
            "%(asctime)s - %(module)s.%(funcName)s (%(lineno)d) - %(levelname)s - %(message)s",
        )

    elif args.console:
        logging.basicConfig(
            level=logging.INFO,
            datefmt="%Y-%m-%d %H:%M:%S",
            format=
            "%(asctime)s - %(module)s.%(funcName)s (%(lineno)d) - %(levelname)s - %(message)s",
        )
    else:
        logging.basicConfig(
            filename=log_file,
            level=logging.INFO,
            datefmt="%Y-%m-%d %H:%M:%S",
            format=
            "%(asctime)s - %(module)s.%(funcName)s (%(lineno)d) - %(levelname)s - %(message)s",
        )

    # Reset logging level (outside of Basic Config)
    logger = logging.getLogger()
    logger_level = logging.DEBUG if args.debug else logging.INFO
    logger.setLevel(logger_level)
def send_discord(msg, title=None, color=16777215, embed=None, media=None):
    """Sends a text-only Discord message.

    Args:
        msg: Message to send to the channel.
        media: Any media to be sent to the Webhook

    Returns:
        None
    """

    args = arguments.get_arguments()

    discordenv = "debug" if args.debugsocial else "prod"
    discord_config = config.discord[discordenv]
    webhook_url = discord_config["webhook_url"]

    # Support multiple Discord Servers
    webhook_url = [webhook_url] if not isinstance(webhook_url, list) else webhook_url

    for url in webhook_url:
        if embed:
            requests.post(url, json=embed)
            continue

        linebreak_msg = f"▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬\n{msg}"
        payload = {"content": linebreak_msg}

        if title:
            embed_msg = {"embeds": [{"title": title, "description": msg, "color": color}]}
            response = requests.post(url, json=embed_msg)
        elif not title and not media:
            embed_msg = {"embeds": [{"title": "Game Bot Update", "description": msg, "color": color}]}
            response = requests.post(url, json=embed_msg)
        else:
            if isinstance(media, list):
                files = dict()
                for idx, image in enumerate(media):
                    files_key = f"file{idx}"
                    files[files_key] = open(image, "rb")
            else:
                files = {"file": open(media, "rb")}
            response = requests.post(url, files=files, data=payload)

        # If we get a non-OK code back from the Discord endpoint, log it.
        if not response.ok:
            logging.warning(response.json())
def is_game_today(team_id, date):
    """Queries the NHL Schedule API to determine if there is a game today.

    Args:
        team_id (int) - The unique identifier of the team (from get_team function).

    Returns:
        (bool, games_info)
        bool - True if game today, False if not.
        games_info (dict) - A dictionary from the Schedule API that describes game information.
    """
    args = arguments.get_arguments()

    url = "/schedule?teamId={id}&expand=" "schedule.broadcasts,schedule.teams&date={date:%Y-%m-%d}".format(
        id=team_id, date=date
    )

    response = api.nhl_api(url)
    if response:
        schedule = response.json()
        games_total = schedule["totalItems"]
    else:
        return False, None

    if games_total == 1:
        games_info = schedule["dates"][0]["games"][0]
        return True, games_info

    if games_total == 2:
        if args.split is False:
            logging.info("Split Squad - spawning a second process to pick up second game.")
            game_index = 0
            process.spawn_another_process()
            time.sleep(10)
        else:
            game_index = 1
            logging.info("Split Squad - this is the process to pick up second game (sleep 5 seconds).")
            time.sleep(5)

        games_info = schedule["dates"][0]["games"][game_index]
        return True, games_info

    date_string = date.date() if args.date else "today"
    logging.info("There are no games scheduled for %s, SAD!", date_string)
    return False, schedule
def get_number_games(season: str, team_id: int, game_type_code: str = "R") -> dict:
    """Queries the NHL Schedule API to how many games are in this season.
        This is particularly important in the 2020-2021 shortened season.

    Args:
        season (str) - The 8-digit season code (ex: 20202021).
        team_id (int) - The unique identifier of the team (from get_team function).

    Returns:
        num_games (int) - The number of games played in the regular season.
    """
    args = arguments.get_arguments()

    endpoint = f"/schedule?teamId={team_id}&season={season}&gameType={game_type_code}"
    response = api.nhl_api(endpoint)

    if response:
        schedule = response.json()
        games_total = schedule["totalItems"]
        return games_total

    # If no valid response, just return default number of games (82)
    return 82
import os
from hockeygamebot.helpers import arguments

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
RESOURCES_PATH = os.path.join(PROJECT_ROOT, "resources")
IMAGES_PATH = os.path.join(RESOURCES_PATH, "images")
LOGS_PATH = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir, "logs"))
TESTS_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir, "tests"))
TESTS_RESOURCES_PATH = os.path.join(TESTS_ROOT, "resources")

# Define CONFIG_PATH separately in case of override
args = arguments.get_arguments()
URLS_PATH = os.path.join(PROJECT_ROOT, "config", "urls.yaml")
CONFIG_FILE = "config.yaml" if not args.config else args.config
CONFIG_PATH = os.path.join(PROJECT_ROOT, "config", CONFIG_FILE)

VERSION = "2.0.1"
Exemple #11
0
def send(msg, **kwargs):
    """The handler function that takes a message and a set of key-value arguments
        to be routed to social media functions.

    Args:
        message: The main message to be sent to all social media sites.
        # TODO: **kwargs

    Returns:
        None
    """
    # If for some reason, message is None (or False), just exit the function.
    if not msg:
        return

    if GlobalGame.game and GlobalGame.game.other_team.team_name == "Washington Capitals":
        msg = msg.lower()

    args = arguments.get_arguments()
    social_config = config.socials

    # Initialize a return dictionary
    return_dict = {"twitter": None, "discord": None, "slack": None}

    if args.notweets:
        logging.info("[SOCIAL] %s", msg)
        if kwargs.get("media"):
            media = kwargs.get("media")
            if isinstance(media, list):
                for single_image in media:
                    Image.open(single_image).show()
            else:
                Image.open(media).show()
            # kwargs.get("media").show()
        return return_dict

    if social_config["twitter"]:
        # tweet_id = twitter.send_tweet(
        #     msg, media=kwargs.get("media"), reply=kwargs.get("reply"), hashtag=kwargs.get("hashtag")
        # )
        team_hashtag = kwargs.get("team_hashtag")
        game_hashtag = kwargs.get("game_hashtag")
        tweet_id = twitter.send_tweet(
            msg,
            media=kwargs.get("media"),
            video=kwargs.get("video"),
            reply=kwargs.get("reply"),
            hashtags=kwargs.get("hashtags"),
            team_hashtag=kwargs.get("team_hashtag"),
            game_hashtag=kwargs.get("game_hashtag"),
        )
        return_dict["twitter"] = tweet_id

    if social_config["discord"]:
        msg = kwargs.get("discord_msg", msg)
        discord.send_discord(
            msg,
            embed=kwargs.get("discord_embed"),
            title=kwargs.get("discord_title"),
            color=kwargs.get("discord_color"),
            media=kwargs.get("media"),
        )

    if social_config["slack"]:
        pass

    return return_dict
Exemple #12
0
def pregame_image(game: Game):
    """Generates the pre-game image that is sent to social media platforms at the first
        run of the hockeygamebot script per day.

    Args:
        game: Current Game object

    Returns:
        bg: Finished Image instance
    """

    # Fonts used within the pregame image
    FONT_TITLE = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.TITLE)
    FONT_RECORD = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.RECORD)
    FONT_STREAK = ImageFont.truetype(FontFiles.BITTER_BOLD, FontSizes.STREAK)
    FONT_DETAIL_LARGE = ImageFont.truetype(FontFiles.BITTER_BOLD,
                                           FontSizes.DETAIL_LARGE)
    FONT_DETAIL_SMALL = ImageFont.truetype(FontFiles.BITTER_BOLD,
                                           FontSizes.DETAIL_SMALL)
    FONT_GAMENUMBER = ImageFont.truetype(FontFiles.BITTER_BOLD,
                                         FontSizes.GAMENUMBER * 3)
    FONT_BOT_TAG = ImageFont.truetype(FontFiles.BITTER_BOLD,
                                      FontSizes.GAMENUMBER * 2)

    # Pre-game specific constant values (incl coordinates)
    HEADER_TEXT = "PRE-GAME MATCHUP"
    LOGO_Y = 150
    COORDS_HOME_X = 245
    COORDS_AWAY_X = 650
    COORDS_HOME_LOGO = (COORDS_HOME_X, LOGO_Y)
    COORDS_AWAY_LOGO = (COORDS_AWAY_X, LOGO_Y)
    COORDS_GAME_NUM = (-90, 80)
    COORDS_BOT_TAG = (910, 330)
    TEAM_RECORD_Y = LOGO_Y + 200
    TEAM_STREAK_Y = TEAM_RECORD_Y + FontSizes.RECORD + 10

    # Generate records, venue & other strings
    # If this is the first team, game then no streak
    home_pts = game.home_team.points
    home_record_str = f"{home_pts} PTS • {game.home_team.current_record}"
    home_streak_last10 = (
        f"{game.home_team.streak} • LAST 10: {game.home_team.last_ten}"
        if game.home_team.games > 0 else "")

    away_pts = game.away_team.points
    away_record_str = f"{away_pts} PTS • {game.away_team.current_record}"
    away_streak_last10 = (
        f"{game.away_team.streak} • LAST 10: {game.away_team.last_ten}"
        if game.away_team.games > 0 else "")

    num_games = schedule.get_number_games(season=game.season,
                                          team_id=game.preferred_team.team_id,
                                          game_type_code=game.game_type)
    text_gamenumber = ("PRESEASON" if game.game_type == "PR" else
                       f"{game.preferred_team.games + 1} OF {num_games}")

    text_datetime = f"{game.game_date_short} • {game.game_time_local}"
    text_hashtags = (
        f"{utils.team_hashtag(game.preferred_team.team_name, game.game_type)} • {game.game_hashtag}"
    )

    bg = Image.open(Backgrounds.PREGAME)
    bg_w, bg_h = bg.size

    away_team = game.away_team.team_name.replace(" ", "")
    home_team = game.home_team.team_name.replace(" ", "")
    away_logo = Image.open(
        os.path.join(PROJECT_ROOT, f"resources/logos/{away_team}.png"))
    home_logo = Image.open(
        os.path.join(PROJECT_ROOT, f"resources/logos/{home_team}.png"))

    # Paste the home / away logos with the mask the same as the image
    bg.paste(away_logo, COORDS_AWAY_LOGO, away_logo)
    bg.paste(home_logo, COORDS_HOME_LOGO, home_logo)

    # Generates a 'draw' object that we use to draw on top of the image
    draw = ImageDraw.Draw(bg)

    # Draw text items on the background now
    # fmt: off
    center_text(draw=draw,
                left=0,
                top=0,
                width=bg_w,
                text=HEADER_TEXT,
                color=Colors.WHITE,
                font=FONT_TITLE)

    center_text(draw=draw,
                left=COORDS_HOME_X,
                top=TEAM_RECORD_Y,
                width=300,
                text=home_record_str,
                color=Colors.WHITE,
                font=FONT_RECORD)

    center_text(draw=draw,
                left=COORDS_HOME_X,
                top=TEAM_STREAK_Y,
                width=300,
                text=home_streak_last10,
                color=Colors.WHITE,
                font=FONT_STREAK)

    center_text(draw=draw,
                left=COORDS_AWAY_X,
                top=TEAM_RECORD_Y,
                width=300,
                text=away_record_str,
                color=Colors.WHITE,
                font=FONT_RECORD)

    center_text(draw=draw,
                left=COORDS_AWAY_X,
                top=TEAM_STREAK_Y,
                width=300,
                text=away_streak_last10,
                color=Colors.WHITE,
                font=FONT_STREAK)

    center_text(
        draw=draw,
        left=0,
        top=480,
        width=bg_w,
        text=text_datetime,
        color=Colors.WHITE,
        font=FONT_DETAIL_LARGE,
    )

    center_text(
        draw=draw,
        left=0,
        top=540,
        width=bg_w,
        text=game.venue.upper(),
        color=Colors.WHITE,
        font=FONT_DETAIL_LARGE,
    )

    center_text(
        draw=draw,
        left=0,
        top=600,
        width=bg_w,
        text=text_hashtags,
        color=Colors.WHITE,
        font=FONT_DETAIL_SMALL,
    )
    # fmt: on

    # Create a new image to put the game number & cleanly rotate it
    txt = Image.new("L", (900, 900))
    d = ImageDraw.Draw(txt)
    center_text(
        draw=d,
        left=0,
        top=0,
        width=900,
        text=text_gamenumber,
        color=255,
        font=FONT_GAMENUMBER,
    )
    w = txt.rotate(315, expand=True, resample=Image.BICUBIC)
    w_resize = w.resize((300, 300), Image.ANTIALIAS)
    bg.paste(w_resize, COORDS_GAME_NUM, w_resize)

    # Create a new image to put the game bot handle & cleanly rotate it
    args = arguments.get_arguments()
    twitterenv = "debug" if args.debugsocial else "prod"
    twitter_config = utils.load_config()["twitter"][twitterenv]
    twitter_handle = twitter_config["handle"]

    txt = Image.new("L", (900, 900))
    d = ImageDraw.Draw(txt)
    center_text(
        draw=d,
        left=0,
        top=0,
        width=900,
        text=f"@{twitter_handle}",
        color=255,
        font=FONT_BOT_TAG,
    )
    w = txt.rotate(315, expand=True, resample=Image.BICUBIC)
    w_resize = w.resize((300, 300), Image.ANTIALIAS)
    bg.paste(w_resize, COORDS_BOT_TAG, w_resize)

    return bg
Exemple #13
0
def run():
    """ The main script runner - everything starts here! """
    config = utils.load_config()
    args = arguments.get_arguments()

    # Setup the logging for this script run (console, file, etc)
    utils.setup_logging()

    # Get the team name the bot is running as
    team_name = args.team if args.team else config["default"]["team_name"]

    # ------------------------------------------------------------------------------
    # PRE-SCRIPT STARTS PROCESSING BELOW
    # ------------------------------------------------------------------------------

    # Log script start lines
    logging.info("#" * 80)
    logging.info("New instance of the Hockey Twitter Bot (V%s) started.",
                 VERSION)
    if args.docker:
        logging.info(
            "Running in a Docker container - environment variables parsed.")
    logging.info("TIME: %s", datetime.now())
    logging.info("ARGS - notweets: %s, console: %s, teamoverride: %s",
                 args.notweets, args.console, args.team)
    logging.info(
        "ARGS - debug: %s, debugsocial: %s, overridelines: %s",
        args.debug,
        args.debugsocial,
        args.overridelines,
    )
    logging.info("ARGS - date: %s, split: %s, localdata: %s", args.date,
                 args.split, args.localdata)
    logging.info(
        "SOCIAL - twitter: %s, discord: %s, slack: %s",
        config["socials"]["twitter"],
        config["socials"]["discord"],
        config["socials"]["slack"],
    )
    logging.info("%s\n", "#" * 80)

    # Check if there is a game scheduled for today -
    # If there is no game, exit the script.
    date = utils.date_parser(args.date) if args.date else datetime.now()
    team_id = schedule.get_team_id(team_name)
    game_today, game_info = schedule.is_game_today(team_id, date)
    if not game_today:
        game_yesterday, prev_game = schedule.was_game_yesterday(team_id, date)
        if game_yesterday:
            logging.info(
                "There was a game yesterday - send recap, condensed game "
                "and generate new season overview stats chart, tweet it & exit."
            )

            # Get Team Information
            home_team = prev_game["teams"]["home"]
            home_team_name = home_team["team"]["name"]
            away_team = prev_game["teams"]["away"]
            away_team_name = away_team["team"]["name"]

            pref_team = home_team if home_team["team"][
                "name"] == team_name else away_team
            other_team = away_team if home_team["team"][
                "name"] == team_name else home_team

            pref_team_name = pref_team["team"]["name"]
            pref_score = pref_team["score"]
            pref_hashtag = utils.team_hashtag(pref_team_name)
            other_team_name = other_team["team"]["name"]
            other_score = other_team["score"]

            # Get the Recap & Condensed Game
            game_id = prev_game["gamePk"]
            content_feed = contentfeed.get_content_feed(game_id)

            # Send Recap Tweet
            try:
                recap, recap_video_url = contentfeed.get_game_recap(
                    content_feed)
                recap_description = recap["items"][0]["description"]
                recap_msg = f"📺 {recap_description}.\n\n{recap_video_url}"
                socialhandler.send(recap_msg)
            except Exception as e:
                logging.error("Error getting Game Recap. %s", e)

            # Send Condensed Game / Extended Highlights Tweet
            try:
                condensed_game, condensed_video_url = contentfeed.get_condensed_game(
                    content_feed)
                condensed_blurb = condensed_game["items"][0]["blurb"]
                condensed_msg = f"📺 {condensed_blurb}.\n\n{condensed_video_url}"
                socialhandler.send(condensed_msg)
            except Exception as e:
                logging.error(
                    "Error getting Condensed Game from NHL - trying YouTube. %s",
                    e)
                try:
                    condensed_game = youtube.youtube_condensed(
                        away_team_name, home_team_name)
                    condensed_blurb = condensed_game["title"]
                    condensed_video_url = condensed_game["yt_link"]
                    condensed_msg = f"📺 {condensed_blurb}.\n\n{condensed_video_url}"
                    socialhandler.send(condensed_msg)
                except Exception as e:
                    logging.error(
                        "Error getting Condensed Game from NHL & YouTube - skip this today. %s",
                        e)

            # Generate the Season Overview charts
            game_result_str = "defeat" if pref_score > other_score else "lose to"

            team_season_msg = (
                f"Updated season overview & last 10 game stats after the {pref_team_name} "
                f"{game_result_str} the {other_team_name} by a score of {pref_score} to {other_score}."
                f"\n\n{pref_hashtag}")

            team_season_fig = nst.generate_team_season_charts(team_name, "sva")
            team_season_fig_last10 = nst.generate_team_season_charts(
                team_name, "sva", lastgames=10)
            team_season_fig_all = nst.generate_team_season_charts(
                team_name, "all")
            team_season_fig_last10_all = nst.generate_team_season_charts(
                team_name, "all", lastgames=10)
            team_season_charts = [
                team_season_fig,
                team_season_fig_last10,
                team_season_fig_all,
                team_season_fig_last10_all,
            ]
            socialhandler.send(team_season_msg, media=team_season_charts)
        else:
            logging.info("There was no game yesterday - exiting!")

        sys.exit()

    # For debugging purposes, print all game_info
    logging.debug("%s", game_info)

    # Create the Home & Away Team objects
    away_team = Team.from_json(game_info, "away")
    home_team = Team.from_json(game_info, "home")

    # If lines are being overriden by a local lineup file,
    # set the overrlide lines property to True
    if args.overridelines:
        home_team.overridelines = True
        away_team.overridelines = True

    # The preferred team is the team the bot is running as
    # This allows us to track if the preferred team is home / away
    home_team.preferred = bool(home_team.team_name == team_name)
    away_team.preferred = bool(away_team.team_name == team_name)

    # Create the Game Object!
    game = Game.from_json_and_teams(game_info, home_team, away_team)
    GlobalGame.game = game

    # Override Game State for localdata testing
    game.game_state = "Live" if args.localdata else game.game_state

    # Update the Team Objects with the gameday rosters
    roster.gameday_roster_update(game)

    # print(vars(game))
    # print(vars(away_team))
    # print(vars(home_team))

    # If the codedGameState is set to 9 originally, game is postponed (exit immediately)
    if game.game_state_code == GameStateCode.POSTPONED.value:
        logging.warning(
            "This game is marked as postponed during our first run - exit silently."
        )
        end_game_loop(game)

    # Return the game object to use in the game loop function
    return game
Exemple #14
0
def send_tweet(tweet_text,
               video=None,
               media=None,
               reply=None,
               hashtags=None,
               team_hashtag=None,
               game_hashtag=None):
    """Generic tweet function that uses logic to call other specific functions.

    Args:
        tweet_text: The text to send as a tweet (may contain URL at end to qote tweet).
        media: Any media we want to upload to Twitter (images, videos, GIFs)
        reply: Are we replying to a specific tweet (for threading purposes)

    Returns:
        last_tweet - A link to the last tweet sent (or search result if duplicate)
                     If duplicate cannot be found, returns base URL (also raises error)
    """
    logging.info("[TWITTER] %s (Media: %s, Reply: %s)", tweet_text, media,
                 reply)
    args = arguments.get_arguments()

    twitterenv = "debug" if args.debugsocial else "prod"
    twitter_config = utils.load_config()["twitter"][twitterenv]
    twitter_handle = twitter_config["handle"]
    if args.notweets:
        logging.info("%s", tweet_text)
        return "https://twitter.com/{handle}/status".format(
            handle=twitter_handle)

    # Get the API session & send a tweet depending on the parameters sent
    api = get_api()

    # Add any hashtags that need to be added
    # Start with team hashtag (most required)
    # if hashtags:
    #     tweet_text = f'{tweet_text}\n\n{hashtags}'
    if game_hashtag:
        tweet_text = f"{tweet_text}\n\n{Hashtag.game_hashtag}"

    # Only use this function for upload highlight videos.
    # Single use case & we know the exact path that triggers this
    if video is not None:
        try:
            logging.info(
                "Video was detected - using chunked video upload to download & send."
            )
            twython_api = get_twython_api()

            logging.info(
                "Uploading the video file using chunked upload to Twitter.")
            video_file = open(video, "rb")
            upload_response = twython_api.upload_video(
                media=video_file,
                media_type="video/mp4",
                media_category="tweet_video",
                check_progress=True)
            processing_info = upload_response["processing_info"]
            state = processing_info["state"]
            wait = processing_info.get("check_after_secs", 1)

            if state == "pending" or state == "in_progress":
                logging.info(f"Upload not done - waiting %s seconds.", wait)
                time.sleep(wait)

            logging.info("Upload completed - sending tweet now.")
            # If we have gotten this far, remove the URL from the tweet text.
            tweet_text = tweet_text.split("\n")[0]
            # tweet_text = f"@{twitter_handle} {tweet_text}"
            status = twython_api.update_status(
                status=tweet_text,
                in_reply_to_status_id=reply,
                auto_populate_reply_metadata=True,
                media_ids=[upload_response["media_id"]],
            )
            return status.get("id_str")
        except Exception as e:
            logging.error(
                "There was an error uploading and sending the embedded video - send with a link."
            )
            logging.error(e)

    try:
        if not reply and not media:
            status = api.update_status(status=tweet_text)
        elif not reply and media:
            if isinstance(media, list):
                media_ids = [
                    api.media_upload(i).media_id_string for i in media
                ]
                status = api.update_status(status=tweet_text,
                                           media_ids=media_ids)
            else:
                status = api.update_with_media(status=tweet_text,
                                               filename=media)
        elif reply and not media:
            # tweet_text = f"@{twitter_handle} {tweet_text}"
            status = api.update_status(status=tweet_text,
                                       in_reply_to_status_id=reply,
                                       auto_populate_reply_metadata=True)
        elif reply and media:
            # tweet_text = f"@{twitter_handle} {tweet_text}"
            if isinstance(media, list):
                media_ids = [
                    api.media_upload(i).media_id_string for i in media
                ]
                status = api.update_status(
                    status=tweet_text,
                    media_ids=media_ids,
                    in_reply_to_status_id=reply,
                    auto_populate_reply_metadata=True,
                )
            else:
                status = api.update_with_media(status=tweet_text,
                                               filename=media,
                                               in_reply_to_status_id=reply)

        return status.id

    except Exception as e:
        logging.error("Failed to send tweet : %s", tweet_text)
        logging.error(e)
        return None
Exemple #15
0
def intermission_loop(game: Game):
    """The live-game intermission loop. Things to do during an intermission

    Args:
        game: Game Object

    Returns:
        live_sleep_time: The amount to sleep until our next check.
    """

    args = arguments.get_arguments()
    config = utils.load_config()

    # If we are in intermission, check if NST is ready for charts.
    # Incorporating the check into this loop will be sure we obey the 60s sleep rule.
    # We use the currentPeriod as the key to lookup if the charts
    # have been sent for the current period's intermission
    nst_chart_period_sent = game.nst_charts.charts_by_period.get(
        game.period.current)
    if not nst_chart_period_sent:
        logging.info(
            "NST Charts not yet sent - check if it's ready for us to scrape.")
        nst_ready = nst.is_nst_ready(
            game.preferred_team.short_name) if not args.date else True
        if nst_ready:
            try:
                list_of_charts = nst.generate_all_charts(game=game)
                # Chart at Position 0 is the Overview Chart & 1-4 are the existing charts
                overview_chart = list_of_charts["overview"]
                team_charts = list_of_charts["barcharts"]
                shift_chart = list_of_charts["shift"]

                overview_chart_msg = (
                    f"Team Overview stat percentages - 5v5 (SVA) after the "
                    f"{game.period.current_ordinal} period (via @NatStatTrick)."
                )

                ov_social_ids = socialhandler.send(overview_chart_msg,
                                                   media=overview_chart,
                                                   game_hashtag=True)

                charts_msg = (
                    f"Individual, on-ice, forward lines & defensive pairs after the "
                    f"{game.period.current_ordinal} period (via @NatStatTrick)."
                )
                ind_social_ids = socialhandler.send(
                    charts_msg,
                    media=team_charts,
                    game_hashtag=True,
                    reply=ov_social_ids["twitter"])

                charts_msg = (
                    f"Shift length breakdown after the "
                    f"{game.period.current_ordinal} period (via @NatStatTrick)."
                )
                social_ids = socialhandler.send(
                    charts_msg,
                    media=shift_chart,
                    game_hashtag=True,
                    reply=ind_social_ids["twitter"])

                game.nst_charts.charts_by_period[game.period.current] = True

            except Exception as e:
                logging.error(
                    "Error creating Natural Stat Trick charts (%s) - sleep for a bit longer.",
                    e)

    # Check if our shotmap was RT'd & if not try to search for it and send it out
    shotmap_retweet_sent = game.period.shotmap_retweet
    if not shotmap_retweet_sent and config["socials"]["twitter"]:
        game.period.shotmap_retweet = common.search_send_shotmap(game=game)

    # Calculate proper sleep time based on intermission status
    if game.period.intermission_remaining > config["script"][
            "intermission_sleep_time"]:
        live_sleep_time = config["script"]["intermission_sleep_time"]
        logging.info(
            "Sleeping for configured intermission time (%ss).",
            config["script"]["intermission_sleep_time"],
        )
    else:
        live_sleep_time = game.period.intermission_remaining
        logging.info("Sleeping for remaining intermission time (%ss).",
                     game.period.intermission_remaining)

    return live_sleep_time
def dailyfaceoff_goalies(pref_team, other_team, pref_homeaway, game_date):
    """Scrapes Daily Faceoff for starting goalies for the night.

    Args:
        pref_team (Team): Preferred team object.
        other_team (Team): Other team object.
        pref_homeaway (str): Is preferred team home or away?
        game_date: Game date to add to DF URL

    Returns:
        Tuple: dictionary {goalie string, goalie confirmed}
    """
    return_dict = {}
    urls = utils.load_urls()
    args = arguments.get_arguments()

    df_goalies_url = urls["endpoints"]["df_starting_goalies"]
    df_goalies_url = f"{df_goalies_url}{game_date}"
    df_linecombos_url = urls["endpoints"]["df_line_combos"]

    logging.info(
        "Trying to get starting goalie information via Daily Faceoff.")
    cache_headers = {"cache-control": "max-age=0"}
    resp = thirdparty_request(df_goalies_url, headers=cache_headers)

    # If we get a bad response from the function above, return False
    if resp is None:
        return False

    soup = bs4_parse(resp.content)
    if soup is None:
        return False

    logging.info(
        "Valid response received & souped - parse the Daily Faceoff page!")
    pref_team_name = pref_team.team_name

    games = soup.find_all("div", class_="starting-goalies-card stat-card")
    team_playing_today = any(pref_team_name in game.text for game in games)
    if games and team_playing_today:
        for game in games:
            teams = game.find("h4", class_="top-heading-heavy").text
            # If the preferred team is not in this matchup, skip this loop iteration
            if pref_team_name not in teams:
                continue

            teams_split = teams.split(" at ")
            home_team = teams_split[1]
            away_team = teams_split[0]
            goalies = game.find("div", class_="stat-card-main-contents")

            away_goalie_info = goalies.find("div", class_="away-goalie")
            away_goalie_name = away_goalie_info.find("h4").text.strip()
            away_goalie_confirm = away_goalie_info.find("h5",
                                                        class_="news-strength")
            away_goalie_confirm = str(away_goalie_confirm.text.strip())
            away_goalie_stats = away_goalie_info.find("p",
                                                      class_="goalie-record")
            away_goalie_stats_str = " ".join(
                away_goalie_stats.text.strip().split())

            away_goalie = dict()
            away_goalie["name"] = away_goalie_name
            away_goalie["confirm"] = away_goalie_confirm
            away_goalie["season"] = away_goalie_stats_str

            home_goalie_info = goalies.find("div", class_="home-goalie")
            home_goalie_name = home_goalie_info.find("h4").text.strip()
            home_goalie_confirm = home_goalie_info.find("h5",
                                                        class_="news-strength")
            home_goalie_confirm = str(home_goalie_confirm.text.strip())
            home_goalie_stats = home_goalie_info.find("p",
                                                      class_="goalie-record")
            home_goalie_stats_str = " ".join(
                home_goalie_stats.text.strip().split())

            home_goalie = dict()
            home_goalie["name"] = home_goalie_name
            home_goalie["confirm"] = home_goalie_confirm
            home_goalie["season"] = home_goalie_stats_str

            if pref_homeaway == "home":
                return_dict["pref"] = home_goalie
                return_dict["other"] = away_goalie
                return_dict["pref"]["homeaway"] = "home"
                return_dict["other"]["homeaway"] = "away"
            else:
                return_dict["pref"] = away_goalie
                return_dict["other"] = home_goalie
                return_dict["pref"]["homeaway"] = "away"
                return_dict["other"]["homeaway"] = "home"

            return_dict["home"] = home_goalie
            return_dict["away"] = away_goalie
            return return_dict

    # If there is any issue parsing the Daily Faceoff page, grab a goalie from each team
    else:
        logging.info(
            "There was an issue parsing the Daily Faceoff page, "
            "grabbing a goalie from each individual team's line combinations page."
        )
        pref_team_encoded = pref_team.team_name.replace(" ", "-").replace(
            "é", "e").lower()
        other_team_encoded = other_team.team_name.replace(" ", "-").replace(
            "é", "e").lower()
        df_url_pref = df_linecombos_url.replace("TEAMNAME", pref_team_encoded)
        df_url_other = df_linecombos_url.replace("TEAMNAME",
                                                 other_team_encoded)

        logging.info("Getting a fallback goalie for the preferred team.")
        resp = thirdparty_request(df_url_pref)
        soup = bs4_parse(resp.content)
        goalie_table = soup.find("table", attrs={
            "summary": "Goalies"
        }).find("tbody").find_all("tr")
        pref_goalie_name = goalie_table[0].find_all("td")[0].find("a").text

        logging.info("Getting a fallback goalie for the other team.")
        resp = thirdparty_request(df_url_other)
        soup = bs4_parse(resp.content)
        goalie_table = soup.find("table", attrs={
            "summary": "Goalies"
        }).find("tbody").find_all("tr")
        other_goalie_name = goalie_table[0].find_all("td")[0].find("a").text

        return_dict["pref_goalie"] = pref_goalie_name
        return_dict["pref_goalie_confirm"] = "Not Found"
        return_dict["other_goalie"] = other_goalie_name
        return_dict["other_goalie_confirm"] = "Not Found"

        return return_dict

    return True
Exemple #17
0
def start_game_loop(game: Game):
    """The main game loop - tracks game state & calls all relevant functions.

    Args:
        game: Current Game object

    Returns:
        None
    """

    args = arguments.get_arguments()
    config = utils.load_config()

    # ------------------------------------------------------------------------------
    # START THE MAIN LOOP
    # ------------------------------------------------------------------------------

    while True:
        if game.game_state == GameState.PREVIEW.value:
            livefeed_resp = livefeed.get_livefeed(game.game_id)
            game.update_game(livefeed_resp)

            # If after the update_game() function runs, we have a Postponed Game
            # We should tweet it - this means it happened after the game was scheduled
            if game.game_state_code == GameStateCode.POSTPONED.value:
                logging.warning(
                    "This game was originally scheduled, but is now postponed."
                )
                social_msg = (
                    f"⚠️ The {game.preferred_team.team_name} game scheduled for today has been postponed."
                )
                socialhandler.send(social_msg)
                end_game_loop(game)

            if game.game_time_countdown > 0:
                logging.info(
                    "Game is in Preview state - send out all pregame information."
                )
                # The core game preview function should run only once
                if not game.preview_socials.core_sent:
                    preview.generate_game_preview(game)

                # The other game preview function should run every xxx minutes
                # until all pregame tweets are sent or its too close to game time
                sleep_time, last_sleep_before_live = preview.game_preview_others(
                    game)
                game.preview_socials.increment_counter()

                # If this is the last sleep before the game goes live, cut it by 5 minutes for starters function.
                if last_sleep_before_live:
                    logging.info(
                        "This is the last sleep before the game goes live - 5 minutes less & starters."
                    )
                    sleep_time = 0 if (sleep_time - 300) < 0 else sleep_time
                    time.sleep(sleep_time)
                    preview.get_starters(game)
                else:
                    time.sleep(sleep_time)

            else:
                logging.info(
                    "Game is in Preview state, but past game start time - sleep for a bit "
                    "& update game attributes so we detect when game goes live."
                )

                # Somehow we got here without the starting lineup - try again
                if not game.preview_socials.starters_sent:
                    preview.get_starters(game)

                sleep_time = config["script"]["pregame_sleep_time"]
                time.sleep(sleep_time)

        elif game.game_state == GameState.LIVE.value:
            try:
                logging.info("-" * 80)
                logging.info(
                    "Game is LIVE (loop #%s) - checking events after event Idx %s.",
                    game.live_loop_counter,
                    game.last_event_idx,
                )

                # On my development machine, this command starts the files for this game
                # python -m hockeygamebot --console --notweets --team 'Vancouver Canucks' --date '2019-09-17' --localdata
                if args.localdata:
                    logging.info(
                        "SIMULATION DETECTED - running a live game replay for Game %s (%s vs. %s).",
                        game.game_id,
                        game.home_team.team_name,
                        game.away_team.team_name,
                    )
                    directory = "/Users/mattdonders/Development/python/devils-goal-twitter-bitbucket/scratchpad/feed-samples"
                    for file in sorted(os.listdir(directory)):
                        filename = os.fsdecode(file)
                        if filename.endswith(".json"):
                            feed_json = os.path.join(directory, filename)
                            with open(feed_json) as json_file:
                                data = json.load(json_file)

                            # Logging (Temporarily) for Penalty Killed Tweets
                            logging.info(
                                "Current Period Info: %s - %s",
                                game.period.current_ordinal,
                                game.period.time_remaining,
                            )
                            logging.info(
                                "Pref On Ice: %s - %s",
                                len(game.preferred_team.onice),
                                game.preferred_team.onice,
                            )
                            logging.info(
                                "Other On Ice: %s - %s",
                                len(game.other_team.onice),
                                game.other_team.onice,
                            )

                            # Penalty Killed Status
                            penalty_situation = game.penalty_situation
                            if penalty_situation.penalty_killed:
                                logging.info(
                                    "***** PENALTY KILLED NOTIFICATION *****")
                                shots_taken = (
                                    penalty_situation.pp_team.shots -
                                    penalty_situation.pp_team_shots_start)
                                logging.info("PP Shots Taken: %s", shots_taken)
                                game.penalty_situation = PenaltySituation()

                            if game.penalty_situation.in_situation:
                                logging.info(
                                    "Current Penalty (In Situation): %s",
                                    vars(game.penalty_situation),
                                )

                            if not game.period.current_oneminute_sent:
                                live.minute_remaining_check(game)

                            live.live_loop(livefeed=data, game=game)
                            game.update_game(data)

                            time.sleep(0.1)

                # Non-Local Data
                livefeed_resp = livefeed.get_livefeed(game.game_id)
                # all_events = live.live_loop(livefeed=livefeed_resp, game=game)

                # Update all game attributes & check for goalie pulls
                game.update_game(livefeed_resp)
                game.goalie_pull_updater(livefeed_resp)

                # Logging (Temporarily) for Penalty Killed Tweets
                logging.info(
                    "Current Period Info: %s - %s",
                    game.period.current_ordinal,
                    game.period.time_remaining,
                )
                logging.info(
                    "Pref On Ice: %s - %s",
                    len(game.preferred_team.onice),
                    game.preferred_team.onice,
                )
                logging.info("Other On Ice: %s - %s",
                             len(game.other_team.onice), game.other_team.onice)

                # Penalty Killed Status
                penalty_situation = game.penalty_situation
                if penalty_situation.penalty_killed:
                    logging.info("***** PENALTY KILLED NOTIFICATION *****")
                    shots_taken = penalty_situation.pp_team.shots - penalty_situation.pp_team_shots_start
                    logging.info("PP Shots Taken: %s", shots_taken)
                    game.penalty_situation = PenaltySituation()

                if game.penalty_situation.in_situation:
                    logging.info("Current Penalty (In Situation): %s",
                                 vars(game.penalty_situation))

                if not game.period.current_oneminute_sent:
                    live.minute_remaining_check(game)

                # Pass the live feed response to the live loop (to parse events)
                live.live_loop(livefeed=livefeed_resp, game=game)
                # game_events = get_game_events(game_obj)
                # loop_game_events(game_events, game_obj)

            except Exception as error:
                logging.error(
                    "Uncaught exception in live game loop - see below error.")
                logging.error(error)

            # Perform any intermission score changes, charts & sleep
            if game.period.intermission:
                # Uncomment this tomorrow to test the function relocation
                live_sleep_time = live.intermission_loop(game)

            else:
                live_sleep_time = config["script"]["live_sleep_time"]
                logging.info(
                    "Sleeping for configured live game time (%ss).",
                    config["script"]["live_sleep_time"],
                )

            # Now increment the counter sleep for the calculated time above
            game.live_loop_counter += 1
            time.sleep(live_sleep_time)

        elif game.game_state == GameState.FINAL.value:
            logging.info(
                "Game is now over & 'Final' - run end of game functions with increased sleep time."
            )

            livefeed_resp = livefeed.get_livefeed(game.game_id)
            game.update_game(livefeed_resp)

            # If (for some reason) the bot was started after the end of the game
            # We need to re-run the live loop once to parse all of the events
            if not game.events:
                logging.info(
                    "Bot started after game ended, pass livefeed into event factory to fill events."
                )
                live.live_loop(livefeed=livefeed_resp, game=game)

            # shotmaps.generate_shotmaps(game=game)

            # Run all end of game / final functions
            if not game.final_socials.final_score_sent:
                final.final_score(livefeed=livefeed_resp, game=game)

            if not game.final_socials.three_stars_sent:
                final.three_stars(livefeed=livefeed_resp, game=game)

            if not game.final_socials.nst_linetool_sent:
                # thirdparty.nst_linetool(game=game, team=game.preferred_team)
                game.final_socials.nst_linetool_sent = True

            if not game.final_socials.shotmap_retweet:
                game.final_socials.shotmap_retweet = common.search_send_shotmap(
                    game=game)

            if not game.final_socials.hsc_sent:
                final.hockeystatcards(game=game)

            if not game.nst_charts.final_charts:
                logging.info(
                    "NST Charts not yet sent - check if it's ready for us to scrape."
                )
                nst_ready = nst.is_nst_ready(
                    game.preferred_team.short_name) if not args.date else True
                if nst_ready:
                    all_charts = nst.generate_all_charts(game=game)
                    # Chart at Position 0 is the Overview Chart & 1-4 are the existing charts
                    overview_chart = all_charts["overview"]
                    team_charts = all_charts["barcharts"]
                    scatter_charts = all_charts["scatters"]
                    shift_chart = all_charts["shift"]

                    overview_chart_msg = (
                        f"Team Overview stat percentages - 5v5 (SVA) at the "
                        f"end of the game (via @NatStatTrick).")

                    ov_social_ids = socialhandler.send(overview_chart_msg,
                                                       media=overview_chart,
                                                       game_hashtag=True)

                    charts_msg = (
                        f"Individual, on-ice, forward lines & defensive pairs at the "
                        f"end of the game (via @NatStatTrick).")
                    ind_social_ids = socialhandler.send(
                        charts_msg,
                        media=team_charts,
                        game_hashtag=True,
                        reply=ov_social_ids["twitter"],
                    )

                    charts_msg = f"Shift length breakdown at the end of the game (via @NatStatTrick)."
                    shift_social_ids = socialhandler.send(
                        charts_msg,
                        media=shift_chart,
                        game_hashtag=True,
                        reply=ind_social_ids["twitter"],
                    )

                    charts_msg = (
                        f"Quality vs. Quantity & Expected Goals Rate / 60 at the"
                        " end of the game (via @NatStatTrick).")
                    xg60_social_ids = socialhandler.send(
                        charts_msg,
                        media=scatter_charts,
                        game_hashtag=True,
                        reply=shift_social_ids["twitter"],
                    )
                    game.nst_charts.final_charts = True

            # If we have exceeded the number of retries, stop pinging NST
            if game.final_socials.retries_exeeded:
                game.final_socials.nst_linetool_sent = True

            if game.final_socials.all_social_sent:
                logging.info(
                    "All end of game socials sent or retries were exceeded - ending game!"
                )
                end_game_loop(game=game)

            # If all socials aren't sent or retry limit is not exceeded, sleep & check again.
            logging.info(
                "Final loop #%s done - sleep for %s seconds and check again.",
                game.final_socials.retry_count,
                config["script"]["final_sleep_time"],
            )

            game.final_socials.retry_count += 1
            time.sleep(config["script"]["final_sleep_time"])

        else:
            logging.warning(
                "Game State %s is unknown - sleep for 5 seconds and check again.",
                game.game_state)
            time.sleep(config["script"]["live_sleep_time"])