Add module.
This commit is contained in:
83
bot/modules/workout/admin.py
Normal file
83
bot/modules/workout/admin.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from settings import GuildSettings, GuildSetting
|
||||||
|
from wards import guild_admin
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
from .data import workout_channels
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class workout_length(settings.Integer, GuildSetting):
|
||||||
|
category = "Workout"
|
||||||
|
|
||||||
|
attr_name = "min_workout_length"
|
||||||
|
_data_column = "min_workout_length"
|
||||||
|
|
||||||
|
display_name = "min_workout_length"
|
||||||
|
desc = "Minimum length of a workout."
|
||||||
|
|
||||||
|
_default = 20
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Minimun time a user must spend in a workout channel for it to count as a valid workout. "
|
||||||
|
"Value must be given in minutes."
|
||||||
|
)
|
||||||
|
_accepts = "An integer number of minutes."
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
return "The minimum workout length is now `{}` minutes.".format(self.formatted)
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class workout_reward(settings.Integer, GuildSetting):
|
||||||
|
category = "Workout"
|
||||||
|
|
||||||
|
attr_name = "workout_reward"
|
||||||
|
_data_column = "workout_reward"
|
||||||
|
|
||||||
|
display_name = "workout_reward"
|
||||||
|
desc = "Number of daily LionCoins to reward for completing a workout."
|
||||||
|
|
||||||
|
_default = 350
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Number of LionCoins given when a member completes their daily workout."
|
||||||
|
)
|
||||||
|
_accepts = "An integer number of LionCoins."
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
return "The workout reward is now `{}` LionCoins.".format(self.formatted)
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class workout_channels_setting(settings.ChannelList, settings.ListData, settings.Setting):
|
||||||
|
category = "Workout"
|
||||||
|
|
||||||
|
attr_name = 'workout_channels'
|
||||||
|
|
||||||
|
_table_interface = workout_channels
|
||||||
|
_id_column = 'guildid'
|
||||||
|
_data_column = 'channelid'
|
||||||
|
_setting = settings.VoiceChannel
|
||||||
|
|
||||||
|
write_ward = guild_admin
|
||||||
|
display_name = "workout_channels"
|
||||||
|
desc = "Channels in which members can do workouts."
|
||||||
|
|
||||||
|
_force_unique = True
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Sessions in these channels will be treated as workouts."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flat cache, no need to expire objects
|
||||||
|
_cache = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
if self.value:
|
||||||
|
return "The workout channels have been updated:\n{}".format(self.formatted)
|
||||||
|
else:
|
||||||
|
return "The workout channels have been removed."
|
||||||
10
bot/modules/workout/data.py
Normal file
10
bot/modules/workout/data.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from data import Table, RowTable
|
||||||
|
|
||||||
|
|
||||||
|
workout_channels = Table('workout_channels')
|
||||||
|
|
||||||
|
workout_sessions = RowTable(
|
||||||
|
'workout_sessions',
|
||||||
|
('sessionid', 'guildid', 'userid', 'start_time', 'duration', 'channelid'),
|
||||||
|
'sessionid'
|
||||||
|
)
|
||||||
4
bot/modules/workout/module.py
Normal file
4
bot/modules/workout/module.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from LionModule import LionModule
|
||||||
|
|
||||||
|
|
||||||
|
module = LionModule("Workout")
|
||||||
247
bot/modules/workout/tracker.py
Normal file
247
bot/modules/workout/tracker.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import datetime as dt
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from core import Lion
|
||||||
|
from settings import GuildSettings
|
||||||
|
from meta import client
|
||||||
|
from data import NULL, tables
|
||||||
|
|
||||||
|
from .module import module
|
||||||
|
from .data import workout_sessions
|
||||||
|
from . import admin
|
||||||
|
|
||||||
|
|
||||||
|
leave_tasks = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_workout_join(member):
|
||||||
|
key = (member.guild.id, member.id)
|
||||||
|
|
||||||
|
# Cancel a leave task if the member rejoined in time
|
||||||
|
if member.id in leave_tasks:
|
||||||
|
leave_tasks[key].cancel()
|
||||||
|
leave_tasks.pop(key)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a started workout entry
|
||||||
|
workout = workout_sessions.create_row(
|
||||||
|
guildid=member.guild.id,
|
||||||
|
userid=member.id,
|
||||||
|
channelid=member.voice.channel.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to current workouts
|
||||||
|
client.objects['current_workouts'][key] = workout
|
||||||
|
|
||||||
|
# Log
|
||||||
|
client.log(
|
||||||
|
"User '{m.name}'(uid:{m.id}) started a workout in channel "
|
||||||
|
"'{m.voice.channel.name}' (cid:{m.voice.channel.id}) "
|
||||||
|
"of guild '{m.guild.name}' (gid:{m.guild.id}).".format(m=member),
|
||||||
|
context="WORKOUT_STARTED"
|
||||||
|
)
|
||||||
|
GuildSettings(member.guild.id).event_log.log(
|
||||||
|
"{} started a workout in {}".format(
|
||||||
|
member.mention,
|
||||||
|
member.voice.channel.mention
|
||||||
|
), title="Workout Started"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_workout_leave(member):
|
||||||
|
key = (member.guild.id, member.id)
|
||||||
|
|
||||||
|
# Create leave task in case of temporary disconnect
|
||||||
|
task = asyncio.create_task(asyncio.sleep(3))
|
||||||
|
leave_tasks[key] = task
|
||||||
|
|
||||||
|
# Wait for the leave task, abort if it gets cancelled
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
if member.id in leave_tasks:
|
||||||
|
if leave_tasks[key] == task:
|
||||||
|
leave_tasks.pop(key)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Task was cancelled by rejoining
|
||||||
|
if key in leave_tasks and leave_tasks[key] == task:
|
||||||
|
leave_tasks.pop(key)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Retrieve workout row and remove from current workouts
|
||||||
|
workout = client.objects['current_workouts'].pop(key)
|
||||||
|
|
||||||
|
await workout_left(member, workout)
|
||||||
|
|
||||||
|
|
||||||
|
async def workout_left(member, workout):
|
||||||
|
time_diff = (dt.datetime.utcnow() - workout.start_time).total_seconds()
|
||||||
|
min_length = GuildSettings(member.guild.id).min_workout_length.value
|
||||||
|
if time_diff < 60 * min_length:
|
||||||
|
# Left workout before it was finished. Log and delete
|
||||||
|
client.log(
|
||||||
|
"User '{m.name}'(uid:{m.id}) left their workout in guild '{m.guild.name}' (gid:{m.guild.id}) "
|
||||||
|
"before it was complete! ({diff:.2f} minutes). Deleting workout.\n"
|
||||||
|
"{workout}".format(
|
||||||
|
m=member,
|
||||||
|
diff=time_diff / 60,
|
||||||
|
workout=workout
|
||||||
|
),
|
||||||
|
context="WORKOUT_ABORTED",
|
||||||
|
post=True
|
||||||
|
)
|
||||||
|
GuildSettings(member.guild.id).event_log.log(
|
||||||
|
"{} left their workout before it was complete! (`{:.2f}` minutes)".format(
|
||||||
|
member.mention,
|
||||||
|
time_diff / 60,
|
||||||
|
), title="Workout Left"
|
||||||
|
)
|
||||||
|
workout_sessions.delete_where(sessionid=workout.sessionid)
|
||||||
|
else:
|
||||||
|
# Completed the workout
|
||||||
|
client.log(
|
||||||
|
"User '{m.name}'(uid:{m.id}) completed their daily workout in guild '{m.guild.name}' (gid:{m.guild.id}) "
|
||||||
|
"({diff:.2f} minutes). Saving workout and notifying user.\n"
|
||||||
|
"{workout}".format(
|
||||||
|
m=member,
|
||||||
|
diff=time_diff / 60,
|
||||||
|
workout=workout
|
||||||
|
),
|
||||||
|
context="WORKOUT_COMPLETED",
|
||||||
|
post=True
|
||||||
|
)
|
||||||
|
workout.duration = time_diff
|
||||||
|
await workout_complete(member, workout)
|
||||||
|
|
||||||
|
|
||||||
|
async def workout_complete(member, workout):
|
||||||
|
key = (member.guild.id, member.id)
|
||||||
|
|
||||||
|
# update and notify
|
||||||
|
user = Lion.fetch(*key)
|
||||||
|
user_data = user.data
|
||||||
|
with user_data.batch_update():
|
||||||
|
user_data.workout_count = user_data.workout_count + 1
|
||||||
|
user_data.last_workout_start = workout.start_time
|
||||||
|
|
||||||
|
settings = GuildSettings(member.guild.id)
|
||||||
|
reward = settings.workout_reward.value
|
||||||
|
user.addCoins(reward)
|
||||||
|
|
||||||
|
settings.event_log.log(
|
||||||
|
"{} completed their daily workout and was rewarded `{}` coins! (`{:.2f}` minutes)".format(
|
||||||
|
member.mention,
|
||||||
|
reward,
|
||||||
|
workout.duration / 60,
|
||||||
|
), title="Workout Completed"
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=(
|
||||||
|
"Congratulations on completing your daily workout!\n"
|
||||||
|
"You have been rewarded with `350` LionCoins. Good job!"
|
||||||
|
),
|
||||||
|
timestamp=dt.datetime.utcnow(),
|
||||||
|
colour=discord.Color.orange()
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text=member.guild.name,
|
||||||
|
icon_url=member.guild.icon_url
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await member.send(embed=embed)
|
||||||
|
except discord.Forbidden:
|
||||||
|
client.log(
|
||||||
|
"Couldn't notify user '{m.name}'(uid:{m.id}) about their completed workout! "
|
||||||
|
"They might have me blocked.".format(m=member),
|
||||||
|
context="WORKOUT_COMPLETED",
|
||||||
|
post=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@client.add_after_event("voice_state_update")
|
||||||
|
async def workout_voice_tracker(client, member, before, after):
|
||||||
|
# Wait until launch tasks are complete
|
||||||
|
while not module.ready:
|
||||||
|
asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# Check whether we are moving to/from a workout channel
|
||||||
|
settings = GuildSettings(member.guild.id)
|
||||||
|
channels = settings.workout_channels.value
|
||||||
|
from_workout = before.channel in channels
|
||||||
|
to_workout = after.channel in channels
|
||||||
|
|
||||||
|
if to_workout ^ from_workout:
|
||||||
|
# Ensure guild row exists
|
||||||
|
tables.guild_config.fetch_or_create(member.guild.id)
|
||||||
|
|
||||||
|
# Fetch workout user
|
||||||
|
user = Lion.fetch(member.guild.id, member.id)
|
||||||
|
|
||||||
|
# Ignore all workout events from users who have already completed their workout today
|
||||||
|
if user.data.last_workout_start is not None:
|
||||||
|
last_date = user.localize(user.data.last_workout_start).date()
|
||||||
|
today = user.localize(dt.datetime.utcnow()).date()
|
||||||
|
if last_date == today:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: Check if they have completed a workout today, if so, ignore
|
||||||
|
if to_workout and not from_workout:
|
||||||
|
await on_workout_join(member)
|
||||||
|
elif from_workout and not to_workout:
|
||||||
|
if (member.guild.id, member.id) in client.objects['current_workouts']:
|
||||||
|
await on_workout_leave(member)
|
||||||
|
else:
|
||||||
|
client.log(
|
||||||
|
"Possible missed workout!\n"
|
||||||
|
"Member '{m.name}'(uid:{m.id}) left the workout channel '{c.name}'(cid:{c.id}) "
|
||||||
|
"in guild '{m.guild.name}'(gid:{m.guild.id}), but we never saw them join!".format(
|
||||||
|
m=member,
|
||||||
|
c=before.channel
|
||||||
|
),
|
||||||
|
context="WORKOUT_TRACKER",
|
||||||
|
level=logging.ERROR,
|
||||||
|
post=True
|
||||||
|
)
|
||||||
|
settings.event_log.log(
|
||||||
|
"{} left the workout channel {}, but I never saw them join!".format(
|
||||||
|
member.mention,
|
||||||
|
before.channel.mention,
|
||||||
|
), title="Possible Missed Workout!"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@module.launch_task
|
||||||
|
async def load_workouts(client):
|
||||||
|
client.objects['current_workouts'] = {} # (guildid, userid) -> Row
|
||||||
|
# Process any incomplete workouts
|
||||||
|
workouts = workout_sessions.fetch_rows_where(
|
||||||
|
duration=NULL
|
||||||
|
)
|
||||||
|
count = 0
|
||||||
|
for workout in workouts:
|
||||||
|
channelids = admin.workout_channels_setting.get(workout.guildid).data
|
||||||
|
member = Lion.fetch(workout.guildid, workout.userid).member
|
||||||
|
if member:
|
||||||
|
if member.voice and (member.voice.channel.id in channelids):
|
||||||
|
client.objects['current_workouts'][(workout.guildid, workout.userid)] = workout
|
||||||
|
count += 1
|
||||||
|
else:
|
||||||
|
asyncio.create_task(workout_left(member, workout))
|
||||||
|
else:
|
||||||
|
client.log(
|
||||||
|
"Removing incomplete workout from "
|
||||||
|
"non-existent member (mid:{}) in guild (gid:{})".format(
|
||||||
|
workout.userid,
|
||||||
|
workout.guildid
|
||||||
|
),
|
||||||
|
context="WORKOUT_LAUNCH",
|
||||||
|
post=True
|
||||||
|
)
|
||||||
|
if count > 0:
|
||||||
|
client.log(
|
||||||
|
"Loaded {} in-progress workouts.".format(count), context="WORKOUT_LAUNCH", post=True
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user