248 lines
8.2 KiB
Python
248 lines
8.2 KiB
Python
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 `{}` LionCoins. Good job!".format(reward)
|
|
),
|
|
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
|
|
)
|