Skip to content
This repository was archived by the owner on Mar 12, 2025. It is now read-only.

Commit 71cac6b

Browse files
authored
feat: add ?practices and daily scheduled practices (#18)
* feat: add ?practices and daily scheduled practices * fix example values
1 parent b4d55d3 commit 71cac6b

File tree

3 files changed

+154
-4
lines changed

3 files changed

+154
-4
lines changed

.env.example

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ GOOGLE_PRIVATE_KEY="CHANGEME"
99
GOOGLE_CLIENT_EMAIL="CHANGEME"
1010
GOOGLE_SHEET_KEY="CHANGEME"
1111
FEEDBACK_SHEET_KEY="CHANGEME"
12+
# Mapping of guild IDs => gsheet keys
13+
SCHEDULE_SHEET_KEYS="123=321"
14+
# Mapping of guild IDs => channel ID where to send daily schedule
15+
SCHEDULE_CHANNELS="456=654"
16+
DAILY_PRACTICE_SEND_TIME=14:00
1217

1318
ZOOM_USER_ID="CHANGEME"
1419
ZOOM_JWT="CHANGEME"

bot.py

+147-4
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
import random
66
import re
77
from contextlib import suppress
8-
from typing import Optional
8+
from typing import Optional, NamedTuple
99
from urllib.parse import quote_plus
1010

1111
import discord
12+
import dateparser
1213
import gspread
13-
from discord.ext import commands
14+
from discord.ext import commands, tasks
1415
from discord.ext.commands import Context
1516
from environs import Env
1617
from google.auth.crypt._python_rsa import RSASigner
1718
from google.oauth2.service_account import Credentials
19+
import pytz
1820

1921
import handshapes
2022
import cuteid
@@ -34,18 +36,26 @@
3436
SECRET_KEY = env.str("SECRET_KEY", required=True)
3537
COMMAND_PREFIX = env.str("COMMAND_PREFIX", "?")
3638

37-
3839
GOOGLE_PROJECT_ID = env.str("GOOGLE_PROJECT_ID", required=True)
3940
GOOGLE_PRIVATE_KEY = env.str("GOOGLE_PRIVATE_KEY", required=True)
4041
GOOGLE_PRIVATE_KEY_ID = env.str("GOOGLE_PRIVATE_KEY_ID", required=True)
4142
GOOGLE_CLIENT_EMAIL = env.str("GOOGLE_CLIENT_EMAIL", required=True)
4243
GOOGLE_TOKEN_URI = env.str("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token")
4344
FEEDBACK_SHEET_KEY = env.str("FEEDBACK_SHEET_KEY", required=True)
45+
SCHEDULE_SHEET_KEYS = env.dict("SCHEDULE_SHEET_KEYS", required=True, subcast_key=int)
46+
SCHEDULE_CHANNELS = env.dict(
47+
"SCHEDULE_CHANNELS", required=True, subcast_key=int, subcast=int
48+
)
49+
4450

4551
ZOOM_USER_ID = env.str("ZOOM_USER_ID", required=True)
4652
ZOOM_JWT = env.str("ZOOM_JWT", required=True)
4753

4854
WATCH2GETHER_API_KEY = env.str("WATCH2GETHER_API_KEY", required=True)
55+
# Default to 10 AM EDT
56+
_daily_practice_send_time_raw = env.str("DAILY_PRACTICE_SEND_TIME", "14:00")
57+
_hour, _min = _daily_practice_send_time_raw.split(":")
58+
DAILY_PRACTICE_SEND_TIME = dt.time(hour=int(_hour), minute=int(_min))
4959

5060
env.seal()
5161

@@ -105,6 +115,7 @@ async def on_ready():
105115
name=f"{COMMAND_PREFIX}sign | {COMMAND_PREFIX}handshapes",
106116
type=discord.ActivityType.playing,
107117
)
118+
daily_practice_message.start()
108119
await bot.change_presence(activity=activity)
109120

110121

@@ -176,6 +187,11 @@ def sign_impl(word: str):
176187

177188
@bot.command(name="sign", aliases=("howsign",), help=SIGN_HELP)
178189
async def sign_command(ctx: Context, *, word: str):
190+
# TODO: Remove. This is just for identifying guild and channel IDs for the daily schedule
191+
if ctx.guild:
192+
logger.info(
193+
f"sign command invoked in guild {ctx.guild.id} ({ctx.guild.name}), channel {ctx.channel.id} (#{ctx.channel.name})"
194+
)
179195
await ctx.send(**sign_impl(word))
180196

181197

@@ -285,9 +301,136 @@ def get_gsheet_client():
285301
return gspread.authorize(credentials)
286302

287303

304+
# -----------------------------------------------------------------------------
305+
306+
EASTERN = pytz.timezone("US/Eastern")
307+
PACIFIC = pytz.timezone("US/Pacific")
308+
TIME_FORMAT = "%-I:%M %p %Z"
309+
310+
311+
class PracticeSession(NamedTuple):
312+
dtime: Optional[dt.datetime]
313+
host: str
314+
notes: str
315+
316+
317+
def get_practice_sessions_today(guild_id: int):
318+
logger.info(f"fetching today's practice sessions for guild {guild_id}")
319+
client = get_gsheet_client()
320+
sheet = client.open_by_key(SCHEDULE_SHEET_KEYS[guild_id])
321+
worksheet = sheet.get_worksheet(0)
322+
all_values = worksheet.get_all_values()
323+
now = dt.datetime.utcnow()
324+
today = now.date()
325+
sessions = [
326+
PracticeSession(
327+
dtime=dateparser.parse(
328+
row[0],
329+
# Use eastern time if timezone can't be parsed; return a UTC datetime
330+
settings={"TIMEZONE": "US/Eastern", "TO_TIMEZONE": "UTC"},
331+
),
332+
host=row[1],
333+
notes=row[2],
334+
)
335+
for row in all_values[2:]
336+
if row
337+
]
338+
return sorted(
339+
[
340+
session
341+
for session in sessions
342+
# Assume pacific time when filtering to include all of US
343+
if session.dtime and session.dtime.astimezone(PACIFIC).date() == today
344+
],
345+
key=lambda s: s.dtime,
346+
)
347+
348+
349+
def make_practice_sessions_today_embed(guild_id: int):
350+
sessions = get_practice_sessions_today(guild_id)
351+
now = dt.datetime.utcnow()
352+
embed = discord.Embed(
353+
description=f"{now:%A, %B %-d}",
354+
color=discord.Color.orange(),
355+
)
356+
if not sessions:
357+
embed.description += "\n\n*There are no scheduled practices yet!*"
358+
else:
359+
for session in sessions:
360+
pacific_dstr = session.dtime.astimezone(PACIFIC).strftime(TIME_FORMAT)
361+
eastern_dstr = session.dtime.astimezone(EASTERN).strftime(TIME_FORMAT)
362+
title = f"{pacific_dstr} / {eastern_dstr}"
363+
value = ""
364+
if session.host:
365+
value += f"Host: {session.host}"
366+
if session.notes:
367+
value += f"\nNotes: {session.notes}"
368+
embed.add_field(name=title, value=value or "Practice", inline=False)
369+
sheet_key = SCHEDULE_SHEET_KEYS[guild_id]
370+
embed.add_field(
371+
name="Schedule a practice here:",
372+
value=f"[Practice Schedule](https://docs.google.com/spreadsheets/d/{sheet_key}/edit)",
373+
)
374+
return embed
375+
376+
377+
async def is_in_guild(ctx: Context):
378+
return bool(ctx.guild)
379+
380+
381+
@bot.command(
382+
name="practices", help="List today's practice schedule for the current server"
383+
)
384+
@commands.check(is_in_guild)
385+
async def practices_command(ctx: Context):
386+
guild = ctx.guild
387+
embed = make_practice_sessions_today_embed(guild.id)
388+
await ctx.send(embed=embed)
389+
390+
391+
@practices_command.error
392+
async def practices_error(ctx, error):
393+
if isinstance(error, commands.errors.CheckFailure):
394+
await ctx.send(
395+
f"`{COMMAND_PREFIX}{ctx.invoked_with}` must be run within a server."
396+
)
397+
else:
398+
logger.error(
399+
f"unexpected error when handling '{ctx.invoked_with}'", exc_info=error
400+
)
401+
402+
403+
@tasks.loop(seconds=10.0)
404+
async def daily_practice_message():
405+
now = dt.datetime.utcnow()
406+
date = now.date()
407+
if now.time() > DAILY_PRACTICE_SEND_TIME:
408+
date = now.date() + dt.timedelta(days=1)
409+
then = dt.datetime.combine(date, DAILY_PRACTICE_SEND_TIME)
410+
logger.info(f"practice schedules will be sent at {then.isoformat()}")
411+
await discord.utils.sleep_until(then)
412+
logger.info("sending daily practice schedules")
413+
for guild_id, channel_id in SCHEDULE_CHANNELS.items():
414+
try:
415+
logger.info(
416+
f"sending daily practice schedule for guild {guild_id}, channel {channel_id}"
417+
)
418+
guild = bot.get_guild(guild_id)
419+
channel = guild.get_channel(channel_id)
420+
embed = make_practice_sessions_today_embed(guild.id)
421+
await channel.send(embed=embed)
422+
except Exception:
423+
logger.exception(
424+
f"could not send message to guild {guild_id}, channel {channel_id}"
425+
)
426+
427+
428+
# -----------------------------------------------------------------------------
429+
430+
288431
def post_feedback(username: str, feedback: str, guild: Optional[str]):
289-
# Assumes rows are in the format (date, feedback, guild, version)
290432
client = get_gsheet_client()
433+
# Assumes rows are in the format (date, feedback, guild, version)
291434
sheet = client.open_by_key(FEEDBACK_SHEET_KEY)
292435
now = dt.datetime.now(dt.timezone.utc)
293436
worksheet = sheet.get_worksheet(0)

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ aiohttp==3.6.2
55
pyyaml==5.3.1
66
python-slugify==4.0.1
77
emoji==0.6.0
8+
dateparser==0.7.6
9+
pytz==2020.1
810
.

0 commit comments

Comments
 (0)