Add module.

This commit is contained in:
2021-09-12 11:36:54 +03:00
parent a001217345
commit 70fa80fe29
4 changed files with 344 additions and 0 deletions

View 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."

View 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'
)

View File

@@ -0,0 +1,4 @@
from LionModule import LionModule
module = LionModule("Workout")

View 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
)