예제 #1
0
class DiscordRelay:
    def __init__(self, bot, config):
        self.bot = bot
        self.config = config
        self.config["embed_colour"] = get_embed_colour(self.config["logo_url"])
        self.client = Client(loop=bot.loop)
        self.client.event(self.on_ready)
        self.client.event(self.on_message)
        bot.loop.create_task(self.client.start(config["token"], bot=False))

    def disconnect(self):
        self.bot.loop.create_task(self.client.logout())

    def get_health(self):
        if self.client.is_logged_in:
            resp = '\n  \u2714 {} ({}) - Connected'
        else:
            resp = '\n  \u2716 {} ({}) - Disconnected'
        fmt = ", ".join(map(lambda x: x.name, self.client.servers))
        return resp.format(self.config['description'], fmt)

    async def on_ready(self):
        await self.client.change_presence(status=Status.invisible)

    async def on_message(self, message):
        if message.mention_everyone:
            package = {
                'body':
                message.clean_content,
                'sender':
                message.author.display_name,
                'destinations':
                self.config['destinations'],
                'description':
                '{}: {}'.format(message.server.name, message.channel.name
                                or "Private Channel"),
                'logo_url':
                self.config['logo_url'],
                'embed_colour':
                self.config["embed_colour"]
            }
            self.bot.dispatch('broadcast', package)
예제 #2
0
class ChatBot(Bot):
    """
    The ChatBot is a wrapper for the Discord client itself
    Any bots that take user input from a channel will inherit this
    
    In order to create a Discord Client, a bot has to have an authorized
    token associated with it. The keys are read from files (no trailing newlines)
    inside of a local "keys" folder. The "keys" folder is at the root of the 
    project, not inside the Bot Data folder
    
    NOTE: when instancing, don't create more than one instance at a time
    """
    PREFIX = "!"
    ACTIONS = dict()
    HELPMSGS = dict()
    BLACKLIST = "blacklist"
    BANS = dict()

    # Bad words to prevent malicious searches from a host device
    BADWORDS = [
        "f**k", "c**k", "child", "kiddy", "p**n", "pron", "pr0n", "m********e",
        "bate", "shit", "piss", "anal", "cum", "wank"
    ]

    # Change this to adjust your default status setting (loads in on_ready())
    STATUS = "Beep bloop!"

    @staticmethod
    def action(help_msg=""):
        """
        Decorator to register functions into the action map
        This is bound to static as we can't use an instance object's method
        as a decorator (could be a classmethod but who cares)
        """
        def regfunc(function):
            if callable(function):
                if function.__name__ not in ChatBot.ACTIONS:
                    fname = f'{ChatBot.PREFIX}{function.__name__}'
                    ChatBot.ACTIONS[fname] = function
                    ChatBot.HELPMSGS[fname] = help_msg.strip()
                    return True
            return function

        return regfunc

    @staticmethod
    def get_emojis(msg_obj):
        "Fetch emojis from a Message object"
        if msg_obj.server is not None:
            return msg_obj.server.emojis
        return list()

    # Instance methods below
    def __init__(self, name):
        super(ChatBot, self).__init__(name)
        self.actions = dict()
        self.client = Client()
        self.token = self.read_key()
        self._load_bans()

    async def message(self, channel, string):
        """
        Shorthand version of client.send_message
        So that we don't have to arbitrarily type 
        'self.client.send_message' all the time
        """ ""
        return await self.client.send_message(channel, string)

    def _load_bans(self):
        "Load the banfile and convert it to a blacklist"
        p = Path(self.BLACKLIST)
        if not p.is_file():
            return self.logger("Local blacklist not found")
        with open(p, 'r') as f:
            self.BANS = jload(f)
        self.logger("Initial banned users:")
        for k, v in self.BANS.items():
            self.logger(f"* {k}")
        return

    @staticmethod
    def convert_user_tag(tag_str):
        "Convert a string <@(?){0,1}[0-9]> to [0-9] (False if invalid)"
        print(f"Given: {tag_str}")
        if not tag_str.startswith("<@") and not tag_str.endswith(">"):
            return False
        inside = tag_str[(3 if tag_str.startswith("<@!") else 2):-1]
        if not inside.isnumeric():
            return False
        return inside

    def display_no_servers(self):
        """
        If the bot isn't connected to any servers, show a link
        that will let you add the bot to one of your current servers
        """
        if not self.client.servers:
            self.logger(
                f"Join link: {discord.utils.oauth_url(self.client.user.id)}")
        return

    def add_ban(self, ban_target):
        "Add a user to the bans and dump the dict (True=Added, False=Not)"
        if ban_target in self.BANS:
            return False
        self.BANS[ban_target] = True
        with open(Path(self.BLACKLIST), 'w') as f:
            jdump(self.BANS, f)
        return True

    def del_ban(self, ban_target):
        "Remove a user from the bans and update the file"
        if ban_target not in self.BANS:
            return False
        self.bans.pop(ban_target)
        with open(Path(self.BLACKLIST), 'w') as f:
            jdump(self.BANS, f)
        return True

    def is_banned(self, userid):
        "Return whether a user is banned or not"
        return userid in self.BANS

    @staticmethod
    def is_admin(mobj):
        "Return whether user is an administrator or not"
        return mobj.channel.permissions_for(mobj.author).administrator

    def get_last_message(self, chan, uid=None):
        """
        Search the given channel for the second-to-last message
        aka: the message before the command was given to the bot
        """
        if len(self.client.messages) == 0:
            raise Exception("Wat")
        if len(client.messages) == 1:
            return None
        c_uid = lambda u, v: True
        if uid is not None:
            c_uid = lambda u, v: u == v
        res = [
            msg for msg in self.client.messages
            if msg.channel == chan and msg.author.id != self.client.user.id
            and c_uid(uid, msg.author.id)
        ]
        if len(res) <= 1:
            return None
        return res[-2]

    async def set_status(self, string):
        "Set the client's presence via a Game object"
        return await self.client.change_presence(game=Game(name=string))

    def event_ready(self):
        "Change this event to change what happens on login"

        async def on_ready():
            self.display_no_servers()
            await self.set_status(self.STATUS)
            return self.logger(
                f"Bot online, {len(self.ACTIONS)} commands loaded")

        return on_ready

    def event_error(self):
        "Change this for better error logging if needed"

        async def on_error(evt, *args, **kwargs):
            self.logger(f"Discord error in '{evt}''")
            return self.logger(exc_info())

        return on_error

    def event_message(self):
        "Change this to change overall on message behavior"

        async def on_message(msg):
            args = msg.content.strip().split(" ")
            key = args.pop(0).lower()  # messages sent can't be empty
            if key in self.ACTIONS:
                if self.is_banned(msg.author.id):
                    self.logger("Banned user attempted command")
                    return await self.client.delete_message(msg)
                return await self.ACTIONS[key](self, args, msg)
            return

        return on_message

    def setup_events(self):
        """
        Set up all events for the Bot
        You can override each event_*() method in the class def
        """
        self.client.event(self.event_message())
        self.client.event(self.event_error())
        self.client.event(self.event_ready())

    def run(self):
        """
        Main event loop
        Set up all Discord client events and then run the loop
        """
        self.setup_events()
        try:
            loop = asyncio.get_event_loop()
            loop.run_until_complete(self.client.start(self.token))
        except Exception as e:
            self.logger(f"Caught an exception: {e}")
        except SystemExit:
            self.logger(f"System Exit signal")
            print("System Exit signal")
        except KeyboardInterrupt:
            self.logger("Keyboard interrupt signal")
        finally:
            self.logger(f"{self.name} quitting")
            loop.run_until_complete(self.client.logout())
            loop.stop()
            loop.close()
            quit()
        return
예제 #3
0
class ChatBot(Bot):
    """
    The ChatBot is a wrapper for the Discord client itself
    Any bots that take user input from a channel will inherit this
    
    In order to create a Discord Client, a bot has to have an authorized
    token associated with it. The keys are read from files (no trailing newlines)
    inside of a local "keys" folder. The "keys" folder is at the root of the 
    project, not inside the Bot Data folder
    
    NOTE: when instancing, don't create more than one instance at a time
    """
    PREFIX = "!"
    ACTIONS = dict()

    # Bad words to prevent malicious searches from a host device
    BADWORDS = [
        "f**k", "c**k", "child", "kiddy", "p**n", "pron", "pr0n", "m********e",
        "bate", "shit", "piss", "anal", "cum", "wank"
    ]

    # Change this to adjust your default status setting (loads in on_ready())
    STATUS = "Beep bloop!"

    @staticmethod
    def action(function):
        """
        Decorator to register functions into the action map
        This is bound to static as we can't use an instance object's method
        as a decorator (could be a classmethod but who cares)
        """
        if callable(function):
            if function.__name__ not in ChatBot.ACTIONS:
                ChatBot.ACTIONS[
                    f"{ChatBot.PREFIX}{function.__name__}"] = function
                return True
        return function

    # Instance methods below
    def __init__(self, name):
        super(ChatBot, self).__init__(name)
        self.actions = dict()
        self.client = Client()
        self.token = self.read_key()

    async def message(self, channel, string):
        """
        Shorthand version of client.send_message
        So that we don't have to arbitrarily type 
        'self.client.send_message' all the time
        """ ""
        return await self.client.send_message(channel, string)

    def display_no_servers(self):
        """
        If the bot isn't connected to any servers, show a link
        that will let you add the bot to one of your current servers
        """
        if not self.client.servers:
            self.logger(
                f"Join link: {discord.utils.oauth_url(self.client.user.id)}")
        return

    def get_last_message(self, chan, uid=None):
        """
        Search the given channel for the second-to-last message
        aka: the message before the command was given to the bot
        """
        if len(self.client.messages) == 0:
            raise Exception("Wat")
        if len(client.messages) == 1:
            return None
        c_uid = lambda u, v: True
        if uid is not None:
            c_uid = lambda u, v: u == v
        res = [
            msg for msg in self.client.messages
            if msg.channel == chan and msg.author.id != self.client.user.id
            and c_uid(uid, msg.author.id)
        ]
        if len(res) <= 1:
            return None
        return res[-2]

    async def set_status(self, string):
        "Set the client's presence via a Game object"
        return await self.client.change_presence(game=Game(name=string))

    def event_ready(self):
        "Change this event to change what happens on login"

        async def on_ready():
            self.display_no_servers()
            await self.set_status(self.STATUS)
            return self.logger(
                f"Connection status: {self.client.is_logged_in}")

        return on_ready

    def event_error(self):
        "Change this for better error logging if needed"

        async def on_error(msg, *args, **kwargs):
            return self.logger(f"Discord error: {msg}")

        return on_error

    def event_message(self):
        "Change this to change overall on message behavior"

        async def on_message(msg):
            args = msg.content.strip().split(" ")
            key = args.pop(0).lower()  # messages sent can't be empty
            if key in self.ACTIONS:
                if len(args) >= 1:
                    if args[0].lower() == "help":
                        return await self.message(
                            msg.channel,
                            self.pre_text(
                                f"Help for '{key}':{self.ACTIONS[key].__doc__}"
                            ))
                return await self.ACTIONS[key](self, args, msg)

        return on_message

    def setup_events(self):
        """
        Set up all events for the Bot
        You can override each event_*() method in the class def
        """
        self.client.event(self.event_message())
        self.client.event(self.event_error())
        self.client.event(self.event_ready())

    def run(self):
        """
        Main event loop
        Set up all Discord client events and then run the loop
        """
        self.setup_events()
        try:
            loop = asyncio.get_event_loop()
            loop.run_until_complete(self.client.start(self.token))
        except Exception as e:
            print(f"Caught an exception: {e}")
        except SystemExit:
            print("System Exit signal")
        except KeyboardInterrupt:
            print("Keyboard Interrupt signal")
        finally:
            print(f"{self.name} quitting")
            loop.run_until_complete(self.client.logout())
            loop.stop()
            loop.close()
            quit()
        return
예제 #4
0
class DiscordHandler(GenericHandler):
    def __init__(self):
        super(DiscordHandler, self).__init__()
        self.bot = Client()
        self.message_handlers = WeakSet()
        self.channels = WeakValueDictionary()

    async def setup(self, token, client_secret):
        await self.bot.login(token=token, bot=True)
        asyncio.ensure_future(self.bot.connect(reconnect=True))
        await self.bot.wait_until_ready()
        print("Logged into Discord")
        self.bot.event(self.on_message)
        self.bot.event(self.on_guild_channel_create)
        self.bot.event(self.on_guild_channel_delete)
        self.bot.event(self.on_guild_channel_update)

    def get_channel(self, serialised) -> Optional[DiscordChannel]:
        channel_id = serialised.get("id")
        try:
            return self.channels[channel_id]
        except KeyError:
            pass
        try:
            channel = self.bot.get_channel(int(channel_id))
        except ValueError:
            return
        if not channel:
            return
        rtn = DiscordChannel(channel)
        self.message_handlers.add(rtn.on_message_handler)
        self.channels[channel_id] = rtn
        return rtn

    @property
    def serialised_channels(self):
        return [{
            "type": "discord",
            "id": f"{channel.id}",
            "name": channel.name,
            "server": {
                "id": f"{channel.guild.id}",
                "name": channel.guild.name,
                "icon": str(channel.guild.icon_url_as(format="png", size=256))
            }
        } for channel in self.bot.get_all_channels()
                if (channel.permissions_for(channel.guild.me).manage_webhooks
                    and isinstance(channel, TextChannel))]

    async def on_message(self, message):
        await asyncio.gather(
            *[handler(message) for handler in self.message_handlers])

    async def on_guild_channel_create(self, channel):
        await self.process_channel_handlers()

    async def on_guild_channel_delete(self, channel):
        await self.process_channel_handlers()

    async def on_guild_channel_update(self, before, after):
        await self.process_channel_handlers()
예제 #5
0
class Bot:
    instance: 'Bot'
    client: Client
    servers: Dict[int, server.Server] = {}
    database: Database

    def __init__(self):
        Bot.instance = self
        self.client = Client()
        # register event listeners
        self.client.event(self.on_ready)
        self.client.event(self.on_message)
        self.client.event(self.on_reaction_add)
        self.client.event(self.on_reaction_remove)
        self.database = Database()
        modules.initializeGames()

    def run(self, token: str):
        """ Run the bot, its a blocking call """
        if token is None:
            raise ValueError('Passed None instead of the token!')
        self.client.run(token)

    def initLoggingAndRun(self, token: str, filename: str):
        """ Initialize logging and run the bot, its a blocking call """
        if token is None:
            raise ValueError('Passed None instead of the token!')
        if filename is None:
            raise ValueError('Passed None instead of the filename!')
        logging.init_logging(filename)
        self.client.run(token)

    async def on_ready(self):
        """	Called when the bot is ready to process incoming messages """
        logger.info(f'{self.client.user}: Ready.')
        logger.info(
            f'The bot is currently in {len( self.client.guilds )} guilds.')

    async def on_reaction_add(self, reaction: Reaction, user: Member):
        """ Called when an user reacts to a message """
        if reaction.message.author == self.client.user:
            return
        guild = reaction.message.guild.id
        await self.servers[guild].handleReactionAdd(reaction, user)

    async def on_reaction_remove(self, reaction: Reaction, user: Member):
        """ Called when an user remove a reaction from a message """
        if reaction.message.author == self.client.user:
            return
        guild = reaction.message.guild.id
        await self.servers[guild].handleReactionRemove(reaction, user)

    async def on_message(self, msg: Message):
        """
		Called when a message arrives
		:param msg: the discord.Message obj
		"""

        # don't permit to use echo to get permission elevation
        # don't respond to other bots
        if msg.author.bot or msg.author == self.client.user:
            if 'echo' not in msg.content.split(' ')[0]:
                return

        # add the guild to the tracked server if it doesn't exist
        if msg.guild.id not in self.servers.keys():
            if msg.guild in self.client.guilds:
                logger.info(
                    f'Got message from new guild "{msg.guild.name}", adding it!'
                )
                self.servers[msg.guild.id] = server.Server(msg.guild)
            else:
                logger.warning(
                    f'Got message form unknown guild {msg.guild.name}, ignoring.'
                )
                return

        # reloads the server instances and modules
        if msg.content == '$$reload' and msg.author.id in utils.getAuthors()():
            logger.warning(
                f'[RELOAD] reload issued in {msg.guild.name} by {msg.author.name}!'
            )
            await msg.channel.send('Reloading!')
            # clear all servers
            self.servers.clear()
            # reload modules
            try:
                from . import defaultCommands
                from . import moduleUtils
            except Exception as e:
                await msg.channel.send(f'something is very wrong')
                await msg.channel.send(embed=utils.getTracebackEmbed(e))
                return
            try:
                # utils may be imported by the command system, reload it first
                moduleUtils.reload(utils)
                # reload command system _BEFORE_ everything else
                moduleUtils.reload(commandSystem)
                moduleUtils.reload(defaultCommands)
                # reload the rest
                moduleUtils.reload(server)
                await EventSystem.INSTANCE.invoke(Events.Reload)
            except Exception as e:
                logger.error(
                    f"[RELOAD] uncaught exception caught, can't complete reload!",
                    exc_info=e)
                await msg.channel.send(embed=utils.getTracebackEmbed(e))
            else:
                logger.info('[RELOAD] reload complete!')
                await msg.channel.send('Reloaded!')
        else:
            # call the right handler for the server
            await self.servers[msg.guild.id].handleMsg(msg)
예제 #6
0
 def __init__(self, client: discord.Client, commands=None):
     if commands is None:
         commands = set()
     self.commands = commands
     self.client = client
     client.event(self.on_message)