def __init__(self, bot): self.bot = bot # type: commands.Bot self.config = get_kaztron_config() self.ch_output = discord.Object( self.config.get("discord", "channel_output")) self.ch_log = discord.Object(self.config.get('modnotes', 'channel_log'))
def check_admin(ctx: commands.Context): """ Check if the sender of a command is an admin (as defined by the roles in the "discord" -> "admin_roles" config). """ config = get_kaztron_config() return check_role(config.get("discord", "admin_roles", []), ctx.message)
def __init__(self, bot): self.bot = bot self.config = get_kaztron_config() self.dest_output = discord.Object( id=self.config.get('discord', 'channel_output')) self.dest_welcome = discord.Object( id=self.config.get("welcome", "channel_welcome"))
def format_date(d: Union[datetime, date]) -> str: """ Format a datetime object as a date (as specified in config). :param d: The date or datetime object to format. :return: """ return d.strftime(get_kaztron_config().get('core', 'date_format'))
def __init__(self, bot): self.bot = bot self.config = get_kaztron_config() self.ch_request = discord.Object( self.config.get('core', 'channel_request')) self.dest_output = discord.Object( id=self.config.get('discord', 'channel_output')) self.name = self.config.get("core", "name", "KazTron")
def __init__(self, bot): self.bot = bot self.config = get_kaztron_config() self.distinguish_map = self.config.get("modtools", "distinguish_map", {}) self.wb_images = self.config.get("modtools", "wb_images", []) self.ch_output = discord.Object( self.config.get("discord", "channel_output"))
def __init__(self, bot): self.bot = bot # type: discord.Client self.config = get_kaztron_config() self.dest_output = discord.Object(id=self.config.get('discord', 'channel_output')) self.voice_channel_ids = self.config.get('role_man', 'channels_voice') self.role_voice_name = self.config.get('role_man', 'role_voice') self.role_voice = None self.voice_feature = False
def format_datetime(dt: datetime, seconds=False) -> str: """ Format a datetime object as a datetime (as specified in config). :param dt: The datetime object to format. :param seconds: Whether or not to display seconds (this determines which config format to use). :return: """ format_key = 'datetime_format' if not seconds else 'datetime_seconds_format' return dt.strftime(get_kaztron_config().get('core', format_key))
def static_init(cls): """ Executes one-time class setup. Called on KazCog __init__ to verify that setup. """ if cls._config is None: cls._config = get_kaztron_config() cls._ch_out_id = cls._config.get("discord", "channel_output") cls._ch_test_id = cls._config.get("discord", "channel_test") if cls._state is None: cls._state = get_runtime_config()
def __init__(self, bot): self.bot = bot self.config = get_kaztron_config() try: self.filter_cfg = get_runtime_config() except OSError as e: logger.error(str(e)) raise RuntimeError("Failed to load runtime config") from e self._make_default_config() self._load_filter_rules() self.dest_output = None self.dest_warning = None self.dest_current = None
def __init__(self, parser: CoreHelpParser, bot: commands.Bot): self.parser = parser self.bot = bot self.output = None # type: list self.commands = None # type: list self.cog = None # type: KazCog self.section = [] self.context = None # type: commands.Context config = get_kaztron_config() config.set_section_view('help', HelpSectionView) self.config: HelpSectionView = config.help self.name = config.core.name
def in_channels_cfg(config_section: str, config_name: str, allow_pm=False, delete_on_fail=False, *, include_mods=False, include_admins=False, include_bot=False, check_id=CheckId.C_LIST): """ Command check decorator. Only allow this command to be run in specific channels (as specified from the config). The configuration can point to either a single channel ID string or a list of channel ID strings. l :param config_section: The KaztronConfig section to access :param config_name: The KaztronConfig key containing the list of channel IDs :param allow_pm: Allow this command in PMs, as well as the configured channels :param delete_on_fail: If this check fails, delete the original message. This option does not delete the message itself, but throws an UnauthorizedChannelDelete error to allow an ``on_command_error`` handler to take appropriate action. :param include_mods: Also include mod channels. :param include_admin: Also include admin channels. :param include_bot: Also include bot and test channels. :raise UnauthorizedChannelError: :raise UnauthorizedChannelDelete: """ config = get_kaztron_config() config_channels = config.get(config_section, config_name) if isinstance(config_channels, str): config_channels = [config_channels] if include_mods: config_channels.extend(config.get('discord', 'mod_channels')) if include_admins: config_channels.extend(config.get('discord', 'admin_channels')) if include_bot: config_channels.append(config.get('discord', 'channel_test')) config_channels.append(config.get('discord', 'channel_public')) return in_channels(config_channels, allow_pm, delete_on_fail, check_id=check_id)
def __init__(self, bot): self.bot = bot self.config = get_kaztron_config() try: self.filter_cfg = get_runtime_config() except OSError as e: logger.error(str(e)) raise RuntimeError("Failed to load runtime config") from e self.filter_cfg.set_defaults('filter', warn=[], delete=[], channel=self.config.get( 'filter', 'channel_warning')) self._load_filter_rules() self.dest_output = None self.dest_warning = None self.dest_current = None
def admin_only(): """ Command check decorator. Only allow admins to execute this command (as defined by the roles in the "discord" -> "admin_roles" config). """ config = get_kaztron_config() def predicate(ctx: commands.Context): if check_role(config.get("discord", "admin_roles", []), ctx.message): logger.info("Validated {!s} as bot administrator".format( ctx.message.author)) return True else: raise AdminOnlyError("Only administrators may use this command.", ctx) return commands.check(predicate)
def in_channels_cfg(config_section: str, config_name: str, allow_pm=False): """ Command check decorator. Only allow this command to be run in specific channels (as specified from the config). """ config = get_kaztron_config() def predicate(ctx: commands.Context): pm_exemption = allow_pm and ctx.message.channel.is_private if ctx.message.channel.id in config.get(config_section, config_name) or pm_exemption: logger.info("Validated command in allowed channel {!s}".format( ctx.message.channel)) return True else: raise UnauthorizedChannelError("Command not allowed in channel.", ctx) return commands.check(predicate)
def __init__(self, bot): self.bot = bot self.config = get_kaztron_config() try: self.db = get_runtime_config() except OSError as e: logger.error(str(e)) raise RuntimeError("Failed to load runtime config") from e self.db.set_defaults('spotlight', current=-1, queue=[]) self.dest_output = None self.dest_spotlight = None self.role_audience_name = self.config.get('spotlight', 'audience_role') self.role_host_name = self.config.get('spotlight', 'host_role') self.user_agent = self.config.get("core", "name") self.gsheet_id = self.config.get("spotlight", "spreadsheet_id") self.gsheet_range = self.config.get("spotlight", "spreadsheet_range") self.applications = [] self.applications_last_refresh = 0 self.current_app_index = int(self.db.get('spotlight', 'current', -1)) self.queue_data = deque(self.db.get('spotlight', 'queue', []))
class DiceCog: config = get_kaztron_config() ch_allowed_list = (config.get('dice', 'channel_dice'), config.get("discord", "channel_test"), config.get("discord", "channel_output")) def __init__(self, bot): self.bot = bot self.ch_dice = None async def on_ready(self): self.ch_dice = self.bot.get_channel( self.config.get('dice', 'channel_dice')) if self.ch_dice is None: raise ValueError("Channel {} not found".format( self.config.get('dice', 'channel_dice'))) @commands.command(pass_context=True, ignore_extra=False, aliases=['rolls']) @in_channels(ch_allowed_list) async def roll(self, ctx, dice: str): """ Rolls dice. Rolls a <sides>-sided die <num> times, and reports the rolls and total. Example: `.rolls 3d6` rolls three six-sided dice. """ logger.info("roll: {}".format(message_log_str(ctx.message))) try: num_rolls, num_sides = map(int, dice.split('d')) except ValueError: err_msg = "Invalid format: {}".format(message_log_str(ctx.message)) logger.warning("rolls(): " + err_msg) await self.bot.say('Invalid format. Please enter `.rolls XdY`, ' 'where X and Y are positive whole numbers.') return if num_rolls <= 0: logger.warning("rolls(): arguments out of range") await self.bot.say("You have to roll at least 1 die.") elif num_sides <= 1: logger.warning("rolls(): arguments out of range") await self.bot.say("Dice must have at least 2 sides.") elif num_sides > 100 or num_rolls > 100: logger.warning("rolls(): arguments out of range") await self.bot.say( "The limit for dice number and sides is 100 each.") else: result = [random.randint(1, num_sides) for _ in range(num_rolls)] total = sum(result) await self.bot.say("{!s}\n**Sum:** {:d}".format(result, total)) logger.info("Rolled dice: {:d}d{:d} = {!r} (sum={})".format( num_rolls, num_sides, result, total)) @commands.command(pass_context=True, ignore_extra=False) @in_channels(ch_allowed_list) async def rollf(self, ctx): """ Rolls four dice for the FATE tabletop roleplaying game system. Arguments: None """ logger.info("roll: {}".format(message_log_str(ctx.message))) dice = (-1, -1, 0, 0, 1, 1) str_map = {-1: '-', 0: '0', 1: '+'} roll_results = [random.choice(dice) for _ in range(4)] total = sum(roll_results) rolls_str = [str_map[roll] for roll in roll_results] await self.bot.say("{!s}\n**Sum:** {:d}".format(rolls_str, total)) logger.info("Rolled FATE dice: {!r} (sum={})".format(rolls_str, total)) @roll.error @rollf.error async def roll_on_error(self, exc, ctx): cmd_string = message_log_str(ctx.message) if isinstance(exc, commands.CommandInvokeError): root_exc = exc.__cause__ if exc.__cause__ is not None else exc if isinstance(root_exc, errors.UnauthorizedChannelError): logger.error("Unauthorized use of command in #{1}: {0}".format( cmd_string, ctx.message.channel.name)) await self.bot.send_message( ctx.message.channel, "Sorry, this command can only be used in {}".format( self.ch_dice.mention)) else: core_cog = self.bot.get_cog("CoreCog") await core_cog.on_command_error(exc, ctx, force=True ) # Other errors can bubble up else: core_cog = self.bot.get_cog("CoreCog") await core_cog.on_command_error(exc, ctx, force=True ) # Other errors can bubble up
class DiceCog(KazCog): config = get_kaztron_config() ch_allowed_list = (config.get('dice', 'channel_dice'), config.get("discord", "channel_test"), config.get("discord", "channel_output")) MAX_CHOICES = 20 def __init__(self, bot): super().__init__(bot) self.ch_dice = None async def on_ready(self): self.ch_dice = self.validate_channel( self.config.get('dice', 'channel_dice')) await super().on_ready() @commands.command(pass_context=True, ignore_extra=False, aliases=['rolls']) @in_channels(ch_allowed_list) async def roll(self, ctx, dice: str): """ Rolls dice. Rolls a <sides>-sided die <num> times, and reports the rolls and total. Example: `.rolls 3d6` rolls three six-sided dice. """ logger.info("roll: {}".format(message_log_str(ctx.message))) try: num_rolls, num_sides = map(int, dice.split('d')) except ValueError: err_msg = "Invalid format: {}".format(message_log_str(ctx.message)) logger.warning("rolls(): " + err_msg) await self.bot.say('Invalid format. Please enter `.rolls XdY`, ' 'where X and Y are positive whole numbers.') return if num_rolls <= 0: logger.warning("rolls(): arguments out of range") await self.bot.say("You have to roll at least 1 die.") elif num_sides <= 1: logger.warning("rolls(): arguments out of range") await self.bot.say("Dice must have at least 2 sides.") elif num_sides > 100 or num_rolls > 100: logger.warning("rolls(): arguments out of range") await self.bot.say( "The limit for dice number and sides is 100 each.") else: result = [random.randint(1, num_sides) for _ in range(num_rolls)] total = sum(result) await self.bot.say("{!s}\n**Sum:** {:d}".format(result, total)) logger.info("Rolled dice: {:d}d{:d} = {!r} (sum={})".format( num_rolls, num_sides, result, total)) @commands.command(pass_context=True, ignore_extra=False) @in_channels(ch_allowed_list) async def rollf(self, ctx): """ Rolls four dice for the FATE tabletop roleplaying game system. Arguments: None """ logger.info("roll: {}".format(message_log_str(ctx.message))) dice = (-1, -1, 0, 0, 1, 1) str_map = {-1: '-', 0: '0', 1: '+'} roll_results = [random.choice(dice) for _ in range(4)] total = sum(roll_results) rolls_str = [str_map[roll] for roll in roll_results] await self.bot.say("[{}]\n**Sum:** {:d}".format( ' '.join(rolls_str), total)) logger.info("Rolled FATE dice: {!r} (sum={})".format(rolls_str, total)) @commands.command(pass_context=True, ignore_extra=False, no_pm=False) async def choose(self, ctx, *, choices: str): """ Need some help making a decision? Let the bot choose for you! Arguments: * choices - Two or more choices, separated by commas `,`. Examples: `.choose a, b, c` """ logger.info("choose: {}".format(message_log_str(ctx.message))) choices = list(map(str.strip, choices.split(","))) if "" in choices: logger.warning("choose(): argument empty") await self.bot.say("I cannot decide if there's an empty choice.") elif len(choices) < 2: logger.warning("choose(): arguments out of range") await self.bot.say("I need something to choose from.") elif len(choices) > self.MAX_CHOICES: logger.warning("choose(): arguments out of range") await self.bot.say("I don't know, that's too much to choose from! " "I can't handle more than {:d} choices!".format( self.MAX_CHOICES)) else: r = random.randint(0, len(choices) - 1) await self.bot.say(choices[r]) @roll.error @rollf.error async def roll_on_error(self, exc, ctx): cmd_string = message_log_str(ctx.message) if isinstance(exc, commands.CommandInvokeError): root_exc = exc.__cause__ if exc.__cause__ is not None else exc if isinstance(root_exc, errors.UnauthorizedChannelError): logger.error("Unauthorized use of command in #{1}: {0}".format( cmd_string, ctx.message.channel.name)) await self.bot.send_message( ctx.message.channel, "Sorry, this command can only be used in {}".format( channel_mention(self.ch_allowed_list[0]))) else: core_cog = self.bot.get_cog("CoreCog") await core_cog.on_command_error(exc, ctx, force=True ) # Other errors can bubble up else: core_cog = self.bot.get_cog("CoreCog") await core_cog.on_command_error(exc, ctx, force=True ) # Other errors can bubble up
async def main(): add_application_path() import sys from urllib.parse import urlparse, parse_qs import kaztron from kaztron.driver.reddit import RedditLoginManager from kaztron.config import get_kaztron_config, get_runtime_config config = get_kaztron_config() state = get_runtime_config() kaztron.KazCog.static_init(config, state) rlm = RedditLoginManager() try: cmd = sys.argv[1].lower() except IndexError: cmd = None if not cmd or cmd == 'login': print(f"KazTron {kaztron.__version__}: Reddit authorization tool\n\n" "This tool allows you to authorize KazTron on a Reddit account, in order to make use " "functionality which requires Reddit access.\n") scopes = rlm.get_extension_scopes() print('') print("Go to this URL to authorize a Reddit account:") print(rlm.get_authorization_url(scopes) + "\n") print("You will be redirected to a URL (it might fail to load, that's OK).\n") response_url = input("Paste the URL here: ") response_parts = urlparse(response_url) query_vars = parse_qs(response_parts.query) await rlm.authorize(query_vars['state'][0], query_vars['code'][0]) print("Done.") exit(0) elif cmd == 'logout': user = None try: user = sys.argv[2] except IndexError: print("Must specify a username to log out.") exit(2) try: await rlm.logout(user) except KeyError: print(f"User '{user}' not logged in.") exit(3) print(f"User '{user}' has been logged out. Note that this app's authorization will remain " "on the user's account and must be removed via the Reddit website.") exit(0) elif cmd == 'clear': await rlm.clear() print(f"All user sessions cleared. Note that this app's authorization will remain " "on user accounts and must be removed via the Reddit website.") exit(0) else: print("Usage: ./reddit_auth.py <login|logout <username>|clear>\n") exit(0)
class RedditLoginManager: """ Manages login credentials and OAuth flow for Reddit, and provides instances of the PRAW Reddit object that allows Reddit access. See `tools/reddit_auth.py` for a usage example. KazTron bot extensions can declare required Reddit scopes by defining a callable `get_reddit_scopes` at the MODULE level. This callable must return a list of scopes as strings, and must work upon being imported (i.e. without a call to `setup()` or an actual cog/bot initialisation). """ _global_config = get_kaztron_config() _global_state = get_runtime_config() _global_config.set_section_view('reddit', RedditConfig) _global_state.set_section_view('reddit', RedditState) _config = _global_config.get_section('reddit') # type: RedditConfig _config.set_defaults(refresh_uri='http://localhost:8080') _state = _global_state.get_section('reddit') # type: RedditState _state.set_defaults(refresh_tokens={}, last_auth_state=None) def __init__(self): # lazy cache of reddit instance objects self.reddit_cache = {u: None for u in self.users } # type: Dict[str, praw.Reddit] @property def users(self) -> Sequence[str]: return tuple(u for u in self._state.refresh_tokens.keys()) @property def refresh_tokens(self) -> MutableMapping[str, Optional[str]]: """ Return a dict of username -> refresh_token """ return self._state.refresh_tokens def get_reddit(self, username: str = None) -> apraw.Reddit: """ Get a :class:`praw.Reddit` instance for the specified user. :param username: Name of user account to work with. If None, first logged-in user currently configured. :return: A :class:`praw.Reddit` instance. :raise KeyError: User is not currently logged in. """ if username is None: # if no username specified, set to the first username username = self.users[0] if self.reddit_cache.get(username, None) is None: self.reddit_cache[username] = apraw.Reddit( client_id=self._config.client_id, client_secret=self._config.client_secret, refresh_token=self.refresh_tokens[username], user_agent=self._config.user_agent) return self.reddit_cache[username] def get_anonymous_reddit(self): return apraw.Reddit(client_id=self._config.client_id, client_secret=self._config.client_secret, redirect_uri=self._config.refresh_uri, user_agent=self._config.user_agent) def get_extension_scopes(self) -> Set[str]: """ Get reddit scopes required from extensions enabled in the configuration file. :raises ImportError: An extension specified in the configuration does not exist. """ import importlib scopes = {'identity'} for ext_name in self._global_config.get('core', 'extensions'): lib = importlib.import_module('kaztron.cog.' + ext_name) try: ext_scopes = lib.get_reddit_scopes() scopes.update(ext_scopes) logger.info( f"Extension {ext_name} defines scopes: {ext_scopes!s}") except AttributeError: logger.warning( f"Extension {ext_name} does not define get_reddit_scopes()" ) return scopes def get_authorization_url(self, scopes: Iterable[str] = ('identity', ), permanent=True) -> str: """ Get the URL a reddit account owner can use to authorize the bot to log in. This will invalidate any previously issued authorisation URLs. :param scopes: Reddit API authorisation scopes to request. :param permanent: Whether the authorisation should be permanent or temporary (time-limited). :return: URL """ with self._state as state: state.last_auth_state = secrets.token_urlsafe(16) return self.get_anonymous_reddit().auth.url( scopes, self._state.last_auth_state, 'permanent' if permanent else 'temporary') async def authorize(self, state: str, code: str) -> apraw.Reddit: """ Once a user has permitted the application, complete the authorization. :raise ValueError: State does not match state of last issued authorization URL: the authorization may not be one that this application initiated. """ logger.info("Checking authorisation...") if state != self._state.last_auth_state: raise ValueError( "State does not match stored state: authorization may not have been " "initiated by this application.") reddit = self.get_anonymous_reddit() refresh_token = await reddit.auth.authorize(code) with self._state as state: name = (await reddit.user.me()).name state.refresh_tokens[name] = refresh_token self.reddit_cache[name] = reddit logger.info(f"Authorised user {name}") return reddit async def logout(self, username: str): """ Doesn't really logout (not meaningful server-side), but clears stored authorization information for that user. :raises KeyError: User not logged in. """ del self.reddit_cache[username] del self._state.refresh_tokens[username] with self._state as state: state.refresh_tokens = self._state.refresh_tokens # force marking as modified logger.info(f"Logged out user {username}") async def clear(self): """ Clear all login information for all reddit users. """ logger.debug("Logging out all known users: {}".format(', '.join( self.users))) self.reddit_cache.clear() with self._state as state: state.refresh_tokens = {} logger.info("Logged out all users")
def run(loop: asyncio.AbstractEventLoop): """ Run the bot once. """ config = get_kaztron_config() state = get_runtime_config() kaztron.KazCog.static_init(config, state) # custom help formatters kaz_help_parser = CoreHelpParser({ 'name': config.core.get('name') }) # create bot instance (+ some custom hacks) client = commands.Bot( command_prefix='.', formatter=DiscordHelpFormatter(kaz_help_parser, show_check_failure=True), description='This an automated bot for the /r/worldbuilding discord server', pm_help=True) apply_patches(client) # KazTron-specific extension classes client.scheduler = Scheduler(client) client.kaz_help_parser = kaz_help_parser # Load core extension (core + rolemanager) client.load_extension("kaztron.core") # Load extensions startup_extensions = config.get("core", "extensions") for extension in startup_extensions: logger.debug("Loading extension: {}".format(extension)) # noinspection PyBroadException try: client.load_extension("kaztron.cog." + extension) except Exception: logger.exception('Failed to load extension {}'.format(extension)) sys.exit(ErrorCodes.EXTENSION_LOAD) # noinspection PyBroadException try: loop.run_until_complete(client.login(config.get("discord", "token"))) loop.run_until_complete(client.connect()) except KeyboardInterrupt: logger.info("Interrupted by user") logger.debug("Waiting for client to close...") loop.run_until_complete(client.close()) logger.info("Client closed.") sys.exit(ErrorCodes.OK) except Exception: logger.exception("Uncaught exception during bot execution") logger.debug("Waiting for client to close...") loop.run_until_complete(client.close()) logger.info("Client closed.") # Let the external retry reboot the bot - attempt recovery from errors # sys.exit(ErrorCodes.ERROR) return finally: logger.debug("Cancelling pending tasks...") # BEGIN CONTRIB # Modified from code from discord.py. # # Source: https://github.com/Rapptz/discord.py/blob/ # 09bd2f4de7cccbd5d33f61e5257e1d4dc96b5caa/discord/client.py#L517 # # Original code Copyright (c) 2015-2016 Rapptz. MIT licence. pending = asyncio.Task.all_tasks(loop=loop) gathered = asyncio.gather(*pending, loop=loop, return_exceptions=True) # noinspection PyBroadException try: gathered.cancel() loop.run_until_complete(gathered) gathered.exception() except Exception: pass # END CONTRIB KazCog.state.write()
import sys import logging import kaztron from kaztron import runner from kaztron.config import get_kaztron_config from kaztron.logging import setup_logging # In the loving memory of my time as a moderator of r/worldbuilding network # To the future dev, this whole thing is a mess that somehow works. Sorry for the inconvenience. # (Assuming this is from Kazandaki -- Laogeodritt) # load configuration try: config = get_kaztron_config(kaztron.cfg_defaults) except OSError as e: print(str(e), file=sys.stderr) sys.exit(runner.ErrorCodes.CFG_FILE) def stop_daemon(): import os import signal # noinspection PyPackageRequirements from daemon import pidfile print("Reading pidfile...") pidf = pidfile.TimeoutPIDLockFile(config.get('core', 'daemon_pidfile')) pid = pidf.read_pid() print("Stopping KazTron (PID={:d})...".format(pid)) os.kill(pid, signal.SIGINT)
def run(loop: asyncio.AbstractEventLoop): """ Run the bot once. """ config = get_kaztron_config() client = commands.Bot(command_prefix='.', description='This an automated bot for the /r/worldbuilding discord server', pm_help=True) patch_smart_quotes_hack(client) # Load extensions startup_extensions = config.get("core", "extensions") client.load_extension("kaztron.cog.core") for extension in startup_extensions: logger.debug("Loading extension: {}".format(extension)) # noinspection PyBroadException try: client.load_extension("kaztron.cog." + extension) except Exception: logger.exception('Failed to load extension {}'.format(extension)) sys.exit(ErrorCodes.EXTENSION_LOAD) # noinspection PyBroadException try: loop.run_until_complete(client.login(config.get("discord", "token"))) loop.run_until_complete(client.connect()) except KeyboardInterrupt: logger.info("Interrupted by user") logger.debug("Waiting for client to close...") loop.run_until_complete(client.close()) logger.info("Client closed.") sys.exit(ErrorCodes.OK) except Exception: logger.exception("Uncaught exception during bot execution") logger.debug("Waiting for client to close...") loop.run_until_complete(client.close()) logger.info("Client closed.") # Let the external retry reboot the bot - attempt recovery from errors # sys.exit(ErrorCodes.ERROR) return finally: logger.debug("Cancelling pending tasks...") # BEGIN CONTRIB # Modified from code from discord.py. # # Source: https://github.com/Rapptz/discord.py/blob/ # 09bd2f4de7cccbd5d33f61e5257e1d4dc96b5caa/discord/client.py#L517 # # Original code Copyright (c) 2015-2016 Rapptz. MIT licence. pending = asyncio.Task.all_tasks(loop=loop) gathered = asyncio.gather(*pending, loop=loop) # noinspection PyBroadException try: gathered.cancel() loop.run_until_complete(gathered) gathered.exception() except Exception: pass # END CONTRIB KazCog._state.write()