From 4ae03c9b9d45731231279e414a063e3e8f2893e8 Mon Sep 17 00:00:00 2001 From: T-BENZIN Date: Sun, 11 Jan 2026 10:03:55 +0500 Subject: [PATCH] Added cog for fitness activity logging feature. At its starting state it would accept following commands: - !getunits : Would send a JSON file with activity units via Discord message. Those files are responsible for setting up a fitness activity to log. - !getlog : Would send a CSV file with fitness activity log. Those files contain records with timestamp, username, and reported activity measurements. - ! : Would create a record with your fitness activity in corresponding CSV log file. If you choose to report different unit (feet, for example), it is going to convert as specified in its corresponding JSON file with units (to meters, for example) if the activity specified as convertable. Non-convertable activities' units are ignored. --- README.md | 1 + fitnessCog/__init__.py | 6 ++ fitnessCog/activities/climb.json | 48 ++++++++++++ fitnessCog/activities/pushups.json | 3 + fitnessCog/activity_logger.py | 119 +++++++++++++++++++++++++++++ fitnessCog/fitness.py | 78 +++++++++++++++++++ 6 files changed, 255 insertions(+) create mode 100644 fitnessCog/__init__.py create mode 100644 fitnessCog/activities/climb.json create mode 100644 fitnessCog/activities/pushups.json create mode 100644 fitnessCog/activity_logger.py create mode 100644 fitnessCog/fitness.py 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)) +