def __init__(self, **kwargs): KiwiBot._bot = self super().__init__(status='idle', **kwargs) # will be used as process exit code after stopping if not None self.exit_code = None # used to prevent reloading commands in on_ready after reconnect self.is_first_on_ready_event = True # timestamp of bot launch, filled in first on_ready call self.start_time = 0 self.proxies = None self.sess = None self.config = Config('config.json', loop=self.loop) logger.verbosity = self.config.get('logger_verbosity', logger.VERBOSITY_INFO) logger.add_file(self.config.get('logs_file', None)) logger.debug('Logger ................. connected') self.is_dev = self.config.get('is_dev', False) self.mm = ModuleManager(self) logger.debug('ModuleManager .......... connected') self.redis = RedisDB() logger.debug('RedisDB ................ connected') self._default_prefix = '+' self._mention_prefixes = [] self.prefixes = [] self._guild_prefixes = {} self._last_messages = {} self._leave_voice_channel_tasks = {} # currently processed commands self._processing_commands = {}
from objects import errors from objects.channel_logger import ChannelLogger from objects.config import Config from objects.glimcontext import GlimContext from objects.help_formatter import GlimmerHelpFormatter from objects.logger import Log from utils import canvases, http, render, sqlite as sql, utils from utils.version import VERSION def get_prefix(bot_, msg: discord.Message): return [sql.guild_get_prefix_by_id(msg.guild.id), bot_.user.mention + " "] \ if msg.guild else [cfg.prefix, bot_.user.mention + " "] cfg = Config() log = Log(''.join(cfg.name.split())) bot = commands.Bot(command_prefix=get_prefix, formatter=GlimmerHelpFormatter()) bot.remove_command('help') ch_log = ChannelLogger(bot) extensions = [ "commands.animotes", "commands.canvas", "commands.configuration", "commands.faction", "commands.general", "commands.template", ] sql.menu_locks_delete_all()
class KiwiBot(discord.AutoShardedClient): _bot = None def __init__(self, **kwargs): KiwiBot._bot = self super().__init__(status='idle', **kwargs) # will be used as process exit code after stopping if not None self.exit_code = None # used to prevent reloading commands in on_ready after reconnect self.is_first_on_ready_event = True # timestamp of bot launch, filled in first on_ready call self.start_time = 0 self.proxies = None self.sess = None self.config = Config('config.json', loop=self.loop) logger.verbosity = self.config.get('logger_verbosity', logger.VERBOSITY_INFO) logger.add_file(self.config.get('logs_file', None)) logger.debug('Logger ................. connected') self.is_dev = self.config.get('is_dev', False) self.mm = ModuleManager(self) logger.debug('ModuleManager .......... connected') self.redis = RedisDB() logger.debug('RedisDB ................ connected') self._default_prefix = '+' self._mention_prefixes = [] self.prefixes = [] self._guild_prefixes = {} self._last_messages = {} self._leave_voice_channel_tasks = {} # currently processed commands self._processing_commands = {} @staticmethod def get_bot(): return KiwiBot._bot @property def uptime(self): return time.time() - self.start_time async def init_prefixes(self): bot_id = self.user.id self.prefixes = [] self._default_prefix = (await self.redis.get('prefix', default='+')).lower() self._mention_prefixes = [f'<@{bot_id}>', f'<@!{bot_id}>'] self.prefixes.extend([self._default_prefix, *self._mention_prefixes]) self._guild_prefixes = {} for key in await self.redis.keys('guild_prefix:*'): guild_id = int(key.partition(':')[2]) self._guild_prefixes[guild_id] = (await self.redis.get(key)).lower() def run(self, token=None): if token is None: token = self.config.get('token', None) if token is None: token = input( 'Token not provided. Please, insert it into config file or paste here for single bot launch: ' ) super().run(token, reconnect=True, fetch_offline_members=True) def restart(self): self.stop(RESTART_EXIT_CODE) def stop(self, exit_code=STOP_EXIT_CODE, force=False): if force: import sys sys.exit(exit_code) self.redis.disconnect() logger.debug('Stopping event loop and cancelling tasks') self.loop.stop() tasks = asyncio.gather(*asyncio.Task.all_tasks(), loop=self.loop) tasks.cancel() self.exit_code = exit_code async def on_ready(self): if not self.is_first_on_ready_event: await self.mm.init_modules() logger.info('Bot reconnected') return self.is_first_on_ready_event = False self.proxies = self.config.get('proxies') self.sess = ClientSession() redis_port = self.config.get('redis_port', None) try: await self.redis.connect(port=redis_port) except ConnectionRefusedError: logger.info('Failed to connect to redis! Stopping bot') logger.info(traceback.format_exc()) self.stop(ERROR_EXIT_CODE, force=True) logger.info('Connected to redis db with %s keys' % await self.redis.get_db_size()) await self.mm.load_modules(strict_mode=False) logger.info('Loaded modules: [%s]' % ' '.join(self.mm.modules.keys())) await self.init_prefixes() self.start_time = time.time() logger.info(ASCII_ART) logger.info(f'Logged in as {self.user} with {len(self.guilds)} guilds') logger.info('Bot ready, good luck!') if self.is_dev: logger.info('Is a dev instance') logger.info('Default prefix: ' + self._default_prefix) async def close(self): await super().close() logger.info('Connection closed') async def on_message(self, msg, from_edit=False): self.register_last_user_message_timestamp(msg) if msg.author.bot: return if not from_edit: await self.track_message(msg) lower_content = msg.content.lower() prefixes = self.prefixes if msg.guild is not None: guild_prefixes = self._guild_prefixes.get(msg.guild.id, None) if guild_prefixes: prefixes = [guild_prefixes] + self._mention_prefixes else: prefixes = prefixes + [''] prefix = None for p in prefixes: if lower_content.startswith(p): prefix = p break if prefix is None: return await self.process_command(Context(self, msg, prefix), msg.content[len(p):].lstrip()) async def process_command(self, ctx, clean_content): module_response = await self.mm.check_modules(ctx, clean_content) if module_response: if not isinstance(module_response, str): return module_response = await formatters.format_response( module_response, ctx.message, self) if module_response: try: await ctx.send(module_response) except Exception: pass def register_last_user_message_timestamp(self, msg): # await self.redis.set( # f'last_message_timestamp:{msg.channel.id}:{msg.author.id}', # msg.created_at.timestamp(), 'EX', 86400 # ) if msg.channel.id not in self._last_messages: self._last_messages[msg.channel.id] = { msg.author.id: msg.edited_at or msg.created_at } else: self._last_messages[msg.channel.id][ msg.author.id] = msg.edited_at or msg.created_at async def on_message_edit(self, before, after): if after.author.bot: return if before.content == after.content: return if before.id in self._processing_commands: # cancel command self._processing_commands[before.id] = False if await self.redis.exists(f'tracked_message:{before.id}'): await self.clear_responses_to_message(before.id) ttl = await self.redis.ttl(f'tracked_message:{before.id}') await self.redis.expire(f'tracked_message:{before.id}', ttl + 60) await self.on_message(after, from_edit=True) async def on_raw_message_delete(self, event): if event.message_id in self._processing_commands: # cancel command self._processing_commands[event.message_id] = False if await self.redis.exists(f'tracked_message:{event.message_id}'): await self.clear_responses_to_message(event.message_id) await self.redis.delete(f'tracked_message:{event.message_id}') async def clear_responses_to_message(self, msg_id): for value in await self.redis.lrange(f'tracked_message:{msg_id}', 1, -1): response_type, _, rest = value.partition(':') if response_type == 'message': channel_id, message_id = rest.split(':') try: await self.http.delete_message(int(channel_id), int(message_id)) except Exception: pass elif response_type == 'reaction': channel_id, message_id, reaction = rest.split(':', 2) if reaction.isdigit(): e = self.get_emoji(int(reaction)) emoji = f'{"a:" if e.animated else ""}{e.name}:{e.id}' if emoji is None: return else: emoji = reaction try: await self.http.remove_own_reaction( int(message_id), int(channel_id), emoji) except Exception as e: pass await self.redis.execute('LTRIM', f'tracked_message:{msg_id}', 0, 0) async def on_voice_state_update(self, member, before, after): if not member.guild.me.voice: # voice connection doesn't exist return if before.channel and after.channel != before.channel: # user left or moved if not member.bot or member == self.user: # action by user or bot was moved if sum(1 for m in before.channel.members if not m.bot) == 0: # no users in channel left self._leave_voice_channel_tasks[ before.channel.id] = self.loop.create_task( self._voice_disconnect_task( before.channel, member.guild.voice_client)) elif after.channel and not before.channel: # user joined if member.guild.me.voice.channel == after.channel and not member.bot: # same channel and not bot if after.channel.id in self._leave_voice_channel_tasks: self._leave_voice_channel_tasks[after.channel.id].cancel() del self._leave_voice_channel_tasks[after.channel.id] async def _voice_disconnect_task(self, channel, vc): await asyncio.sleep(60) if sum(1 for m in channel.members if not m.bot) > 0: # there are users in channel return try: del self._leave_voice_channel_tasks[channel.id] except KeyError: pass if vc.is_connected(): await vc.disconnect() async def send_message(self, target, content=None, *, response_to=None, replace_mass_mentions=True, replace_mentions=True, **fields): if isinstance(target, discord.Member) or isinstance( target, discord.User): if target.dm_channel is None: channel = await target.create_dm() else: channel = target.dm_channel elif isinstance(target, (discord.Message, Context)): channel = target.channel elif isinstance( target, (discord.DMChannel, discord.TextChannel, discord.Webhook)): channel = target else: raise ValueError('Unknown target passed to send message') content = str(content) if content is not None else '' content = content.replace(self.http.token, 'TOKEN_LEAKED') if replace_mentions: content = await formatters.replace_mentions(content, channel, self) if replace_mass_mentions: content = formatters.replace_mass_mentions(content) fields['content'] = formatters.trim_text(content) message = None dm_message = None try: message = await channel.send(**fields) except discord.Forbidden: if response_to is not None: try: error_dm_message = await response_to.author.send( f'I was not able to send this message to channel ' f'{channel.mention} in guild **{response_to.guild}**, result is below' ) await self.register_response(response_to, error_dm_message) dm_message = await response_to.author.send(**fields) await self.register_response(response_to, dm_message) except Exception: pass except Exception: exception = traceback.format_exc() exception = '\n'.join(exception.split('\n')[-4:]) exception = f'❗ Message delivery failed\n```\n{exception}```' message = await channel.send(exception) finally: if response_to is not None: if message is not None: await self.register_response(response_to, message) return message or dm_message async def edit_message(self, msg, content=None, *, replace_mass_mentions=True, replace_mentions=True, **fields): content = str(content) if content is not None else '' content = content.replace(self.http.token, 'TOKEN_LEAKED') if replace_mentions: content = await formatters.replace_mentions( content, msg.channel, self) if replace_mass_mentions: content = formatters.replace_mass_mentions(content) fields['content'] = formatters.trim_text(content) try: return await msg.edit(**fields) except discord.errors.NotFound: logger.debug('edit_message: message not found') return None except Exception: exception = traceback.format_exc() exception = '\n'.join(exception.split('\n')[-4:]) exception = f'❗ Message edit failed\n```\n{exception}```' return await msg.edit(content=exception) async def delete_message(self, message, raise_on_errors=False): if message is None: return try: return await message.delete() except Exception: if raise_on_errors: raise async def add_reaction(self, target, reaction, response_to=None, raise_on_errors=False): if isinstance(target, Context): message = target.message elif isinstance(target, discord.Message): message = target else: raise ValueError( f'Unknown target type is passed: {type(target)}. Expected {Context} or {discord.Message}' ) reaction_type = type(reaction) if reaction_type is str: # unicode emoji = reaction elif reaction_type is int: # id emoji = self.get_emoji(reaction) if emoji is None: if raise_on_errors: raise ValueError( f'Emoji with id {reaction} not found in cache') else: return elif reaction_type is discord.Emoji: emoji = reaction else: raise ValueError( f'Unknown emoji type is passed: {type(reaction)}. Expected one of {discord.Emoji}, {str}, {int}' ) try: await message.add_reaction(emoji) except Exception as e: if raise_on_errors: raise e else: if response_to is not None: await self.register_reaction_response(response_to, message, emoji) async def track_message(self, message): if await self.redis.exists(f'tracked_message:{message.id}'): return await self.redis.rpush(f'tracked_message:{message.id}', 0) # insert 0 to prevent key from deleting await self.redis.expire(f'tracked_message:{message.id}', 86400) # 24 hours async def register_response(self, request, response): if await self.redis.exists(f'tracked_message:{request.id}'): await self.redis.rpush( f'tracked_message:{request.id}', f'message:{response.channel.id}:{response.id}') async def register_reaction_response(self, request, message, emoji): if isinstance(emoji, discord.Emoji): emoji = emoji.id if await self.redis.exists(f'tracked_message:{request.id}'): await self.redis.rpush( f'tracked_message:{request.id}', f'reaction:{message.channel.id}:{message.id}:{emoji}') def dispatch(self, event, *args, **kwargs): super().dispatch(event, *args, **kwargs) for module in self.mm.modules.values(): handler = module.events.get(event) if handler: coro = self._run_event(handler, event, *args, **kwargs) asyncio.ensure_future(coro, loop=self.loop)
def __init__(self, bot): self.bot = bot self.ch_log = ChannelLogger(bot) self.cfg = Config() self.log = Log(__name__)