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)
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
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
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()
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)
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)