Beispiel #1
0
    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 = {}
Beispiel #2
0
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()

Beispiel #3
0
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)
Beispiel #4
0
 def __init__(self, bot):
     self.bot = bot
     self.ch_log = ChannelLogger(bot)
     self.cfg = Config()
     self.log = Log(__name__)