diff --git a/README.md b/README.md index 2c3b5c1..a7270e7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ cogs made for our Royal butt. ## Our Cogs: +- [FitnessCog](./fitnessCog) - [RecruitmentCog](./recruitmentCog/) - [ReginaldCog](./reginaldCog/) - [TrafficCog](./trafficCog) diff --git a/fitnessCog/__init__.py b/fitnessCog/__init__.py new file mode 100644 index 0000000..daa3854 --- /dev/null +++ b/fitnessCog/__init__.py @@ -0,0 +1,6 @@ +from redbot.core.bot import Red +from .fitness import FitnessCog + +async def setup(bot: Red): + cog = FitnessCog(bot) + await bot.add_cog(cog) \ No newline at end of file diff --git a/fitnessCog/activities/climb.json b/fitnessCog/activities/climb.json new file mode 100644 index 0000000..b806ffb --- /dev/null +++ b/fitnessCog/activities/climb.json @@ -0,0 +1,48 @@ +{ + "convertable": true, + "units": { + "meter": { + "factor": 1, + "aliases": [ + "meter", + "meters", + "m", + "m." + ] + }, + "kilometer": { + "factor": 1000, + "aliases": [ + "kilometer", + "kilometers", + "km", + "km." + ] + }, + "foot": { + "factor": 0.3048, + "aliases": [ + "foot", + "feet", + "ft", + "ft." + ] + }, + "mile": { + "factor": 1609.344, + "aliases": [ + "mile", + "miles", + "mi", + "mi." + ] + }, + "step": { + "factor": 0.18, + "aliases": [ + "step", + "steps" + ] + } + } +} diff --git a/fitnessCog/activities/pushups.json b/fitnessCog/activities/pushups.json new file mode 100644 index 0000000..e26c057 --- /dev/null +++ b/fitnessCog/activities/pushups.json @@ -0,0 +1,3 @@ +{ + "convertable": false +} \ No newline at end of file diff --git a/fitnessCog/activity_logger.py b/fitnessCog/activity_logger.py new file mode 100644 index 0000000..f409dd7 --- /dev/null +++ b/fitnessCog/activity_logger.py @@ -0,0 +1,119 @@ +import json +import csv +import os + + +def activity_log_path(activity: str) -> str: + """ + Returns the path of the activity log file in .csv format + :param activity: + :return: + """ + filename = os.path.join("activity_logs", f"{activity}.csv") + return filename + + +def activity_units_path(activity: str) -> str: + """ + Returns the path of the activity activities file in .json format + :param activity: + :return: + """ + filename = os.path.join("activities", f"{activity}.json") + return filename + + +def is_convertable(activity: str) -> bool: + """ + Returns True if the activity has multiple units that needed to be converted. + i.e. distance may be sent in feet, but would require to be converted in meters. + :param activity: + :return: + """ + filename = activity_units_path(activity) + raw_data = json.load(open(filename)) + return raw_data.get("transformable") + + + +def create_activity_log_file(activity: str) -> None: + """ + Creates the activity log file in .csv format + :param activity: + :return: + """ + if not os.path.exists(activity_units_path(activity)): + raise ValueError(f"{activity} is not a valid activity") + if not os.path.exists("activity_logs"): + os.makedirs("activity_logs") + filename = activity_log_path(activity) + with open(filename, 'a') as f: + writer = csv.writer(f) + writer.writerow(["timestamp", "username", "value"]) + + + +def convert_units(activity: str, value: int | float, unit: str) -> float: + """ + Returns the value of activity in a factor of 1, i.e. converting feet to meters + :param activity: + :param value: + :param unit: + :return: + """ + filename = activity_units_path(activity) + if not os.path.exists(filename): + raise ValueError(f"{activity} is not a valid activity") + + # Consider None unit value as factor of 1 + if unit is None: + return value + + raw_data = json.load(open(filename)) + units_data = raw_data.get("units") + + for unit_name, data in units_data.items(): + if unit in data["aliases"]: + return value * data["factor"] + + raise ValueError(f"{unit} is not a valid unit") + + +def log_activity( + timestamp: int, + username: str, + activity: str, + value: int | float, + unit: str | None +) -> None: + """ + Logs the activity in .csv file + :param timestamp: + :param username: + :param activity: + :param value: + :param unit: + :return: + """ + if value <= 0 or not isinstance(value, int | float): + raise ValueError(f"{value} is not a valid value") + filename = activity_log_path(activity) + if not os.path.exists(filename): + create_activity_log_file(activity) + if is_convertable(activity): + converted_value = round(convert_units(activity, value, unit), 2) + else: + converted_value = round(value, 2) + with open(filename, 'a') as f: + writer = csv.writer(f) + writer.writerow([timestamp, username, converted_value]) + + +if __name__ == "__main__": + log_activity( + timestamp=0, + username="test", + activity="climb", + value=1, + unit="m" + ) diff --git a/fitnessCog/fitness.py b/fitnessCog/fitness.py new file mode 100644 index 0000000..bce83a8 --- /dev/null +++ b/fitnessCog/fitness.py @@ -0,0 +1,78 @@ +import os +import discord +from redbot.core import Config, commands +from activity_logger import log_activity, activity_log_path, activity_units_path + + + +class FitnessCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + # Some hardcoded values, this probably should be changed with redbot config. IDK how to use it or what's in it. + default_threads = "1457402451530350727,1328363409648910356" + self.threads_id = list(map(int, os.getenv("THREADS_ID", default_threads).split(","))) + self.confirm_reactions = os.getenv("CONFIRMATION_REACTIONS", "🐈💨") + + @commands.Cog.listener() + async def on_message(self, message): + if message.author.bot: + return + if message.channel.id not in self.threads_id: + return + if not message.content.startswith("!"): + return + + # region Fitness commands + if message.content.startswith("!getunits"): + await self.get_units_file(message) + return + if message.content.startswith("!getlog"): + await self.get_log_file(message) + return + # Make sure this one is past every other ! commands + await self.fitness_activity_log(message) + return + # endregion Fitness commands + + # region Fitness commands functions + @staticmethod + async def get_units_file(message: discord.Message): + try: + raw = message.content[1:].lower().strip().split(" ", 1) + filename = activity_units_path(raw[1]) + await message.channel.send(file=discord.File(fp=filename)) + except Exception as e: + await message.channel.send(str(e)) + + @staticmethod + async def get_log_file(message: discord.Message): + try: + raw = message.content[1:].lower().strip().split(" ", 1) + filename = activity_log_path(raw[1]) + await message.channel.send(file=discord.File(fp=filename)) + except Exception as e: + await message.channel.send(str(e)) + + async def log_fitness(self, message): + try: + raw = message.content[1:].lower().strip().split(" ", 2) + + timestamp = int(message.created_at.timestamp()) + username = message.author.name + activity = raw[0] + value = float(raw[1]) + unit = raw[2] if len(raw) > 2 else "m" + + log_activity(timestamp, username, activity, value, unit) + + for r in self.confirm_reactions: + await message.add_reaction(r) + + except Exception as e: + await message.reply(str(e)) + # endregion Fitness commands functions + + +def setup(bot): + bot.add_cog(FitnessCog(bot)) +