1- from datetime import UTC , timedelta
1+ from datetime import UTC , datetime , timedelta
22
33import arrow
44import dateutil
@@ -89,6 +89,24 @@ async def reschedule_roles(self) -> None:
8989 else :
9090 await self .handle_moderator_state (mod )
9191
92+ @staticmethod
93+ async def _parse_schedule (schedule : str ) -> tuple [datetime , datetime ]:
94+ """Parse the schedule string stored in the schedules cache into the closest start and end times."""
95+ start_time , shift_duration = schedule .split ("|" )
96+ start = dateutil_parse (start_time ).replace (tzinfo = UTC )
97+ end = start + timedelta (seconds = int (shift_duration ))
98+ now = arrow .utcnow ()
99+
100+ # Move the shift's day such that the end time is in the future and is closest.
101+ if start - timedelta (days = 1 ) < now < end - timedelta (days = 1 ): # The shift started yesterday and is ongoing.
102+ start -= timedelta (days = 1 )
103+ end -= timedelta (days = 1 )
104+ elif now > end : # Today's shift already ended, next one is tomorrow.
105+ start += timedelta (days = 1 )
106+ end += timedelta (days = 1 )
107+
108+ return start , end
109+
92110 async def handle_moderator_state (self , mod : Member ) -> None :
93111 """Add/remove and/or schedule add/remove of the moderators role according to the mod's state in the caches."""
94112 expiry_iso = await self .pings_off_mods .get (mod .id , None )
@@ -103,23 +121,12 @@ async def handle_moderator_state(self, mod: Member) -> None:
103121 await mod .add_roles (self .moderators_role , reason = "Pings off period expired." )
104122 return
105123
106- start_time , shift_duration = schedule_str .split ("|" )
107- start = dateutil_parse (start_time ).replace (tzinfo = UTC )
108- end = start + timedelta (seconds = int (shift_duration ))
109- now = arrow .utcnow ()
110-
111- # Move the shift's day such that the end time is in the future and is closest.
112- if start - timedelta (days = 1 ) < now < end - timedelta (days = 1 ): # The shift started yesterday and is ongoing.
113- start -= timedelta (days = 1 )
114- end -= timedelta (days = 1 )
115- elif now > end : # Today's shift already ended, next one is tomorrow.
116- start += timedelta (days = 1 )
117- end += timedelta (days = 1 )
124+ start , end = await self ._parse_schedule (schedule_str )
118125
119126 # The calls to `handle_moderator_state` here aren't recursive as the scheduler creates separate tasks.
120127 # Start/end have to be differentiated in scheduler task ID. The task is removed from the scheduler only after
121128 # completion. That means that task with ID X can't schedule a task with the same ID X.
122- if start < now < end :
129+ if start < arrow . utcnow () < end :
123130 if mod .get_role (self .moderators_role .id ) is None :
124131 await mod .add_roles (self .moderators_role , reason = "Mod active hours started." )
125132 if f"{ mod .id } _end" not in self ._shift_scheduler :
@@ -136,6 +143,33 @@ async def end_pings_off_period(self, mod: Member) -> None:
136143 await self .pings_off_mods .delete (mod .id )
137144 await self .handle_moderator_state (mod )
138145
146+ async def _get_current_status (self , mod_id : int ) -> str :
147+ """Build a string summarizing the moderator's current state and schedule (if one exists)."""
148+ state = "on"
149+ expiry_iso = await self .pings_off_mods .get (mod_id )
150+ if expiry_iso is not None :
151+ state = f"off until { discord_timestamp (isoparse (expiry_iso ), format = TimestampFormats .DAY_TIME )} "
152+
153+ schedule = ""
154+ schedule_str = await self .modpings_schedules .get (mod_id , None )
155+ if schedule_str is not None :
156+ start , end = await self ._parse_schedule (schedule_str )
157+ if state == "on" :
158+ if start < arrow .utcnow () < end :
159+ state = "on according to schedule"
160+ else :
161+ state = "off according to schedule"
162+ if state .startswith ("off until" ):
163+ schedule = " Otherwise, pings are on every day between "
164+ else :
165+ schedule = " Pings are on every day between "
166+ schedule += (
167+ f"{ discord_timestamp (start , TimestampFormats .TIME )} and "
168+ f"{ discord_timestamp (end , TimestampFormats .TIME )} ."
169+ )
170+
171+ return f"Pings are { state } .{ schedule } "
172+
139173 @group (name = "modpings" , aliases = ("modping" ,), invoke_without_command = True )
140174 async def modpings_group (self , ctx : Context ) -> None :
141175 """Allow the removal and re-addition of the pingable moderators role."""
@@ -201,7 +235,8 @@ async def on_command(self, ctx: Context) -> None:
201235
202236 await self .handle_moderator_state (mod )
203237
204- await ctx .send (f"{ Emojis .check_mark } Moderators role has been re-applied." ) # TODO make message more accurate.
238+ status = await self ._get_current_status (mod .id )
239+ await ctx .send (f"{ Emojis .check_mark } { status } " )
205240
206241 @modpings_group .group (name = "schedule" , aliases = ("s" ,), invoke_without_command = True )
207242 async def schedule_modpings (self , ctx : Context , start_time : str , end_time : str , tz : float | None ) -> None :
@@ -259,11 +294,19 @@ async def modpings_schedule_delete(self, ctx: Context) -> None:
259294 await self .handle_moderator_state (ctx .author )
260295 await ctx .reply (f"{ Emojis .ok_hand } Deleted your modpings schedule." )
261296
297+ @modpings_group .command (name = "status" , aliases = ("state" ,))
298+ async def status_command (self , ctx : Context ) -> None :
299+ """Show your current state and schedule (if one exists)."""
300+ status = await self ._get_current_status (ctx .author .id )
301+ await ctx .reply (f":information: { status } " )
302+
262303 @modpings_group .command (name = "sync" )
263304 async def sync_command (self , ctx : Context ) -> None :
264305 """
265306 Attempt to re-sync your pingable moderators role with the stored state.
266307
308+ You can view your stored state using the `modpings status` command.
309+
267310 If there is a reoccurring problem, please report it.
268311 """
269312 await self .handle_moderator_state (ctx .author )
0 commit comments