Files
croccybot/src/modules/pending-rewrite/gui-commands/timer.py

170 lines
4.9 KiB
Python

import asyncio
import time
import logging
import traceback
from collections import defaultdict
import discord
from utils.lib import utc_now
from core import Lion
from meta import client
from modules.study.timers.Timer import Timer
from ...cards import FocusTimerCard, BreakTimerCard
from ...utils import get_avatar_key, image_as_file, edit_files, asset_path
async def status(self):
stage = self.current_stage
name = self.data.pretty_name
remaining = int((stage.end - utc_now()).total_seconds())
duration = int(stage.duration)
next_starts = int(stage.end.timestamp())
users = [
(get_avatar_key(client, member.id),
session.duration if (session := Lion.fetch(member.guild.id, member.id).session) else 0,
session.data.tag if session else None)
for member in self.members
]
if stage.name == 'FOCUS':
card_class = FocusTimerCard
content = f"**Focus!** Session ends <t:{next_starts}:R>."
else:
card_class = BreakTimerCard
content = f"**Have a rest!** Break finishes <t:{next_starts}:R>."
page = await card_class.request(
name,
remaining,
duration,
users=users,
skin=card_class.skin_args_for(guildid=self.data.guildid)
)
return {
'content': content,
'files': [image_as_file(page, name="timer.png")]
}
_guard_delay = 60
_guarded = {} # timer channel id -> (last_executed_time, currently_waiting)
async def guard_request(id):
if (result := _guarded.get(id, None)):
last, currently = result
if currently:
return False
else:
_guarded[id] = (last, True)
await asyncio.sleep(_guard_delay - (time.time() - last))
_guarded[id] = (time.time(), False)
return True
else:
_guarded[id] = (time.time(), False)
return True
async def update_last_status(self):
"""
Update the last posted status message, if it exists.
"""
old_message = self.reaction_message
if not await guard_request(self.channelid):
return
if old_message != self.reaction_message:
return
args = await self.status()
repost = True
if self.reaction_message:
try:
await edit_files(
client._connection.http,
self.reaction_message.channel.id,
self.reaction_message.id,
**args
)
except discord.HTTPException:
pass
else:
repost = False
if repost and self.text_channel:
try:
self.reaction_message = await self.text_channel.send(**args)
await self.reaction_message.add_reaction('')
except discord.HTTPException:
pass
return
guild_locks = defaultdict(asyncio.Lock)
async def play_alert(channel: discord.VoiceChannel, alert_file):
if not channel.members:
# Don't notify an empty channel
return
async with guild_locks[channel.guild.id]:
try:
vc = channel.guild.voice_client
if not vc:
vc = await asyncio.wait_for(
channel.connect(timeout=10, reconnect=False),
20
)
elif vc.channel != channel:
await vc.move_to(channel)
except asyncio.TimeoutError:
client.log(
f"Timed out while attempting to connect to '{channel.name}' (cid:{channel.id}) "
f"in '{channel.guild.name}' (gid:{channel.guild.id}).",
context="TIMER_ALERT",
level=logging.WARNING
)
vc = channel.guild.voice_client
if vc:
await vc.disconnect(force=True)
return
audio_stream = open(alert_file, 'rb')
try:
vc.play(discord.PCMAudio(audio_stream), after=lambda e: audio_stream.close())
except discord.HTTPException:
pass
count = 0
while vc.is_playing() and count < 10:
await asyncio.sleep(1)
count += 1
await vc.disconnect(force=True)
async def notify_hook(self, old_stage, new_stage):
try:
if new_stage.name == 'BREAK':
await play_alert(self.channel, asset_path('timer/voice/break_alert.wav'))
else:
await play_alert(self.channel, asset_path('timer/voice/focus_alert.wav'))
except Exception:
full_traceback = traceback.format_exc()
client.log(
f"Caught an unhandled exception while playing timer alert in '{self.channel.name}' (cid:{self.channel.id})"
f" in '{self.channel.guild.name}' (gid:{self.channel.guild.id}).\n"
f"{full_traceback}",
context="TIMER_ALERT",
level=logging.ERROR
)
Timer.status = status
Timer.update_last_status = update_last_status
Timer.notify_hook = notify_hook