296 lines
10 KiB
Python
296 lines
10 KiB
Python
"""
|
|
The helper core of the raid-callouts bot.
|
|
This module(s) will contain all of the helper functions for the bot
|
|
|
|
@author: Gabriella 'contrastellar' Agathon
|
|
"""
|
|
|
|
import psycopg2
|
|
import psycopg2.extensions
|
|
from configparser import ConfigParser
|
|
import datetime
|
|
|
|
|
|
def load_config(filename='database.ini', section='postgresql'):
|
|
"""
|
|
Args:
|
|
filename (str, optional): filename for the ini file. Defaults to 'database.ini'.
|
|
section (str, optional): defines the section for the ini file to read from. Defaults to 'postgresql'.
|
|
|
|
Raises:
|
|
Exception: Will raise an exception if the postgresql section is not found in the ini file.
|
|
|
|
Returns:
|
|
dict: A dictionary containing the parsed values from the ini file, to be used for the database connection.
|
|
"""
|
|
parser = ConfigParser()
|
|
parser.read(filename)
|
|
|
|
# get section, default is postgresql
|
|
config = {}
|
|
if parser.has_section(section):
|
|
params = parser.items(section)
|
|
for param in params:
|
|
config[param[0]] = param[1]
|
|
|
|
else:
|
|
raise Exception('Section {0} not found in the {1} file'.format(section, filename))
|
|
|
|
return config
|
|
|
|
def connect_config(config) -> psycopg2.extensions.connection:
|
|
""" Connect to the PostgreSQL database server """
|
|
try:
|
|
# connecting to the PostgreSQL server
|
|
with psycopg2.connect(**config) as conn:
|
|
print('Connected to the PostgreSQL server.')
|
|
return conn
|
|
|
|
except (psycopg2.DatabaseError, Exception) as error:
|
|
print(error)
|
|
|
|
finally:
|
|
if conn is None:
|
|
raise psycopg2.DatabaseError('Failed to connect to the PostgreSQL database')
|
|
|
|
class DateTimeError(Exception):
|
|
def __init__(self, *args):
|
|
super().__init__(*args)
|
|
|
|
|
|
class DBHelper():
|
|
"""
|
|
The helper class for the raid-callouts bot.
|
|
This class will contain all of the helper functions for the bot
|
|
"""
|
|
|
|
_config: dict
|
|
__CONN: psycopg2.extensions.connection = None
|
|
is_procedure_queued: bool = False
|
|
is_unregister_queued: bool = False
|
|
|
|
def __init__(self, filename = 'database.ini', section = 'postgresql') -> None:
|
|
self._config = load_config(filename=filename, section=section)
|
|
|
|
|
|
def __del__(self):
|
|
"""
|
|
Destructor for the DBHelper class
|
|
No need to do anything here
|
|
"""
|
|
# self.__CONN.close()
|
|
pass
|
|
|
|
|
|
def query_callouts(self, days: int) -> list:
|
|
"""This function will query the database for the callouts for the next X days, where X is defined by the days parameter.
|
|
|
|
Args:
|
|
days int: number of days in the future to query for callouts
|
|
|
|
Returns:
|
|
list: list of users + their callouts for the next X days
|
|
"""
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
# Weird query, but it grabs the callouts from the last day to the next X days.
|
|
cursor.execute(f"SELECT * FROM newcallouts WHERE date >= NOW() - INTERVAL '1 day' AND date <= NOW() + INTERVAL '{days} days' ORDER BY date ASC;")
|
|
self.__CONN.commit()
|
|
|
|
return cursor.fetchall()
|
|
|
|
def query_self_callouts(self, user_id: int, days: int = 365):
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
cursor.execute(f"SELECT * FROM newcallouts WHERE date >= NOW() - INTERVAL '1 day' AND date <= NOW() + INTERVAL '{days} days' AND user_id = {user_id} ORDER BY date ASC;")
|
|
self.__CONN.commit()
|
|
|
|
return cursor.fetchall()
|
|
|
|
|
|
def add_callout(self, user_id: int, callout: datetime.date, reason: str, nickname: str, char_name: str, potential_fill: str) -> None:
|
|
"""Add a callout to the database
|
|
|
|
Args:
|
|
user_id (int): the Discord UUID of the user adding things to the db
|
|
callout (datetime.date): The day of the callout
|
|
reason (str): The reason of the callout
|
|
nickname (str): The server(guild) nickname of the user who is making the callout
|
|
char_name (str): The character name (as supplied from registration) of the user inserting a callout
|
|
"""
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
cursor.execute("INSERT INTO newcallouts (user_id, date, reason, nickname, charname, fill) VALUES (%s, %s, %s, %s, %s, %s)", (user_id, callout, reason, nickname, char_name, potential_fill))
|
|
self.__CONN.commit()
|
|
|
|
return
|
|
|
|
|
|
def remove_callout(self, user_id: int, callout: datetime.date) -> None:
|
|
"""Remove a callout based on user + date, which form the primary key in the db
|
|
|
|
Args:
|
|
user_id (int): The Discord UUID of the user removing something from the db
|
|
callout (datetime.datetime): The date of the callout
|
|
"""
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
|
|
cursor.execute("DELETE FROM newcallouts WHERE user_id = %s AND date = %s", (user_id, callout))
|
|
self.__CONN.commit()
|
|
|
|
return
|
|
|
|
|
|
def formatted_list_of_callouts(self, callouts: list) -> str:
|
|
"""Format the python list of callouts.
|
|
|
|
Args:
|
|
callouts (list): The list that needs to be formatted
|
|
|
|
Returns:
|
|
str: The formatted list
|
|
"""
|
|
length = len(callouts)
|
|
output: str = ''
|
|
|
|
# Quick and dirty way to say that there were no callouts found during the query
|
|
if length == 0:
|
|
return 'No callouts found for the requested timeframe'
|
|
|
|
for entry in callouts:
|
|
|
|
# this is a bit wonky, but we take the known constant width of each entry (4 columns)
|
|
# then we use python's range function to turn "item" into an interator
|
|
# Then we do some funky logic on the entry that we're iterating over
|
|
# in order to get the proper formatting
|
|
for item in range(5):
|
|
if item == 0:
|
|
# skip discord user ID always
|
|
continue
|
|
|
|
elif item == 1:
|
|
# handles the date displaying logic
|
|
if datetime.date.today() == entry[1]:
|
|
output += '**TODAY** • '
|
|
else:
|
|
output += f'**{entry[1]}** • '
|
|
|
|
elif item == 2:
|
|
# in the database, this is actually the "reason" place
|
|
# instead of doing that, we call the last column's value
|
|
# which is the char name
|
|
# this was requested by Yasu
|
|
output += "**" + entry[4] + '** • '
|
|
|
|
elif item == 3:
|
|
# Finally add the reason for the user's callout
|
|
# two line breaks as Yasu requested
|
|
output += entry[2] + ' '
|
|
|
|
elif item == 4:
|
|
if entry[5] is not None:
|
|
output += f'• potential fill -- {entry[5]}\n--\n'
|
|
else:
|
|
output += '\n--\n'
|
|
|
|
|
|
output += "END OF MESSAGE"
|
|
return output
|
|
|
|
|
|
def format_list_of_callouts(self, callouts: list) -> str:
|
|
"""Format the python list of callouts.
|
|
|
|
Args:
|
|
callouts (list): The list that needs to be formatted
|
|
|
|
Returns:
|
|
str: The formatted list
|
|
"""
|
|
return self.formatted_list_of_callouts(callouts=callouts)
|
|
|
|
|
|
def register_char_name(self, uid: int, char_name: str) -> None:
|
|
""" allows users to register their character name with the bot, allowing silly nicknames to be used independent of their
|
|
character's name
|
|
|
|
Arguments:
|
|
uid -- Discord User ID of the user to be registered
|
|
char_name -- User-supplied character name, to be inserted into the table
|
|
"""
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
|
|
cursor.execute("INSERT INTO charnames (uid, charname) VALUES (%s, %s)", (uid, char_name))
|
|
|
|
self.__CONN.commit()
|
|
|
|
return
|
|
|
|
|
|
def return_char_name(self, uid: int) -> str:
|
|
"""Utility method to return the character name based on a specific discord ID
|
|
|
|
Arguments:
|
|
uid -- Discord User ID of the user to be queried
|
|
|
|
Returns:
|
|
String; either character name or empty.
|
|
"""
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
|
|
# was getting weird index error on this line due to tuples, so we're using an f-string
|
|
cursor.execute(f"SELECT charname FROM charnames WHERE uid = {uid}")
|
|
output: str = ""
|
|
try:
|
|
output = cursor.fetchone()[0]
|
|
except TypeError:
|
|
return ""
|
|
else:
|
|
return output
|
|
|
|
|
|
def remove_registration(self, uid: int, isOkay: bool) -> None:
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
|
|
# need to remove all callouts!
|
|
cursor.execute(f"DELETE FROM newcallouts WHERE user_id = {uid}")
|
|
|
|
cursor.execute(f"DELETE FROM charnames WHERE uid = {uid}")
|
|
return
|
|
|
|
|
|
def number_affected_in_cleanup(self) -> int:
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
cursor = self.__CONN.cursor()
|
|
cursor.execute(f"SELECT count(*) FROM newcallouts WHERE date < NOW();")
|
|
|
|
return cursor.fetchone()[0]
|
|
|
|
|
|
def call_cleanup(self, is_okay: bool) -> int:
|
|
|
|
number_to_be_affected = self.number_affected_in_cleanup()
|
|
|
|
if not is_okay:
|
|
raise Exception("Not queued properly!")
|
|
|
|
self.__CONN = connect_config(self._config)
|
|
self.__CONN.autocommit = True
|
|
|
|
cursor = self.__CONN.cursor()
|
|
cursor.execute(f"CALL cleanup();")
|
|
print("Cleanup was called!")
|
|
return number_to_be_affected
|