2929import os
3030import re
3131import sys
32+ import typing
3233
3334from datetime import datetime
3435from pkg_resources import parse_version
4950from core .clients import DatabaseClient , PluginDatabaseClient
5051from core .config import ConfigManager
5152from core .utils import info , error , human_join
52- from core .models import Bot , PermissionLevel
53+ from core .models import PermissionLevel
5354from core .thread import ThreadManager
5455from core .time import human_timedelta
5556
@@ -93,14 +94,16 @@ def format(self, record):
9394 Style .RESET_ALL
9495
9596
96- class ModmailBot (Bot ):
97+ class ModmailBot (commands . Bot ):
9798
9899 def __init__ (self ):
99100 super ().__init__ (command_prefix = None ) # implemented in `get_prefix`
100101 self ._threads = None
101102 self ._session = None
102103 self ._config = None
103104 self ._db = None
105+ self .start_time = datetime .utcnow ()
106+ self ._connected = asyncio .Event ()
104107
105108 self ._configure_logging ()
106109 # TODO: Raise fatal error if mongo_uri or other essentials are not found
@@ -112,6 +115,20 @@ def __init__(self):
112115 self .autoupdate_task = self .loop .create_task (self .autoupdate_loop ())
113116 self ._load_extensions ()
114117
118+ @property
119+ def uptime (self ) -> str :
120+ now = datetime .utcnow ()
121+ delta = now - self .start_time
122+ hours , remainder = divmod (int (delta .total_seconds ()), 3600 )
123+ minutes , seconds = divmod (remainder , 60 )
124+ days , hours = divmod (hours , 24 )
125+
126+ fmt = '{h}h {m}m {s}s'
127+ if days :
128+ fmt = '{d}d ' + fmt
129+
130+ return fmt .format (d = days , h = hours , m = minutes , s = seconds )
131+
115132 def _configure_logging (self ):
116133 level_text = self .config .log_level .upper ()
117134 logging_levels = {
@@ -133,31 +150,31 @@ def _configure_logging(self):
133150 logger .info (info ('Using default logging level: INFO' ))
134151
135152 @property
136- def version (self ):
153+ def version (self ) -> str :
137154 return __version__
138155
139156 @property
140- def db (self ):
157+ def db (self ) -> typing . Optional [ AsyncIOMotorClient ] :
141158 return self ._db
142159
143160 @property
144- def api (self ):
161+ def api (self ) -> DatabaseClient :
145162 return self ._api
146163
147164 @property
148- def config (self ):
165+ def config (self ) -> ConfigManager :
149166 if self ._config is None :
150167 self ._config = ConfigManager (self )
151168 return self ._config
152169
153170 @property
154- def session (self ):
171+ def session (self ) -> ClientSession :
155172 if self ._session is None :
156173 self ._session = ClientSession (loop = self .loop )
157174 return self ._session
158175
159176 @property
160- def threads (self ):
177+ def threads (self ) -> ThreadManager :
161178 if self ._threads is None :
162179 self ._threads = ThreadManager (self )
163180 return self ._threads
@@ -220,13 +237,13 @@ def run(self, *args, **kwargs):
220237 self .loop .close ()
221238 logger .info (error (' - Shutting down bot - ' ))
222239
223- async def is_owner (self , user ) :
240+ async def is_owner (self , user : discord . User ) -> bool :
224241 raw = str (self .config .get ('owners' , '0' )).split (',' )
225242 allowed = {int (x ) for x in raw }
226243 return (user .id in allowed ) or await super ().is_owner (user )
227244
228245 @property
229- def log_channel (self ):
246+ def log_channel (self ) -> typing . Optional [ discord . TextChannel ] :
230247 channel_id = self .config .get ('log_channel_id' )
231248 if channel_id is not None :
232249 return self .get_channel (int (channel_id ))
@@ -235,31 +252,31 @@ def log_channel(self):
235252 return None
236253
237254 @property
238- def snippets (self ):
255+ def snippets (self ) -> typing . Dict [ str , str ] :
239256 return {k : v for k , v in self .config .get ('snippets' , {}).items () if v }
240257
241258 @property
242- def aliases (self ):
259+ def aliases (self ) -> typing . Dict [ str , str ] :
243260 return {k : v for k , v in self .config .get ('aliases' , {}).items () if v }
244261
245262 @property
246- def token (self ):
263+ def token (self ) -> str :
247264 return self .config .token
248265
249266 @property
250- def guild_id (self ):
267+ def guild_id (self ) -> int :
251268 return int (self .config .guild_id )
252269
253270 @property
254- def guild (self ):
271+ def guild (self ) -> discord . Guild :
255272 """
256273 The guild that the bot is serving
257274 (the server where users message it from)
258275 """
259276 return discord .utils .get (self .guilds , id = self .guild_id )
260277
261278 @property
262- def modmail_guild (self ):
279+ def modmail_guild (self ) -> discord . Guild :
263280 """
264281 The guild that the bot is operating in
265282 (where the bot is creating threads)
@@ -270,11 +287,11 @@ def modmail_guild(self):
270287 return discord .utils .get (self .guilds , id = int (modmail_guild_id ))
271288
272289 @property
273- def using_multiple_server_setup (self ):
290+ def using_multiple_server_setup (self ) -> bool :
274291 return self .modmail_guild != self .guild
275292
276293 @property
277- def main_category (self ):
294+ def main_category (self ) -> typing . Optional [ discord . TextChannel ] :
278295 category_id = self .config .get ('main_category_id' )
279296 if category_id is not None :
280297 return discord .utils .get (self .modmail_guild .categories ,
@@ -286,15 +303,15 @@ def main_category(self):
286303 return None
287304
288305 @property
289- def blocked_users (self ):
306+ def blocked_users (self ) -> typing . Dict [ str , str ] :
290307 return self .config .get ('blocked' , {})
291308
292309 @property
293- def prefix (self ):
310+ def prefix (self ) -> str :
294311 return self .config .get ('prefix' , '?' )
295312
296313 @property
297- def mod_color (self ):
314+ def mod_color (self ) -> typing . Union [ discord . Color , int ] :
298315 color = self .config .get ('mod_color' )
299316 if not color :
300317 return discord .Color .green ()
@@ -307,7 +324,7 @@ def mod_color(self):
307324 return color
308325
309326 @property
310- def recipient_color (self ):
327+ def recipient_color (self ) -> typing . Union [ discord . Color , int ] :
311328 color = self .config .get ('recipient_color' )
312329 if not color :
313330 return discord .Color .gold ()
@@ -320,7 +337,7 @@ def recipient_color(self):
320337 return color
321338
322339 @property
323- def main_color (self ):
340+ def main_color (self ) -> typing . Union [ discord . Color , int ] :
324341 color = self .config .get ('main_color' )
325342 if not color :
326343 return discord .Color .blurple ()
@@ -414,7 +431,7 @@ async def on_ready(self):
414431
415432 logger .info (LINE )
416433
417- async def convert_emoji (self , name ) :
434+ async def convert_emoji (self , name : str ) -> str :
418435 ctx = SimpleNamespace (bot = self , guild = self .modmail_guild )
419436 converter = commands .EmojiConverter ()
420437
@@ -425,7 +442,7 @@ async def convert_emoji(self, name):
425442 logger .warning (f'{ name } is not a valid emoji.' )
426443 return name
427444
428- async def retrieve_emoji (self ):
445+ async def retrieve_emoji (self ) -> typing . Tuple [ str , str ] :
429446
430447 # TODO: use a function to convert emojis
431448
@@ -461,7 +478,7 @@ async def retrieve_emoji(self):
461478
462479 return sent_emoji , blocked_emoji
463480
464- async def process_modmail (self , message ) :
481+ async def process_modmail (self , message : discord . Message ) -> None :
465482 """Processes messages sent to the bot."""
466483 sent_emoji , blocked_emoji = await self .retrieve_emoji ()
467484 now = datetime .utcnow ()
@@ -631,7 +648,8 @@ async def get_context(self, message, *, cls=commands.Context):
631648
632649 return ctx
633650
634- async def update_perms (self , name , value , add = True ):
651+ async def update_perms (self , name : typing .Union [PermissionLevel , str ],
652+ value : int , add : bool = True ) -> None :
635653 if isinstance (name , PermissionLevel ):
636654 permissions = self .config .level_permissions
637655 name = name .name
@@ -862,7 +880,7 @@ async def on_command_error(self, ctx, exception):
862880 logger .error (error ('Unexpected exception:' ), exc_info = exception )
863881
864882 @staticmethod
865- def overwrites (ctx ) :
883+ def overwrites (ctx : commands . Context ) -> dict :
866884 """Permission overwrites for the guild."""
867885 overwrites = {
868886 ctx .guild .default_role : discord .PermissionOverwrite (
0 commit comments