From 20ca7befdffb5516c072e34dd0bb13ea62d6597a Mon Sep 17 00:00:00 2001 From: c0de Date: Fri, 23 Sep 2022 17:40:31 -0500 Subject: [PATCH] Initial Code --- .gitignore | 4 +- src/main/configs.py | 100 ++-- .../database_classes/db_classes.py | 56 +-- src/main/database_module/guess_dao.py | 244 +++++----- src/main/database_module/play_dao.py | 192 ++++---- src/main/db_session.py | 96 ++-- src/main/discord_module/bot_runner.py | 440 +++++++++--------- src/main/discord_module/leaderboard_config.py | 30 +- src/main/services/points_service.py | 96 ++-- 9 files changed, 629 insertions(+), 629 deletions(-) diff --git a/.gitignore b/.gitignore index 48fd780..8f72e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -.idea/* -__pycache__/ +.idea/* +__pycache__/ diff --git a/src/main/configs.py b/src/main/configs.py index bcfe843..3cc008a 100644 --- a/src/main/configs.py +++ b/src/main/configs.py @@ -1,51 +1,51 @@ -# Names of Configurations -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -DATABASE_USERNAME = 'database_username' -DATABASE_PASSWORD = 'database_password' -DATABASE_HOST = 'database_host' -DATABASE_PORT = 'database_port' -DATABASE_NAME = 'database_name' -SEASON_1_SPREADSHEET_ID = 's1_spreadsheet_id' -SEASON_2_SPREADSHEET_ID = 's2_spreadsheet_id' -PLAYER_SPREADSHEET = 'player_spreadsheet' - -''' -Main source for configurations fetched from a startup configuration file. Includes the ability to fetch all, or fetch -one configuration once the file is loaded. - -You'll find the names of these configs above as constants that can be used throughout the rest of this repository -''' -class Configs(): - configs = {} - - def __init__(self, config_file_path): - self.config_file_path = config_file_path - self.__load_configs__() - - ''' - Fetches a single configuration by the name of that configuration. - Returns None if that configuration does not exist - ''' - def get_config_by_name(self, name): - try: - return Configs.configs[name] - except KeyError: - return None - - ''' - Fetches all configurations and returns them as a dictionary of config_key -> config_value - ''' - def get_all_configs(self): - return Configs.configs - - ''' - Performs the initial load of configurations from a startup configuration file - ''' - def __load_configs__(self): - Configs.configs = {} - config_file = open(self.config_file_path, 'r') - for line in config_file: - split_line = line.split('=') +# Names of Configurations +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +DATABASE_USERNAME = 'ghost_user' +DATABASE_PASSWORD = 'root' +DATABASE_HOST = '192.168.0.11' +DATABASE_PORT = '5432' +DATABASE_NAME = 'ghostball' +SEASON_1_SPREADSHEET_ID = 's1_spreadsheet_id' +SEASON_2_SPREADSHEET_ID = 's2_spreadsheet_id' +PLAYER_SPREADSHEET = 'player_spreadsheet' + +''' +Main source for configurations fetched from a startup configuration file. Includes the ability to fetch all, or fetch +one configuration once the file is loaded. + +You'll find the names of these configs above as constants that can be used throughout the rest of this repository +''' +class Configs(): + configs = {} + + def __init__(self, config_file_path): + self.config_file_path = config_file_path + self.__load_configs__() + + ''' + Fetches a single configuration by the name of that configuration. + Returns None if that configuration does not exist + ''' + def get_config_by_name(self, name): + try: + return Configs.configs[name] + except KeyError: + return None + + ''' + Fetches all configurations and returns them as a dictionary of config_key -> config_value + ''' + def get_all_configs(self): + return Configs.configs + + ''' + Performs the initial load of configurations from a startup configuration file + ''' + def __load_configs__(self): + Configs.configs = {} + config_file = open(self.config_file_path, 'r') + for line in config_file: + split_line = line.split('=') Configs.configs[split_line[0]] = split_line[1].strip('\n') \ No newline at end of file diff --git a/src/main/database_module/database_classes/db_classes.py b/src/main/database_module/database_classes/db_classes.py index 83f3371..46ac1b7 100644 --- a/src/main/database_module/database_classes/db_classes.py +++ b/src/main/database_module/database_classes/db_classes.py @@ -1,28 +1,28 @@ -from sqlalchemy import Column, String, Integer, ForeignKey, Date -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship - -Base = declarative_base() - -class Play(Base): - __tablename__ = 'play' - - play_id = Column(String, nullable=False, primary_key=True) - pitch_value = Column(Integer, nullable=True) - creation_date = Column(Date, nullable=False) - server_id = Column(String, nullable=False) - - guesses = relationship(lambda : Guess) - -class Guess(Base): - __FAKE_VALUE__ = -5000 - - __tablename__ = 'guess' - member_id = Column(String, nullable=False, primary_key=True) - play_id = Column(UUID, ForeignKey(Play.play_id), nullable=False, primary_key=True) - guessed_number = Column(Integer, nullable=False) - member_name = Column(String, nullable=False) - difference = Column(Integer) - - play = relationship("Play", back_populates="guesses") +from sqlalchemy import Column, String, Integer, ForeignKey, Date +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + +Base = declarative_base() + +class Play(Base): + __tablename__ = 'play' + + play_id = Column(String, nullable=False, primary_key=True) + pitch_value = Column(Integer, nullable=True) + creation_date = Column(Date, nullable=False) + server_id = Column(String, nullable=False) + + guesses = relationship(lambda : Guess) + +class Guess(Base): + __FAKE_VALUE__ = -5000 + + __tablename__ = 'guess' + member_id = Column(String, nullable=False, primary_key=True) + play_id = Column(UUID, ForeignKey(Play.play_id), nullable=False, primary_key=True) + guessed_number = Column(Integer, nullable=False) + member_name = Column(String, nullable=False) + difference = Column(Integer) + + play = relationship("Play", back_populates="guesses") diff --git a/src/main/database_module/guess_dao.py b/src/main/database_module/guess_dao.py index 376f6ac..4ac9b60 100644 --- a/src/main/database_module/guess_dao.py +++ b/src/main/database_module/guess_dao.py @@ -1,123 +1,123 @@ -from copy import deepcopy - -from src.main.database_module.database_classes.db_classes import Guess -from src.main.db_session import DatabaseSession - - -MEMBER_ID = 'member_id' -PLAY_ID = 'play_id' -GUESSED_NUMBER = 'guessed_number' -DIFFERENCE = 'difference' -MEMBER_NAME = 'member_name' - -class GuessDAO(): - db_string = None - session = None - Session = None - engine = None - - _database_session = None - - def __init__(self): - self._database_session = DatabaseSession() - - def insert(self, guess_info, allow_update=False): - session = self._database_session.get_or_create_session() - - guess = Guess( - member_id=guess_info[MEMBER_ID], - play_id = guess_info[PLAY_ID], - guessed_number = guess_info[GUESSED_NUMBER], - member_name = guess_info[MEMBER_NAME] - ) - - existing_guess = self.__convert_all__(session\ - .query(Guess)\ - .filter(Guess.member_id == guess_info[MEMBER_ID], Guess.play_id == guess_info[PLAY_ID])) - - if len(existing_guess) == 0: - session.add(guess) - session.commit() - return True - elif allow_update: - session\ - .query(Guess)\ - .filter(Guess.member_id == guess_info[MEMBER_ID], Guess.play_id == guess_info[PLAY_ID], Guess.member_name == guess_info[MEMBER_NAME])\ - .update({Guess.guessed_number: guess_info[GUESSED_NUMBER]}) - return True - else: - return False - - ''' - Converts the database object into a Dictionary, so that the database object is not passed out of the - datastore layer. - ''' - def __convert_all__(self, games): - converted_games = [] - for game in games: - game_dict = {} - for column in game.__dict__: - game_dict[column] = str(getattr(game, column)) - - converted_games.append(deepcopy(game_dict)) - - return converted_games - - def set_differences(self, pitch_value, play_id): - session = self._database_session.get_or_create_session() - games_to_update = self.__convert_all__(session.query(Guess).filter(Guess.play_id == play_id)) - - for game in games_to_update: - difference = self.calculate_difference(pitch_value, game[GUESSED_NUMBER]) - session.query(Guess).filter(Guess.member_id == game[MEMBER_ID], Guess.play_id == game[PLAY_ID]).update({Guess.difference: difference}) - - session.commit() - - def calculate_difference(self, pitch_value, guess_value): - pitched_number = int(pitch_value) - possible_value = abs(int(guess_value) - pitched_number) - - if possible_value > 500: - return 1000 - possible_value - else: - return possible_value - - def fetch_closest(self, num_to_fetch): - session = self._database_session.get_or_create_session() - - return self.__convert_all__( - session\ - .query(Guess)\ - .order_by(Guess.difference)\ - .limit(num_to_fetch) - ) - - def refresh(self): - self._database_session.__create_new_session__() # I know, I know. It's fine. - - def get_all_guesses_for_plays(self, play_ids): - session = self._database_session.get_or_create_session() - return self.__convert_all__( - session - .query(Guess) - .filter(Guess.play_id.in_(play_ids)) - ) - - def get_closest_on_play(self, play): - session = self._database_session.get_or_create_session() - - # TODO: Make this a MAX query for ties - converted_guesses = self.__convert_all__( - session - .query(Guess) - .filter(Guess.play_id == play) - .order_by(Guess.difference) - .limit(1) - ) - - if len(converted_guesses) > 1: - raise AssertionError("More than one best guess! Can't continue!") - elif len(converted_guesses) == 0: - return None - else: +from copy import deepcopy + +from src.main.database_module.database_classes.db_classes import Guess +from src.main.db_session import DatabaseSession + + +MEMBER_ID = 'member_id' +PLAY_ID = 'play_id' +GUESSED_NUMBER = 'guessed_number' +DIFFERENCE = 'difference' +MEMBER_NAME = 'member_name' + +class GuessDAO(): + db_string = None + session = None + Session = None + engine = None + + _database_session = None + + def __init__(self): + self._database_session = DatabaseSession() + + def insert(self, guess_info, allow_update=False): + session = self._database_session.get_or_create_session() + + guess = Guess( + member_id=guess_info[MEMBER_ID], + play_id = guess_info[PLAY_ID], + guessed_number = guess_info[GUESSED_NUMBER], + member_name = guess_info[MEMBER_NAME] + ) + + existing_guess = self.__convert_all__(session\ + .query(Guess)\ + .filter(Guess.member_id == guess_info[MEMBER_ID], Guess.play_id == guess_info[PLAY_ID])) + + if len(existing_guess) == 0: + session.add(guess) + session.commit() + return True + elif allow_update: + session\ + .query(Guess)\ + .filter(Guess.member_id == guess_info[MEMBER_ID], Guess.play_id == guess_info[PLAY_ID], Guess.member_name == guess_info[MEMBER_NAME])\ + .update({Guess.guessed_number: guess_info[GUESSED_NUMBER]}) + return True + else: + return False + + ''' + Converts the database object into a Dictionary, so that the database object is not passed out of the + datastore layer. + ''' + def __convert_all__(self, games): + converted_games = [] + for game in games: + game_dict = {} + for column in game.__dict__: + game_dict[column] = str(getattr(game, column)) + + converted_games.append(deepcopy(game_dict)) + + return converted_games + + def set_differences(self, pitch_value, play_id): + session = self._database_session.get_or_create_session() + games_to_update = self.__convert_all__(session.query(Guess).filter(Guess.play_id == play_id)) + + for game in games_to_update: + difference = self.calculate_difference(pitch_value, game[GUESSED_NUMBER]) + session.query(Guess).filter(Guess.member_id == game[MEMBER_ID], Guess.play_id == game[PLAY_ID]).update({Guess.difference: difference}) + + session.commit() + + def calculate_difference(self, pitch_value, guess_value): + pitched_number = int(pitch_value) + possible_value = abs(int(guess_value) - pitched_number) + + if possible_value > 500: + return 1000 - possible_value + else: + return possible_value + + def fetch_closest(self, num_to_fetch): + session = self._database_session.get_or_create_session() + + return self.__convert_all__( + session\ + .query(Guess)\ + .order_by(Guess.difference)\ + .limit(num_to_fetch) + ) + + def refresh(self): + self._database_session.__create_new_session__() # I know, I know. It's fine. + + def get_all_guesses_for_plays(self, play_ids): + session = self._database_session.get_or_create_session() + return self.__convert_all__( + session + .query(Guess) + .filter(Guess.play_id.in_(play_ids)) + ) + + def get_closest_on_play(self, play): + session = self._database_session.get_or_create_session() + + # TODO: Make this a MAX query for ties + converted_guesses = self.__convert_all__( + session + .query(Guess) + .filter(Guess.play_id == play) + .order_by(Guess.difference) + .limit(1) + ) + + if len(converted_guesses) > 1: + raise AssertionError("More than one best guess! Can't continue!") + elif len(converted_guesses) == 0: + return None + else: return converted_guesses[0] \ No newline at end of file diff --git a/src/main/database_module/play_dao.py b/src/main/database_module/play_dao.py index f035a87..2bcaf2a 100644 --- a/src/main/database_module/play_dao.py +++ b/src/main/database_module/play_dao.py @@ -1,96 +1,96 @@ -from copy import deepcopy -from sqlalchemy.sql.expression import and_ - -from src.main.db_session import DatabaseSession -from src.main.database_module.database_classes.db_classes import Play - -import datetime - -PLAY_ID = 'play_id' -PITCH_VALUE = 'pitch_value' -CREATION_DATE = 'creation_date' -SERVER_ID = 'server_id' - -class PlayDAO(): - db_string = None - session = None - Session = None - engine = None - _database_session = None - - def __init__(self): - self._database_session = DatabaseSession() - - def insert(self, play_info): - session = self._database_session.get_or_create_session() - - play = Play( - play_id = play_info[PLAY_ID], - pitch_value = play_info[PITCH_VALUE] if PITCH_VALUE in play_info else None, - creation_date = play_info[CREATION_DATE], - server_id = play_info[SERVER_ID] - ) - - session.add(play) - session.commit() - - def get_play_by_id(self, input_id): - session = self._database_session.get_or_create_session() - return self.__convert_all__(session.query(Play).filter(Play.play_id == input_id)) - - def get_all_plays_after(self, timestamp, input_server_id): - session = self._database_session.get_or_create_session() - return self.__convert_all__(session.query(Play).filter(and_(Play.server_id == str(input_server_id), Play.creation_date > timestamp))) - - def get_all_plays_on_server(self, input_server_id, earliest_timestamp): - session = self._database_session.get_or_create_session() - converted_datetime = datetime.datetime.fromtimestamp(earliest_timestamp / 1000.0) - - return self.__convert_all__(session.query(Play).filter(and_(Play.server_id == str(input_server_id), Play.creation_date > converted_datetime))) - - ''' - Checks to see if there is a play that is currently active or not - ''' - def is_active_play(self, server_id): - return self.get_active_play(server_id) != None - - def get_active_play(self, input_server_id): - session = self._database_session.get_or_create_session() - plays = self.__convert_all__(session.query(Play).filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id)))) - - if len(plays) > 1: - raise AssertionError("More than one active play! Can't continue!") - elif len(plays) == 0: - return None - else: - return plays[0] - - def resolve_play(self, input_pitch, input_server_id): - session = self._database_session.get_or_create_session() - active_id = self.get_active_play(input_server_id) - - session\ - .query(Play)\ - .filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id)))\ - .update({Play.pitch_value: input_pitch}) - session.commit() - - return active_id - - def refresh(self): - self._database_session.__create_new_session__() # I know, I know. It's fine. - - ''' - Converts the database object into a Dictionary, so that the database object is not passed out of the - datastore layer. - ''' - def __convert_all__(self, plays): - converted_plays = [] - for play in plays: - play_dict = {} - for column in play.__dict__: - play_dict[column] = str(getattr(play, column)) - - converted_plays.append(deepcopy(play_dict)) - - return converted_plays +from copy import deepcopy +from sqlalchemy.sql.expression import and_ + +from src.main.db_session import DatabaseSession +from src.main.database_module.database_classes.db_classes import Play + +import datetime + +PLAY_ID = 'play_id' +PITCH_VALUE = 'pitch_value' +CREATION_DATE = 'creation_date' +SERVER_ID = 'server_id' + +class PlayDAO(): + db_string = None + session = None + Session = None + engine = None + _database_session = None + + def __init__(self): + self._database_session = DatabaseSession() + + def insert(self, play_info): + session = self._database_session.get_or_create_session() + + play = Play( + play_id = play_info[PLAY_ID], + pitch_value = play_info[PITCH_VALUE] if PITCH_VALUE in play_info else None, + creation_date = play_info[CREATION_DATE], + server_id = play_info[SERVER_ID] + ) + + session.add(play) + session.commit() + + def get_play_by_id(self, input_id): + session = self._database_session.get_or_create_session() + return self.__convert_all__(session.query(Play).filter(Play.play_id == input_id)) + + def get_all_plays_after(self, timestamp, input_server_id): + session = self._database_session.get_or_create_session() + return self.__convert_all__(session.query(Play).filter(and_(Play.server_id == str(input_server_id), Play.creation_date > timestamp))) + + def get_all_plays_on_server(self, input_server_id, earliest_timestamp): + session = self._database_session.get_or_create_session() + converted_datetime = datetime.datetime.fromtimestamp(earliest_timestamp / 1000.0) + + return self.__convert_all__(session.query(Play).filter(and_(Play.server_id == str(input_server_id), Play.creation_date > converted_datetime))) + + ''' + Checks to see if there is a play that is currently active or not + ''' + def is_active_play(self, server_id): + return self.get_active_play(server_id) != None + + def get_active_play(self, input_server_id): + session = self._database_session.get_or_create_session() + plays = self.__convert_all__(session.query(Play).filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id)))) + + if len(plays) > 1: + raise AssertionError("More than one active play! Can't continue!") + elif len(plays) == 0: + return None + else: + return plays[0] + + def resolve_play(self, input_pitch, input_server_id): + session = self._database_session.get_or_create_session() + active_id = self.get_active_play(input_server_id) + + session\ + .query(Play)\ + .filter(and_(Play.pitch_value == None, Play.server_id == str(input_server_id)))\ + .update({Play.pitch_value: input_pitch}) + session.commit() + + return active_id + + def refresh(self): + self._database_session.__create_new_session__() # I know, I know. It's fine. + + ''' + Converts the database object into a Dictionary, so that the database object is not passed out of the + datastore layer. + ''' + def __convert_all__(self, plays): + converted_plays = [] + for play in plays: + play_dict = {} + for column in play.__dict__: + play_dict[column] = str(getattr(play, column)) + + converted_plays.append(deepcopy(play_dict)) + + return converted_plays diff --git a/src/main/db_session.py b/src/main/db_session.py index 53b5192..e0c65b6 100644 --- a/src/main/db_session.py +++ b/src/main/db_session.py @@ -1,49 +1,49 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -import sys -import sqlite3 -sys.path.append('../../../../../src') - -from src.main.configs import Configs, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_HOST, DATABASE_NAME - -''' -Stores a database session for use throughout the application. Must be initialized at startup before any database calls -are made and AFTER the Configurations are setup. - -This shouldn't need to be touched after startup. To use, see the sqlalchemy docs...or just start by calling Session() -and then use it to handle the necessary CRUD operations. - -You should NOT instantiate this in any method except the main application runner -''' -class DatabaseSession(): - _session = None - - def __init__(self): - self.__create_new_session__() - - def __create_new_session__(self): - if self._session is not None: - self._session.close() - - config_map = Configs.configs - db_string = self._pgsql_conn_string_(config_map) - Session = sessionmaker(create_engine(db_string)) - self._session = Session() - - return self._session - - def get_or_create_session(self): - try: - self._session.connection() - return self._session - except: # The linter can scream all it wants, this makes sense. If it's this broke, we want a new one anyway. - return self.__create_new_session__() - - # Look, this kinda sucks. But it's for fun and friends and I'm doing it quick and dirty. - def _pgsql_conn_string_(self, config_map): - return 'postgresql://%s:%s@%s/%s' % \ - (config_map[DATABASE_USERNAME], config_map[DATABASE_PASSWORD], config_map[DATABASE_HOST], config_map[DATABASE_NAME]) - - def _sqlite_conn_string(self, config_map): +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +import sys +import sqlite3 +sys.path.append('../../../../../../src') + +from src.main.configs import Configs, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_HOST, DATABASE_NAME + +''' +Stores a database session for use throughout the application. Must be initialized at startup before any database calls +are made and AFTER the Configurations are setup. + +This shouldn't need to be touched after startup. To use, see the sqlalchemy docs...or just start by calling Session() +and then use it to handle the necessary CRUD operations. + +You should NOT instantiate this in any method except the main application runner +''' +class DatabaseSession(): + _session = None + + def __init__(self): + self.__create_new_session__() + + def __create_new_session__(self): + if self._session is not None: + self._session.close() + + config_map = Configs.configs + db_string = self._pgsql_conn_string_(config_map) + Session = sessionmaker(create_engine(db_string)) + self._session = Session() + + return self._session + + def get_or_create_session(self): + try: + self._session.connection() + return self._session + except: # The linter can scream all it wants, this makes sense. If it's this broke, we want a new one anyway. + return self.__create_new_session__() + + # Look, this kinda sucks. But it's for fun and friends and I'm doing it quick and dirty. + def _pgsql_conn_string_(self, config_map): + return 'postgresql://%s:%s@%s/%s' % \ + (config_map[DATABASE_USERNAME], config_map[DATABASE_PASSWORD], config_map[DATABASE_HOST], config_map[DATABASE_NAME]) + + def _sqlite_conn_string(self, config_map): return "sqlite:///ghostball.db" \ No newline at end of file diff --git a/src/main/discord_module/bot_runner.py b/src/main/discord_module/bot_runner.py index 7955800..7471a2c 100644 --- a/src/main/discord_module/bot_runner.py +++ b/src/main/discord_module/bot_runner.py @@ -1,220 +1,220 @@ -import sys - -import discord -from discord.utils import get - -import uuid -import datetime -import dateparser - -from src.main.configs import Configs -from src.main.database_module.guess_dao import GuessDAO, GUESSED_NUMBER, MEMBER_ID, MEMBER_NAME, DIFFERENCE -from src.main.services.points_service import PointsService -from src.main.database_module.play_dao import PlayDAO, PLAY_ID, CREATION_DATE, SERVER_ID -from src.main.db_session import DatabaseSession -from src.main.discord_module.leaderboard_config import LeaderboardConfig - -play_dao = None -guess_dao = None -points_service = PointsService() -bot = discord.Client() - - -@bot.event -async def on_ready(): - print('Logged in as') - print(bot.user.name) - print(bot.user.id) - print('------') - - -@bot.event -async def on_message(message): - if message.author == bot.user: - return - - content = message.content - server_id = message.guild.id - - ''' - Sets up the next set of guesses. - ''' - if content.startswith('!ghostball'): - if play_dao.is_active_play(server_id): - await message.channel.send("There's already an active play. Could you close that one first, please?") - else: - generated_play_id = uuid.uuid4() - play_object = {PLAY_ID: generated_play_id, CREATION_DATE: datetime.datetime.now(), SERVER_ID: server_id} - play_dao.insert(play_object) - - await message.channel.send("@flappy ball, pitch is in! Send me your guesses with a !guess command.") - - if content.startswith("!guess"): - guess_value = None - try: - guess_value = __parse_guess__(content) - except ValueError: - await message.channel.send("That number is not between 1 and 1000. We're still in MLN so don't try to cheat.") - return - - if guess_value is None: - await message.channel.send("I don't know what you did but I'm pretty sure you're tyring to break the bot so please stop.") - return - - if not play_dao.is_active_play(server_id): - await message.channel.send("Hey, there's no active play! Start one up first with !ghostball.") - else: - play = play_dao.get_active_play(server_id) - guess_object = {PLAY_ID: play['play_id'], - MEMBER_ID: str(message.author.id), - GUESSED_NUMBER: guess_value, - MEMBER_NAME: str(message.author.name)} - - if guess_dao.insert(guess_object, allow_update=True): - await message.add_reaction(emoji="\N{THUMBS UP SIGN}") - - # Closes off the active play to be ready for the next set - if content.startswith('!resolve'): - # try: - pitch_number, batter_id, batter_guess, has_batter = __parse_resolve_play__(content) - if args is None: - await message.channel.send("Hey " + "<@" + str(message.author.id) + ">, I'm not sure what you meant. " - "You need real, numeric, values for this command to work. " - "Use !resolve " - " and try again.") - - # Check if we have an active play - if not play_dao.is_active_play(server_id): - await message.channel.send("You confused me. There's no active play so I have nothing to close!") - else: - if has_batter: - referenced_member_id = batter_id[3:-1] - play = play_dao.get_active_play(server_id) - guess_object = {PLAY_ID: play['play_id'], - MEMBER_ID: str(referenced_member_id), - GUESSED_NUMBER: batter_guess, - MEMBER_NAME: bot.get_user(int(referenced_member_id)).name} - - guess_dao.insert(guess_object, True) - - play = play_dao.resolve_play(pitch_number, server_id) - guess_dao.set_differences(pitch_number, play['play_id']) - guesses = points_service.fetch_sorted_guesses_by_play(guess_dao, play['play_id']) - - response_message = "Closed this play! Here are the results:\n" - response_message += "PLAYER --- DIFFERENCE --- POINTS GAINED\n" - for guess in guesses: - response_message += guess[1] + " --- " + str(guess[2]) + " --- " + str(guess[3]) + "\n" - - if len(guesses) < 2: - response_message += "Not enough people participated to give best and worst awards. Stop being lazy." - - else: - response_message += "\nCongrats to <@" + str(guesses[0][0]) + "> for being the closest! \n" - response_message += "And tell <@" + str(guesses[-1][0]) + "> they suck." - - await message.channel.send(response_message) - - if content.startswith("!points"): - try: - timestamp = __parse_points_message__(content) - except: - await message.channel.send("You gave me a timestamp that was so bad, the best date handling library in the" - " world of software couldn't figure out what you meant. That's...impressive. Now" - " fix your shit and try again.") - return - - points_by_user = points_service.fetch_points(timestamp, server_id, play_dao, guess_dao) - response = "Here are the top guessers by points as per your request..." - for user in points_by_user: - if str(user[2]) != '0': - response += "\n" + str(user[1]) + " : " + str(user[2]) - - await message.channel.send(response) - - # Refresh Postgres connection - if content.startswith('!restart'): - play_dao.refresh() - guess_dao.refresh() - - if content.startswith('!help'): - help_message = __get_help_message__() - recipient = await bot.fetch_user(message.author.id) - await recipient.send(help_message) - - -def __get_help_message__(): - # Start message with person who asked for help - help_message = "Hey! I can be instructed to do any number of things! Use the following commands: \n" \ - "!guess --- This will add your guess to the currently active play. " \ - "I will give you a thumbs up if everything worked!\n" \ - "!ghostball --- Starts a new play. I'll let you know if this didn't work for some reason!\n" \ - "!help --- You just asked for this. If you ask for it again, I'll repeat myself.\n" \ - "!resolve --- " \ - "Uses the pitch number and real swing number to figure out who was closest and ends the active play." \ - "If you include the batter and their swing number, they will get credit for how well they did!\n" \ - "!points Fetches all plays since your requested time, or the beginning of the unvierse " \ - "if none given. Will currently always dump all players - top X coming soon...\n" \ - "!restart --- If the bot looks broken, this will take a shot at fixing it. It won't answer your commands " \ - "for about 3 seconds after you do this! BE CAREFUL! ONLY USE IN AN EMERGENCY!\n" \ - "\n" - - return help_message - - -def __parse_leaderboard_message__(message_content): - return LeaderboardConfig(message_content) - - -def __parse_points_message__(message_content): - pieces = message_content.split(' ') - - if len(pieces) > 1: - try: - timestamp = dateparser.parse(pieces[1]) - except: - raise RuntimeError("Unable to parse timestamp!") - else: - timestamp = dateparser.parse("1970-01-01") - - return timestamp - - -def __parse_guess__(message_content): - pieces = message_content.split(' ') - try: - guess_value = pieces[1] - guess_as_int = int(guess_value) - if guess_as_int > 1000 or guess_as_int < 1: - raise ValueError("Number not between 1 and 1000 inclusive") - else: - return guess_value - except TypeError: - return None - - -def __parse_resolve_play__(message_content): - pieces = message_content.split() - try: - if len(pieces) == 2: - return pieces[1], None, None, False - elif len(pieces) == 4: - return pieces[1], pieces[2], pieces[3], True - else: - print("Illegal resolution command") - return None, None - except TypeError: - return None, None - - -if __name__ == '__main__': - args = sys.argv - token = args[1] - file_path = args[2] - - configs = Configs(file_path) - databaseSession = DatabaseSession() - - play_dao = PlayDAO() - guess_dao = GuessDAO() - bot.run(token) +import sys + +import discord +from discord.utils import get + +import uuid +import datetime +import dateparser + +from src.main.configs import Configs +from src.main.database_module.guess_dao import GuessDAO, GUESSED_NUMBER, MEMBER_ID, MEMBER_NAME, DIFFERENCE +from src.main.services.points_service import PointsService +from src.main.database_module.play_dao import PlayDAO, PLAY_ID, CREATION_DATE, SERVER_ID +from src.main.db_session import DatabaseSession +from src.main.discord_module.leaderboard_config import LeaderboardConfig + +play_dao = None +guess_dao = None +points_service = PointsService() +bot = discord.Client() + + +@bot.event +async def on_ready(): + print('Logged in as') + print(bot.user.name) + print(bot.user.id) + print('------') + + +@bot.event +async def on_message(message): + if message.author == bot.user: + return + + content = message.content + server_id = message.guild.id + + ''' + Sets up the next set of guesses. + ''' + if content.startswith('!ghostball'): + if play_dao.is_active_play(server_id): + await message.channel.send("There's already an active play. Could you close that one first, please?") + else: + generated_play_id = uuid.uuid4() + play_object = {PLAY_ID: generated_play_id, CREATION_DATE: datetime.datetime.now(), SERVER_ID: server_id} + play_dao.insert(play_object) + + await message.channel.send("@flappy ball, pitch is in! Send me your guesses with a !guess command.") + + if content.startswith("!guess"): + guess_value = None + try: + guess_value = __parse_guess__(content) + except ValueError: + await message.channel.send("That number is not between 1 and 1000. We're still in MLN so don't try to cheat.") + return + + if guess_value is None: + await message.channel.send("I don't know what you did but I'm pretty sure you're tyring to break the bot so please stop.") + return + + if not play_dao.is_active_play(server_id): + await message.channel.send("Hey, there's no active play! Start one up first with !ghostball.") + else: + play = play_dao.get_active_play(server_id) + guess_object = {PLAY_ID: play['play_id'], + MEMBER_ID: str(message.author.id), + GUESSED_NUMBER: guess_value, + MEMBER_NAME: str(message.author.name)} + + if guess_dao.insert(guess_object, allow_update=True): + await message.add_reaction(emoji="\N{THUMBS UP SIGN}") + + # Closes off the active play to be ready for the next set + if content.startswith('!resolve'): + # try: + pitch_number, batter_id, batter_guess, has_batter = __parse_resolve_play__(content) + if args is None: + await message.channel.send("Hey " + "<@" + str(message.author.id) + ">, I'm not sure what you meant. " + "You need real, numeric, values for this command to work. " + "Use !resolve " + " and try again.") + + # Check if we have an active play + if not play_dao.is_active_play(server_id): + await message.channel.send("You confused me. There's no active play so I have nothing to close!") + else: + if has_batter: + referenced_member_id = batter_id[3:-1] + play = play_dao.get_active_play(server_id) + guess_object = {PLAY_ID: play['play_id'], + MEMBER_ID: str(referenced_member_id), + GUESSED_NUMBER: batter_guess, + MEMBER_NAME: bot.get_user(int(referenced_member_id)).name} + + guess_dao.insert(guess_object, True) + + play = play_dao.resolve_play(pitch_number, server_id) + guess_dao.set_differences(pitch_number, play['play_id']) + guesses = points_service.fetch_sorted_guesses_by_play(guess_dao, play['play_id']) + + response_message = "Closed this play! Here are the results:\n" + response_message += "PLAYER --- DIFFERENCE --- POINTS GAINED\n" + for guess in guesses: + response_message += guess[1] + " --- " + str(guess[2]) + " --- " + str(guess[3]) + "\n" + + if len(guesses) < 2: + response_message += "Not enough people participated to give best and worst awards. Stop being lazy." + + else: + response_message += "\nCongrats to <@" + str(guesses[0][0]) + "> for being the closest! \n" + response_message += "And tell <@" + str(guesses[-1][0]) + "> they suck." + + await message.channel.send(response_message) + + if content.startswith("!points"): + try: + timestamp = __parse_points_message__(content) + except: + await message.channel.send("You gave me a timestamp that was so bad, the best date handling library in the" + " world of software couldn't figure out what you meant. That's...impressive. Now" + " fix your shit and try again.") + return + + points_by_user = points_service.fetch_points(timestamp, server_id, play_dao, guess_dao) + response = "Here are the top guessers by points as per your request..." + for user in points_by_user: + if str(user[2]) != '0': + response += "\n" + str(user[1]) + " : " + str(user[2]) + + await message.channel.send(response) + + # Refresh Postgres connection + if content.startswith('!restart'): + play_dao.refresh() + guess_dao.refresh() + + if content.startswith('!help'): + help_message = __get_help_message__() + recipient = await bot.fetch_user(message.author.id) + await recipient.send(help_message) + + +def __get_help_message__(): + # Start message with person who asked for help + help_message = "Hey! I can be instructed to do any number of things! Use the following commands: \n" \ + "!guess --- This will add your guess to the currently active play. " \ + "I will give you a thumbs up if everything worked!\n" \ + "!ghostball --- Starts a new play. I'll let you know if this didn't work for some reason!\n" \ + "!help --- You just asked for this. If you ask for it again, I'll repeat myself.\n" \ + "!resolve --- " \ + "Uses the pitch number and real swing number to figure out who was closest and ends the active play." \ + "If you include the batter and their swing number, they will get credit for how well they did!\n" \ + "!points Fetches all plays since your requested time, or the beginning of the unvierse " \ + "if none given. Will currently always dump all players - top X coming soon...\n" \ + "!restart --- If the bot looks broken, this will take a shot at fixing it. It won't answer your commands " \ + "for about 3 seconds after you do this! BE CAREFUL! ONLY USE IN AN EMERGENCY!\n" \ + "\n" + + return help_message + + +def __parse_leaderboard_message__(message_content): + return LeaderboardConfig(message_content) + + +def __parse_points_message__(message_content): + pieces = message_content.split(' ') + + if len(pieces) > 1: + try: + timestamp = dateparser.parse(pieces[1]) + except: + raise RuntimeError("Unable to parse timestamp!") + else: + timestamp = dateparser.parse("1970-01-01") + + return timestamp + + +def __parse_guess__(message_content): + pieces = message_content.split(' ') + try: + guess_value = pieces[1] + guess_as_int = int(guess_value) + if guess_as_int > 1000 or guess_as_int < 1: + raise ValueError("Number not between 1 and 1000 inclusive") + else: + return guess_value + except TypeError: + return None + + +def __parse_resolve_play__(message_content): + pieces = message_content.split() + try: + if len(pieces) == 2: + return pieces[1], None, None, False + elif len(pieces) == 4: + return pieces[1], pieces[2], pieces[3], True + else: + print("Illegal resolution command") + return None, None + except TypeError: + return None, None + + +if __name__ == '__main__': + args = sys.argv + token = args[1] + file_path = args[2] + + configs = Configs(file_path) + databaseSession = DatabaseSession() + + play_dao = PlayDAO() + guess_dao = GuessDAO() + bot.run(token) diff --git a/src/main/discord_module/leaderboard_config.py b/src/main/discord_module/leaderboard_config.py index 1fc6b44..ff38fd0 100644 --- a/src/main/discord_module/leaderboard_config.py +++ b/src/main/discord_module/leaderboard_config.py @@ -1,15 +1,15 @@ -class LeaderboardConfig(): - closest = True - def __init__(self, message_content): - pieces = message_content.split(' ') - if len(pieces) == 1: - return - - if pieces[1] == 'average': - self.closest = False - - def should_sort_by_pure_closest(self): - return self.closest - - def should_sort_by_best_average(self): - return not self.closest +class LeaderboardConfig(): + closest = True + def __init__(self, message_content): + pieces = message_content.split(' ') + if len(pieces) == 1: + return + + if pieces[1] == 'average': + self.closest = False + + def should_sort_by_pure_closest(self): + return self.closest + + def should_sort_by_best_average(self): + return not self.closest diff --git a/src/main/services/points_service.py b/src/main/services/points_service.py index 9aeb77d..9093a8f 100644 --- a/src/main/services/points_service.py +++ b/src/main/services/points_service.py @@ -1,48 +1,48 @@ - - -class PointsService(): - _point_table = [(5,25),(25, 75), (75, 50), (150, 25)] - - def fetch_points(self, timestamp, server_id, play_dao, guess_dao): - plays = play_dao.get_all_plays_after(timestamp, server_id) - all_guesses = guess_dao.get_all_guesses_for_plays(x['play_id'] for x in plays) - - # Build a dictionary of each member and their total points - totals_by_player = {} - for guess in all_guesses: - if guess['member_id'] in totals_by_player: - totals_by_player[guess['member_id']]['points'] += self.__get_points_for_diff__(guess['difference']) - else: - totals_by_player[guess['member_id']] = {} - totals_by_player[guess['member_id']]['points'] = self.__get_points_for_diff__(guess['difference']) - totals_by_player[guess['member_id']]['member_name'] = guess['member_name'] - - # And now pull those numbers out into a list and sort them - sorted_players = [] - for player in totals_by_player: - sorted_players.append([player, - totals_by_player[player]['member_name'], - totals_by_player[player]['points']]) - - sorted_players.sort(key=lambda x: x[2], reverse=True) - return sorted_players - - def fetch_sorted_guesses_by_play(self, guess_dao, play_id): - all_guesses = guess_dao.get_all_guesses_for_plays([play_id]) - player_list = [] - for guess in all_guesses: - player_list.append([guess['member_id'], guess['member_name'], int(guess['difference']), self.__get_points_for_diff__(guess['difference'])]) - - player_list.sort(key=lambda x: x[2]) - return player_list - - # Iterates through the point table, which we assume is sorted, and gets the points - def __get_points_for_diff__(self, diff): - if diff == 'None': - return 0 - - for i in range(0, len(self._point_table)): - if int(diff) < self._point_table[i][0]: - return self._point_table[i][1] - - return 0 + + +class PointsService(): + _point_table = [(5,25),(25, 75), (75, 50), (150, 25)] + + def fetch_points(self, timestamp, server_id, play_dao, guess_dao): + plays = play_dao.get_all_plays_after(timestamp, server_id) + all_guesses = guess_dao.get_all_guesses_for_plays(x['play_id'] for x in plays) + + # Build a dictionary of each member and their total points + totals_by_player = {} + for guess in all_guesses: + if guess['member_id'] in totals_by_player: + totals_by_player[guess['member_id']]['points'] += self.__get_points_for_diff__(guess['difference']) + else: + totals_by_player[guess['member_id']] = {} + totals_by_player[guess['member_id']]['points'] = self.__get_points_for_diff__(guess['difference']) + totals_by_player[guess['member_id']]['member_name'] = guess['member_name'] + + # And now pull those numbers out into a list and sort them + sorted_players = [] + for player in totals_by_player: + sorted_players.append([player, + totals_by_player[player]['member_name'], + totals_by_player[player]['points']]) + + sorted_players.sort(key=lambda x: x[2], reverse=True) + return sorted_players + + def fetch_sorted_guesses_by_play(self, guess_dao, play_id): + all_guesses = guess_dao.get_all_guesses_for_plays([play_id]) + player_list = [] + for guess in all_guesses: + player_list.append([guess['member_id'], guess['member_name'], int(guess['difference']), self.__get_points_for_diff__(guess['difference'])]) + + player_list.sort(key=lambda x: x[2]) + return player_list + + # Iterates through the point table, which we assume is sorted, and gets the points + def __get_points_for_diff__(self, diff): + if diff == 'None': + return 0 + + for i in range(0, len(self._point_table)): + if int(diff) < self._point_table[i][0]: + return self._point_table[i][1] + + return 0