Added cog for fitness activity logging feature.
At its starting state it would accept following commands: - !getunits <activity>: Would send a JSON file with activity units via Discord message. Those files are responsible for setting up a fitness activity to log. - !getlog <activity>: Would send a CSV file with fitness activity log. Those files contain records with timestamp, username, and reported activity measurements. - !<activity> <value> <unit (optional)>: 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.
This commit is contained in:
parent
074ff8bbb4
commit
4ae03c9b9d
@ -2,6 +2,7 @@
|
|||||||
cogs made for our Royal butt.
|
cogs made for our Royal butt.
|
||||||
|
|
||||||
## Our Cogs:
|
## Our Cogs:
|
||||||
|
- [FitnessCog](./fitnessCog)
|
||||||
- [RecruitmentCog](./recruitmentCog/)
|
- [RecruitmentCog](./recruitmentCog/)
|
||||||
- [ReginaldCog](./reginaldCog/)
|
- [ReginaldCog](./reginaldCog/)
|
||||||
- [TrafficCog](./trafficCog)
|
- [TrafficCog](./trafficCog)
|
||||||
|
|||||||
6
fitnessCog/__init__.py
Normal file
6
fitnessCog/__init__.py
Normal file
@ -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)
|
||||||
48
fitnessCog/activities/climb.json
Normal file
48
fitnessCog/activities/climb.json
Normal file
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
fitnessCog/activities/pushups.json
Normal file
3
fitnessCog/activities/pushups.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"convertable": false
|
||||||
|
}
|
||||||
119
fitnessCog/activity_logger.py
Normal file
119
fitnessCog/activity_logger.py
Normal file
@ -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"
|
||||||
|
)
|
||||||
78
fitnessCog/fitness.py
Normal file
78
fitnessCog/fitness.py
Normal file
@ -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))
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user