Skip to content

Commit 9b0bdad

Browse files
authored
Merge pull request #1 from Grimwald-SMP/feature/timezone-map
Feature/timezone map
2 parents 6d32dd0 + 6b346e1 commit 9b0bdad

5 files changed

Lines changed: 507 additions & 13 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ logs
33
keys
44
backups/roles/*
55
assets/graphs
6+
.claude
67

78
# Files
89
.env*
910
package-lock.json
1011
*.sqlite
11-
__pycache__
12+
__pycache__
13+
availability.png

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
rich
2+
pillow
23
pyyaml
34
pymongo
45
discord

src/bot/cogs/timezones.py

Lines changed: 213 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import io
12
from datetime import datetime, timedelta
23
from datetime import timezone as timez
34
from zoneinfo import ZoneInfo
45

5-
from discord import app_commands, Interaction, Embed, User
6+
from discord import app_commands, Interaction, Embed, User, File
67
from discord.ext.commands import GroupCog
78

89
from src.base.config import config
910
from src.bot.bot import Bot
1011
from src.database.database import database
12+
from src.utils.availability import generate_chart
1113

1214

1315
class Timezones(GroupCog, name="timezone", description="Timezone commands"):
@@ -18,7 +20,13 @@ def __init__(self, bot: Bot):
1820
super().__init__()
1921

2022
@app_commands.command(name="set", description="Set your timezone")
21-
async def add(self, ctx: Interaction, timezone: str = None, utc_offset: int = None, current_time: str = None):
23+
async def add(
24+
self,
25+
ctx: Interaction,
26+
timezone: str | None = None,
27+
utc_offset: int | None = None,
28+
current_time: str | None = None,
29+
):
2230
"""
2331
Add your timezone to the database.
2432
@@ -45,15 +53,16 @@ async def add(self, ctx: Interaction, timezone: str = None, utc_offset: int = No
4553
now = datetime.now(tz)
4654
fmt_tz = tzinfo_to_storage(tz)
4755

48-
res = database.users.update_one({"user_id": ctx.user.id}, {"$set": {"timezone": fmt_tz}}, upsert=True)
56+
res = database.users.update_one(
57+
{"user_id": ctx.user.id}, {"$set": {"timezone": fmt_tz}}, upsert=True
58+
)
4959
print(res.upserted_id)
5060

5161
embed = Embed(
5262
color=config.colors["primary"],
5363
title="Timezone Set",
5464
description=(
55-
f"Set with value `{fmt_tz}`\n"
56-
f"-# Timezone: {now.tzname()}\n"
65+
f"Set with value `{fmt_tz}`\n" f"-# Timezone: {now.tzname()}\n"
5766
),
5867
)
5968

@@ -63,7 +72,7 @@ async def add(self, ctx: Interaction, timezone: str = None, utc_offset: int = No
6372
print("An error has occurred:", e)
6473

6574
@app_commands.command(name="get", description="Get your timezone")
66-
async def get(self, ctx: Interaction, user: User = None):
75+
async def get(self, ctx: Interaction, user: User | None = None):
6776
"""Get your timezone"""
6877
user = user or ctx.user
6978
res = database.users.find_one({"user_id": user.id})
@@ -92,13 +101,207 @@ async def get(self, ctx: Interaction, user: User = None):
92101
embed.set_thumbnail(url=user.display_avatar.url)
93102
await ctx.response.send_message(embed=embed)
94103

104+
@app_commands.command(
105+
name="set-availability", description="Set the times when you're available"
106+
)
107+
async def set_availability(
108+
self,
109+
ctx: Interaction,
110+
start_time: int,
111+
end_time: int,
112+
):
113+
"""
114+
Set your availability times in your timezone.
115+
116+
Parameters:
117+
start_time: int
118+
The start hour of your availability in 24 hr time (ex: 15 for 3:00pm).
119+
end_time: int
120+
The end hour of your availability in 24 hr time (ex: 15 for 3:00pm).
121+
"""
122+
123+
try:
124+
if not (0 <= start_time < 24):
125+
raise ValueError("Invalid start time")
126+
if not (0 <= end_time < 24):
127+
raise ValueError("Invalid end time")
128+
129+
database.users.update_one(
130+
{"user_id": ctx.user.id},
131+
{
132+
"$set": {
133+
"availability": {
134+
"start_time": start_time,
135+
"end_time": end_time,
136+
}
137+
}
138+
},
139+
upsert=True,
140+
)
141+
142+
embed = Embed(
143+
color=config.colors["primary"],
144+
title="Availability Set",
145+
description=(
146+
f"Your availability has been set from `{start_time:02d}` "
147+
f"to `{end_time:02d}` in your timezone."
148+
),
149+
)
150+
151+
await ctx.response.send_message(embed=embed)
152+
153+
except ValueError:
154+
embed = Embed(
155+
color=config.colors["error"],
156+
description="Invalid time format. Please use 'HH' in 24 hr format.",
157+
)
158+
await ctx.response.send_message(embed=embed)
159+
except Exception as e:
160+
print("An error has occurred:", e)
161+
162+
@app_commands.command(
163+
name="chart", description="Get a chart of user availabilities"
164+
)
165+
async def chart(
166+
self,
167+
ctx: Interaction,
168+
user1: User | None = None,
169+
user2: User | None = None,
170+
user3: User | None = None,
171+
offset: int = 0,
172+
):
173+
"""Get a chart of user availabilities."""
174+
175+
try:
176+
users_data = []
177+
178+
for user in [ctx.user, user1, user2, user3]:
179+
if user is None:
180+
continue
181+
182+
res = database.users.find_one(
183+
{"user_id": user.id, "availability": {"$exists": True}}
184+
)
185+
186+
if res is None or res.get("availability") is None:
187+
await ctx.response.send_message(
188+
embed=Embed(
189+
color=config.colors["error"],
190+
description=(
191+
f"{user.name} hasn't set their availability yet.\n"
192+
"Use `/timezone set-availability` to set it."
193+
),
194+
),
195+
ephemeral=True,
196+
)
197+
return
198+
199+
tz = storage_to_tzinfo(res["timezone"])
200+
utc_offset = get_utc_offset(tz) + offset
201+
202+
start = res["availability"]["start_time"]
203+
end = res["availability"]["end_time"]
204+
205+
users_data.append({
206+
"id": user.display_name,
207+
"free": f"{start}-{end}",
208+
"utc_offset": utc_offset,
209+
})
210+
211+
if not users_data:
212+
await ctx.response.send_message(
213+
embed=Embed(
214+
color=config.colors["error"],
215+
description="No users provided.",
216+
),
217+
ephemeral=True,
218+
)
219+
return
220+
221+
png_bytes = generate_chart(users_data, output_path=None, display_offset=offset)
222+
223+
file = File(io.BytesIO(png_bytes), filename="availability.png")
224+
# embed = Embed(color=config.colors["primary"])
225+
# embed.set_image(url="attachment://availability.png")
226+
227+
await ctx.response.send_message(file=file)
228+
229+
except Exception as e:
230+
print("An error has occurred:", e)
231+
232+
@app_commands.command(
233+
name="recent-chatters", description="Get the timezones of recent chatters"
234+
)
235+
async def recent_chatters(
236+
self,
237+
ctx: Interaction,
238+
):
239+
"""Get the timezones of recent chatters."""
240+
try:
241+
seen = set()
242+
recent = []
243+
244+
async for message in ctx.channel.history(limit=200):
245+
if message.author.id not in seen and not message.author.bot:
246+
seen.add(message.author.id)
247+
recent.append(message.author)
248+
if len(recent) >= 5:
249+
break
250+
251+
if not recent:
252+
embed = Embed(
253+
color=config.colors["error"],
254+
description="No recent chatters found.",
255+
)
256+
return await ctx.response.send_message(embed=embed)
257+
258+
timezones = []
259+
for user in recent:
260+
res = database.users.find_one({"user_id": user.id})
261+
if res is None:
262+
continue
263+
tz_str = res["timezone"] if res else None
264+
tz = storage_to_tzinfo(tz_str)
265+
now = datetime.now(tz)
266+
current_time = now.strftime("%H:%M %p")
267+
timezones.append(f"`{current_time}` | {user.mention}")
268+
269+
if not timezones:
270+
embed = Embed(
271+
color=config.colors["error"],
272+
description="No timezones found for recent chatters.",
273+
)
274+
return await ctx.response.send_message(embed=embed)
275+
276+
embed = Embed(
277+
color=config.colors["primary"],
278+
title="Recent Chatters",
279+
description="\n".join(timezones) + f"\n-# Requested at <t:{int(datetime.now().timestamp())}:t>",
280+
)
281+
await ctx.response.send_message(embed=embed)
282+
283+
except Exception as e:
284+
print("An error has occurred:", e)
285+
95286

96287
async def setup(bot: Bot):
97288
await bot.add_cog(Timezones(bot))
98289

99290

100-
def resolve_timezone(iana: str | None, utc_offset: int | None, current_time: str | None):
291+
292+
def get_utc_offset(tz) -> float:
293+
"""Get the UTC offset of a timezone."""
294+
offset = tz.utcoffset(datetime.now())
295+
return offset.total_seconds() / 3600
296+
297+
298+
def resolve_timezone(
299+
iana: str | None, utc_offset: int | None, current_time: str | None
300+
):
101301
if iana:
302+
first, second = iana.split("/")
303+
iana = f"{first.capitalize()}/{second.capitalize()}"
304+
102305
return ZoneInfo(iana)
103306

104307
if utc_offset is not None:
@@ -110,9 +313,8 @@ def resolve_timezone(iana: str | None, utc_offset: int | None, current_time: str
110313

111314
utc_now = datetime.now(timez.utc)
112315

113-
user_today = (
114-
utc_now.replace(tzinfo=None)
115-
.replace(hour=h, minute=m, second=0, microsecond=0)
316+
user_today = utc_now.replace(tzinfo=None).replace(
317+
hour=h, minute=m, second=0, microsecond=0
116318
)
117319

118320
diff = user_today - utc_now.replace(tzinfo=None)
@@ -181,4 +383,4 @@ def storage_to_tzinfo(stored_str: str):
181383
try:
182384
return ZoneInfo(stored_str)
183385
except Exception:
184-
return dt_timezone.utc
386+
return timez.utc

0 commit comments

Comments
 (0)