1+ import io
12from datetime import datetime , timedelta
23from datetime import timezone as timez
34from zoneinfo import ZoneInfo
45
5- from discord import app_commands , Interaction , Embed , User
6+ from discord import app_commands , Interaction , Embed , User , File
67from discord .ext .commands import GroupCog
78
89from src .base .config import config
910from src .bot .bot import Bot
1011from src .database .database import database
12+ from src .utils .availability import generate_chart
1113
1214
1315class 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
96287async 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