def _configure_logging(self): level_text = self.config.log_level.upper() logging_levels = { "CRITICAL": logging.CRITICAL, "ERROR": logging.ERROR, "WARNING": logging.WARNING, "INFO": logging.INFO, "DEBUG": logging.DEBUG, } log_file_name = self.config.token.split(".")[0] ch_debug = logging.FileHandler(os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+") ch_debug.setLevel(logging.DEBUG) formatter_debug = FileFormatter( "%(asctime)s %(filename)s - " "%(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) ch_debug.setFormatter(formatter_debug) logger.addHandler(ch_debug) log_level = logging_levels.get(level_text) logger.info(LINE) if log_level is not None: logger.setLevel(log_level) ch.setLevel(log_level) logger.info(info("Logging level: " + level_text)) else: logger.info(error("Invalid logging level set. ")) logger.info(info("Using default logging level: INFO"))
def run(self, *args, **kwargs): try: self.loop.run_until_complete(self.start(self.token)) except discord.LoginFailure: logger.critical(error("Invalid token")) except KeyboardInterrupt: pass except Exception: logger.critical(error("Fatal exception"), exc_info=True) finally: try: self.metadata_task.cancel() self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: logger.debug(info("data_task has been cancelled.")) try: self.autoupdate_task.cancel() self.loop.run_until_complete(self.autoupdate_task) except asyncio.CancelledError: logger.debug(info("autoupdate_task has been cancelled.")) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): task.cancel() try: self.loop.run_until_complete( asyncio.gather(*asyncio.Task.all_tasks())) except asyncio.CancelledError: logger.debug(info("All pending tasks has been cancelled.")) finally: self.loop.run_until_complete(self.session.close()) self.loop.close() logger.info(error(" - Shutting down bot - "))
async def retrieve_emoji(self) -> typing.Tuple[str, str]: sent_emoji = self.config.get("sent_emoji", "✅") blocked_emoji = self.config.get("blocked_emoji", "🚫") if sent_emoji != "disable": try: sent_emoji = await self.convert_emoji(sent_emoji) except commands.BadArgument: logger.warning(info("Removed sent emoji (%s)."), sent_emoji) del self.config.cache["sent_emoji"] await self.config.update() sent_emoji = "✅" if blocked_emoji != "disable": try: blocked_emoji = await self.convert_emoji(blocked_emoji) except commands.BadArgument: logger.warning(info("Removed blocked emoji (%s)."), blocked_emoji) del self.config.cache["blocked_emoji"] await self.config.update() blocked_emoji = "🚫" return sent_emoji, blocked_emoji
async def validate_api_token(self): try: self.config.modmail_api_token except KeyError: logger.critical(error(f'MODMAIL_API_TOKEN not found.')) logger.critical( error('Set a config variable called ' 'MODMAIL_API_TOKEN with a token from ' 'https://dashboard.modmail.tk.')) logger.critical( error('If you want to self-host logs, ' 'input a MONGO_URI config variable.')) logger.critical( error('A Modmail API token is not needed ' 'if you are self-hosting logs.')) return await self.logout() else: valid = await self.api.validate_token() if not valid: logger.critical( error('Invalid MODMAIL_API_TOKEN - get one ' 'from https://dashboard.modmail.tk')) return await self.logout() user = await self.api.get_user_info() username = user['user']['username'] logger.info(info('Validated token.')) logger.info(info('GitHub user: ' + username))
async def retrieve_emoji(self): # TODO: use a function to convert emojis ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() sent_emoji = self.config.get('sent_emoji', '✅') blocked_emoji = self.config.get('blocked_emoji', '🚫') if sent_emoji not in UNICODE_EMOJI: try: sent_emoji = await converter.convert(ctx, sent_emoji.strip(':')) except commands.BadArgument: if sent_emoji != 'disable': logger.warning( info(f'Sent Emoji ({sent_emoji}) ' f'is not a valid emoji.')) del self.config.cache['sent_emoji'] await self.config.update() if blocked_emoji not in UNICODE_EMOJI: try: blocked_emoji = await converter.convert( ctx, blocked_emoji.strip(':')) except commands.BadArgument: if blocked_emoji != 'disable': logger.warning( info(f'Blocked emoji ({blocked_emoji}) ' 'is not a valid emoji.')) del self.config.cache['blocked_emoji'] await self.config.update() return sent_emoji, blocked_emoji
def actions(self): """ Executes every action """ at("actions") for action in self.timeline.actions(): info(action, "is execing") action.listeners['exec']()
async def autoupdate_loop(self): await self.wait_until_ready() if self.config.get("disable_autoupdates"): logger.warning(info("Autoupdates disabled.")) logger.info(LINE) return if not self.config.get("github_access_token"): logger.warning(info("GitHub access token not found.")) logger.warning(info("Autoupdates disabled.")) logger.info(LINE) return logger.info(info("Autoupdate loop started.")) while not self.is_closed(): changelog = await Changelog.from_url(self) latest = changelog.latest_version if parse_version(self.version) < parse_version(latest.version): data = await self.api.update_repository() embed = discord.Embed(color=discord.Color.green()) commit_data = data["data"] user = data["user"] embed.set_author( name=user["username"] + " - Updating Bot", icon_url=user["avatar_url"], url=user["url"], ) embed.set_footer( text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" ) embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) if commit_data: message = commit_data["commit"]["message"] html_url = commit_data["html_url"] short_sha = commit_data["sha"][:6] embed.add_field( name="Merge Commit", value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}", ) logger.info(info("Bot has been updated.")) channel = self.log_channel await channel.send(embed=embed) await asyncio.sleep(3600)
def _load_extensions(self): """Adds commands automatically""" self.remove_command('help') logger.info(LINE) logger.info(info(' _____ _ _ ______ _____ ')) logger.info(info('| __ \(_) | | | ____| __ \ ')) logger.info(info('| | | |_ ___ ___ ___ _ __ __| | | |__ | |__) |')) logger.info(info('| | | | / __|/ __/ _ \| __/ _` | | __| | _ / ')) logger.info(info('| |__| | \__ \ (_| (_) | | | (_| |_| | | | \ \ ')) logger.info( info('|_____/|_|___/\___\___/|_| \__,_(_)_| |_| \_\ ')) logger.info(info(f'v{__version__}')) logger.info(info('Authors: kyb3r, fourjr, Taaku18')) logger.info(LINE) for file in os.listdir('cogs'): if not file.endswith('.py'): continue cog = f'cogs.{file[:-3]}' logger.info(info(f'Loading {cog}')) try: self.load_extension(cog) except Exception: logger.exception(error(f'Failed to load {cog}'))
async def on_ready(self): """Bot startup, sets uptime.""" await self._connected.wait() logger.info(LINE) logger.info(info('Client ready.')) logger.info(LINE) logger.info(info(f'Logged in as: {self.user}')) logger.info(info(f'User ID: {self.user.id}')) logger.info(info(f'Guild ID: {self.guild.id if self.guild else 0}')) logger.info(LINE) if not self.guild: logger.error( error('WARNING - The GUILD_ID ' 'provided does not exist!')) else: await self.threads.populate_cache() # Wait until config cache is populated with stuff from db await self.config.wait_until_ready() # closures closures = self.config.closures.copy() logger.info( info(f'There are {len(closures)} thread(s) ' 'pending to be closed.')) for recipient_id, items in closures.items(): after = (datetime.fromisoformat(items['time']) - datetime.utcnow()).total_seconds() if after < 0: after = 0 recipient = self.get_user(int(recipient_id)) thread = await self.threads.find(recipient=recipient) if not thread: # If the recipient is gone or channel is deleted self.config.closures.pop(str(recipient_id)) await self.config.update() continue # TODO: Low priority, # Retrieve messages/replies when bot is down, from history? await thread.close(closer=self.get_user(items['closer_id']), after=after, silent=items['silent'], delete_channel=items['delete_channel'], message=items['message']) logger.info(LINE)
async def autoupdate_loop(self): await self.wait_until_ready() if self.config.get('disable_autoupdates'): logger.warning(info('Autoupdates disabled.')) logger.info(LINE) return if not self.config.get('github_access_token'): logger.warning(info('GitHub access token not found.')) logger.warning(info('Autoupdates disabled.')) logger.info(LINE) return logger.info(info('Autoupdate loop started.')) while not self.is_closed(): changelog = await Changelog.from_url(self) latest = changelog.latest_version if parse_version(self.version) < parse_version(latest.version): data = await self.api.update_repository() embed = discord.Embed(color=discord.Color.green()) commit_data = data['data'] user = data['user'] embed.set_author(name=user['username'] + ' - Updating Bot', icon_url=user['avatar_url'], url=user['url']) embed.set_footer(text=f'Updating Modmail v{self.version} ' f'-> v{latest.version}') embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) if commit_data: message = commit_data['commit']['message'] html_url = commit_data["html_url"] short_sha = commit_data['sha'][:6] embed.add_field(name='Merge Commit', value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}") logger.info(info('Bot has been updated.')) channel = self.log_channel await channel.send(embed=embed) await asyncio.sleep(3600)
async def on_ready(self): """Bot startup, sets uptime.""" await self._connected.wait() logger.info(LINE) logger.info(info("Client ready.")) logger.info(LINE) logger.info(info(f"Logged in as: {self.user}")) logger.info(info(f"User ID: {self.user.id}")) logger.info(info(f"Guild ID: {self.guild.id if self.guild else 0}")) logger.info(LINE) if not self.guild: logger.error( error("WARNING - The GUILD_ID " "provided does not exist!")) else: await self.threads.populate_cache() # Wait until config cache is populated with stuff from db await self.config.wait_until_ready() # closures closures = self.config.closures.copy() logger.info( info(f"There are {len(closures)} thread(s) " "pending to be closed.")) for recipient_id, items in closures.items(): after = (datetime.fromisoformat(items["time"]) - datetime.utcnow()).total_seconds() if after < 0: after = 0 thread = await self.threads.find(recipient_id=int(recipient_id)) if not thread: # If the channel is deleted self.config.closures.pop(str(recipient_id)) await self.config.update() continue await thread.close( closer=self.get_user(items["closer_id"]), after=after, silent=items["silent"], delete_channel=items["delete_channel"], message=items["message"], ) logger.info(LINE)
async def load_plugin(self, username, repo, plugin_name): ext = f'plugins.{username}-{repo}.{plugin_name}.{plugin_name}' dirname = f'plugins/{username}-{repo}/{plugin_name}' if 'requirements.txt' in os.listdir(dirname): # Install PIP requirements try: await self.bot.loop.run_in_executor( None, self._asubprocess_run, f'python3 -m pip install -U -r {dirname}/' 'requirements.txt --user -q -q') # -q -q (quiet) # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf8').strip() if err: raise DownloadError( f'Unable to download requirements: ```\n{error}\n```' ) from exc else: if not os.path.exists(site.USER_SITE): os.makedirs(site.USER_SITE) sys.path.insert(0, site.USER_SITE) try: self.bot.load_extension(ext) except commands.ExtensionError as exc: raise DownloadError('Invalid plugin') from exc else: msg = f'Loaded plugins.{username}-{repo}.{plugin_name}' logger.info(info(msg))
async def metadata_loop(self): await self.wait_until_ready() owner = (await self.application_info()).owner while not self.is_closed(): data = { "owner_name": str(owner), "owner_id": owner.id, "bot_id": self.user.id, "bot_name": str(self.user), "avatar_url": str(self.user.avatar_url), "guild_id": self.guild_id, "guild_name": self.guild.name, "member_count": len(self.guild.members), "uptime": (datetime.utcnow() - self.start_time).total_seconds(), "latency": f"{self.ws.latency * 1000:.4f}", "version": self.version, "selfhosted": True, "last_updated": str(datetime.utcnow()), } async with self.session.post("https://api.modmail.tk/metadata", json=data): logger.debug(info("Uploading metadata to Modmail server.")) await asyncio.sleep(3600)
async def validate_database_connection(self): try: await self.db.command("buildinfo") except Exception as exc: logger.critical( error("Something went wrong " "while connecting to the database.")) message = f"{type(exc).__name__}: {str(exc)}" logger.critical(error(message)) if "ServerSelectionTimeoutError" in message: logger.critical( error("This may have been caused by not whitelisting " "IPs correctly. Make sure to whitelist all " "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png")) if "OperationFailure" in message: logger.critical( error( "This is due to having invalid credentials in your MONGO_URI." )) logger.critical( error( "Recheck the username/password and make sure to url encode them. " "https://www.urlencoder.io/")) return await self.logout() else: logger.info(info("Successfully connected to the database."))
async def on_connect(self): logger.info(LINE) if not self.self_hosted: logger.info(info('MODE: Using the Modmail API')) logger.info(LINE) await self.validate_api_token() logger.info(LINE) else: logger.info(info('Mode: Self-hosting logs.')) await self.validate_database_connection() logger.info(LINE) logger.info(info('Connected to gateway.')) await self.config.refresh() if self.db: await self.setup_indexes() self._connected.set()
async def setup_indexes(self): """Setup text indexes so we can use the $search operator""" coll = self.db.logs index_name = 'messages.content_text_messages.author.name_text' index_info = await coll.index_information() # Backwards compatibility old_index = 'messages.content_text' if old_index in index_info: logger.info(info(f'Dropping old index: {old_index}')) await coll.drop_index(old_index) if index_name not in index_info: logger.info(info('Creating "text" index for logs collection.')) logger.info(info('Name: ' + index_name)) await coll.create_index([('messages.content', 'text'), ('messages.author.name', 'text')])
async def on_connect(self): logger.info(LINE) await self.validate_database_connection() logger.info(LINE) logger.info(info("Connected to gateway.")) await self.config.refresh() if self.db: await self.setup_indexes() self._connected.set()
async def validate_database_connection(self): try: await self.db.command('buildinfo') except Exception as exc: logger.critical(error('Something went wrong ' 'while connecting to the database.')) logger.critical(error(f'{type(exc).__name__}: {str(exc)}')) return await self.logout() else: logger.info(info('Successfully connected to the database.'))
def _configure_logging(self): level_text = self.config.log_level.upper() logging_levels = { 'CRITICAL': logging.CRITICAL, 'ERROR': logging.ERROR, 'WARNING': logging.WARNING, 'INFO': logging.INFO, 'DEBUG': logging.DEBUG, } log_level = logging_levels.get(level_text) logger.info(LINE) if log_level is not None: logger.setLevel(log_level) ch.setLevel(log_level) logger.info(info('Logging level: ' + level_text)) else: logger.info(error('Invalid logging level set. ')) logger.info(info('Using default logging level: INFO'))
def _configure_logging(self): level_text = self.config.log_level.upper() logging_levels = { "CRITICAL": logging.CRITICAL, "ERROR": logging.ERROR, "WARNING": logging.WARNING, "INFO": logging.INFO, "DEBUG": logging.DEBUG, } log_level = logging_levels.get(level_text) logger.info(LINE) if log_level is not None: logger.setLevel(log_level) ch.setLevel(log_level) logger.info(info("Logging level: " + level_text)) else: logger.info(error("Invalid logging level set. ")) logger.info(info("Using default logging level: INFO"))
async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() if name not in UNICODE_EMOJI: try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument: logger.warning(info("%s is not a valid emoji."), name) raise return name
async def setup_indexes(self): """Setup text indexes so we can use the $search operator""" coll = self.db.logs index_name = "messages.content_text_messages.author.name_text_key_text" index_info = await coll.index_information() # Backwards compatibility old_index = "messages.content_text_messages.author.name_text" if old_index in index_info: logger.info(info(f"Dropping old index: {old_index}")) await coll.drop_index(old_index) if index_name not in index_info: logger.info(info('Creating "text" index for logs collection.')) logger.info(info("Name: " + index_name)) await coll.create_index([ ("messages.content", "text"), ("messages.author.name", "text"), ("key", "text"), ])
def check_exits(self, dep): """ Checks if any of the exits dependant on dep trigger, and cleans up after him (remove, recursivly call deps) """ at("check_exits") for exit in self.exits[dep]: #Loops through all the exits dependant on this change for being in self.being_list: if exit.condition(being, self): info(exit.name, "triggered, changes:", exit.changes) exit.effect(being, self) self.remove_being(being) for change in exit.changes: info("checking", change) self.check_exits(change) if not len(self.beings): #If the length has a False value (e.g, 0) self.end = True else: print(len(self.beings), "beings left")
async def load_plugin(self, username, repo, plugin_name, branch): ext = f"plugins.{username}-{repo}-{branch}.{plugin_name}.{plugin_name}" dirname = f"plugins/{username}-{repo}-{branch}/{plugin_name}" if "requirements.txt" in os.listdir(dirname): # Install PIP requirements venv = hasattr(sys, "real_prefix") # in a virtual env user_install = "--user" if not venv else "" try: if os.name == "nt": # Windows await self.bot.loop.run_in_executor( None, self._asubprocess_run, f"pip install -r {dirname}/requirements.txt {user_install} -q -q", ) else: await self.bot.loop.run_in_executor( None, self._asubprocess_run, f"python3 -m pip install -U -r {dirname}/" f"requirements.txt {user_install} -q -q", ) # -q -q (quiet) # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode("utf8").strip() if err: msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" logger.error(error(msg)) raise DownloadError( f"Unable to download requirements: ```\n{err}\n```" ) from exc else: if not os.path.exists(site.USER_SITE): os.makedirs(site.USER_SITE) sys.path.insert(0, site.USER_SITE) await asyncio.sleep(0.5) try: self.bot.load_extension(ext) except commands.ExtensionError as exc: msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]" logger.error(error(msg)) raise DownloadError("Invalid plugin") from exc else: msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]" logger.info(info(msg))
async def retrieve_emoji(self) -> typing.Tuple[str, str]: sent_emoji = self.config.get('sent_emoji', '✅') blocked_emoji = self.config.get('blocked_emoji', '🚫') if sent_emoji != 'disable': try: sent_emoji = await self.convert_emoji(sent_emoji) except commands.BadArgument: logger.warning(info('Removed sent emoji (%s).'), sent_emoji) del self.config.cache['sent_emoji'] await self.config.update() sent_emoji = '✅' if blocked_emoji != 'disable': try: blocked_emoji = await self.convert_emoji(blocked_emoji) except commands.BadArgument: logger.warning(info('Removed blocked emoji (%s).'), blocked_emoji) del self.config.cache['blocked_emoji'] await self.config.update() blocked_emoji = '🚫' return sent_emoji, blocked_emoji
async def load_plugin(self, username, repo, plugin_name): ext = f"plugins.{username}-{repo}.{plugin_name}.{plugin_name}" dirname = f"plugins/{username}-{repo}/{plugin_name}" if "requirements.txt" in os.listdir(dirname): # Install PIP requirements try: if os.name == "nt": # Windows await self.bot.loop.run_in_executor( None, self._asubprocess_run, f"pip install -r {dirname}/requirements.txt -q -q", ) else: await self.bot.loop.run_in_executor( None, self._asubprocess_run, f"python3 -m pip install -U -r {dirname}/" "requirements.txt --user -q -q", ) # -q -q (quiet) # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode("utf8").strip() if err: raise DownloadError( f"Unable to download requirements: ```\n{error}\n```" ) from exc else: if not os.path.exists(site.USER_SITE): os.makedirs(site.USER_SITE) sys.path.insert(0, site.USER_SITE) try: self.bot.load_extension(ext) except commands.ExtensionError as exc: # TODO: Add better error handling for plugin load faliure import traceback traceback.print_exc() raise DownloadError("Invalid plugin") from exc else: msg = f"Loaded plugins.{username}-{repo}.{plugin_name}" logger.info(info(msg))
async def download_initial_plugins(self): await self.bot._connected.wait() for i in self.bot.config.plugins: parsed_plugin = self.parse_plugin(i) try: await self.download_plugin_repo(*parsed_plugin[:-1]) except DownloadError as exc: msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' logger.error(error(msg)) else: try: await self.load_plugin(*parsed_plugin) except DownloadError as exc: msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' logger.error(error(msg)) await async_all(env() for env in self.bot.extra_events.get('on_plugin_ready', [])) logger.debug(info('on_plugin_ready called.'))
async def update_perms(self, name, value, add=True): if isinstance(name, PermissionLevel): permissions = self.config.level_permissions name = name.name else: permissions = self.config.command_permissions if name not in permissions: if add: permissions[name] = [value] else: if add: if value not in permissions[name]: permissions[name].append(value) else: if value in permissions[name]: permissions[name].remove(value) logger.info( info(f'Updating permissions for {name}, {value} (add={add}).')) await self.config.update()
async def update_perms( self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True ) -> None: if isinstance(name, PermissionLevel): permissions = self.config.level_permissions name = name.name else: permissions = self.config.command_permissions if name not in permissions: if add: permissions[name] = [value] else: if add: if value not in permissions[name]: permissions[name].append(value) else: if value in permissions[name]: permissions[name].remove(value) logger.info(info(f"Updating permissions for {name}, {value} (add={add}).")) await self.config.update()
async def login(cls, bot: Bot) -> 'GitHub': """ Logs in to GitHub with configuration variable information. Parameters ---------- bot : Bot The Modmail bot. Returns ------- GitHub The newly created `GitHub` object. """ self = cls(bot, bot.config.get('github_access_token'), ) resp: dict = await self.request('https://api.github.com/user') self.username: str = resp['login'] self.avatar_url: str = resp['avatar_url'] self.url: str = resp['html_url'] logger.info(info(f'GitHub logged in to: {self.username}')) return self
async def download_initial_plugins(self): await self.bot._connected.wait() for i in self.bot.config.plugins: username, repo, name, branch = self.parse_plugin(i) try: await self.download_plugin_repo(username, repo, branch) except DownloadError as exc: msg = f"{username}/{repo}@{branch} - {exc}" logger.error(error(msg)) else: try: await self.load_plugin(username, repo, name, branch) except DownloadError as exc: msg = f"{username}/{repo}@{branch}[{name}] - {exc}" logger.error(error(msg)) await async_all( env() for env in self.bot.extra_events.get("on_plugin_ready", [])) logger.debug(info("on_plugin_ready called."))
def _load_extensions(self): """Adds commands automatically""" logger.info(LINE) logger.info(info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")) logger.info(info("││││ │ │││││├─┤││")) logger.info(info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")) logger.info(info(f"v{__version__}")) logger.info(info("Authors: kyb3r, fourjr, Taaku18")) logger.info(LINE) for file in os.listdir("cogs"): if not file.endswith(".py"): continue cog = f"cogs.{file[:-3]}" logger.info(info(f"Loading {cog}")) try: self.load_extension(cog) except Exception: logger.exception(error(f"Failed to load {cog}"))