class Bot(object): """A bot instance""" CONSOLE_BOT_PREFIX = " --- " def __init__(self, settings=None, ui_queues=None, wrapper=None, irc_wrapper=None, logger=None, wrap_irc=True): self.settings = settings self.wrapper = wrapper self.logger = logger self.ui_queues = ui_queues if irc_wrapper: iw = irc_wrapper(logger, self.wrapper, settings, settings.CHANNEL_LIST, settings.USER, settings.HOST, settings.OAUTH_TOKEN, settings.PORT, settings.COMMAND_PREFIX) if wrap_irc: self.ircWrapper = ThreadCallRelay() self.ircWrapper.set_call_object(iw) iw.set_call_relay(self.ircWrapper) else: self.ircWrapper = iw else: self.ircWrapper = None self.command_managers = {} self.blacklist_managers = {} self.channel_models = {} self.db = None # # Public API # def run(self): """ Run the bot until we want to stop :return: None """ self.logger.info(u"Starting bot...") self._initialize_models() self._initialize_command_managers() self._initialize_blacklists() self._send_initial_ui_data() self.logger.info(u"Starting IRC connection") self.ircWrapper.start() for channel in self.settings.CHANNEL_LIST: self.console(channel, "Bot started") # Run until we want to exit self.wrapper.loop() self._stop() def _stop(self): """ Stop everything we're doing :return: """ if self.ircWrapper: self.ircWrapper.stop() for key in self.command_managers: self.command_managers[key].stop_timers() def get_settings(self): """ Get the bot settings, needed due to ThreadCallRelay :return: """ return self.settings def get_irc(self): """ Get the IRC wrapper object :return: """ return self.ircWrapper def console(self, channel, message): self.ui_queues["in"].put(ConsoleMsg(channel, message)) def bot_console(self, channel, message): self.ui_queues["in"].put( ConsoleMsg(channel, self.CONSOLE_BOT_PREFIX + " " + message)) def chat_message(self, channel, nick, text, timestamp): """ Process a non-command line from the chat :param channel: The channel where the command was issued on :param nick: The nick of the user that issued the command :param text: The text content of the message :param timestamp: The unixtime for when the event happened :return: """ self.console(channel, "<{0}> {1}".format(nick, text)) user_level = self._get_user_level(channel, nick) if user_level not in ("mod", "owner"): mgr = self.blacklist_managers[channel] res, rule_id, ban_time = mgr.is_blacklisted(text) if res: self.logger.info( u"{nick} will be timed out for {time} due to blacklist " u"rule #{id}".format(nick=nick, time=human_readable_time(ban_time), id=rule_id)) self.timeout(channel, nick, ban_time) message = u"{nick}, you triggered blacklist rule #{id}, " \ u"you were timed out for {time}".format( nick=nick, id=rule_id, time=human_readable_time(ban_time) ) self._message(channel, message) def irc_command(self, channel, nick, command, args, timestamp): """ Process a command from the chat :param channel: The channel where the command was issued on :param nick: The nick of the user that issued the command :param command: The command issued :param args: All the words on the line after the command :param timestamp: The unixtime for when the event happened :return: If this was a valid command that was executed """ try: self.logger.debug(u"Got command {0} from {1} in {2}, with args: " u"{3}".format(command, nick, channel, " ".join(args))) if not self._is_core_command(command): cm = self.command_managers[channel] if cm.is_valid_command(command): self._handle_custom_command(channel, nick, command, args, timestamp) return False if not self._is_allowed_to_run_command(channel, nick, command): self.logger.info(u"Command access denied") message = u"{0}, sorry, but you are not allowed to use that " \ u"command." self._message(channel, message.format(nick)) return False if command == u"addquote": self._add_quote(channel, nick, args) elif command == u"delquote": self._del_quote(channel, nick, args) elif command == u"quote": self._show_quote(channel, nick, args) elif command == u"reg": self._manage_regulars(channel, nick, args) elif command == u"def" or command == u"com": cm = self.command_managers[channel] if command == u"def": added, channel, command, flags, user_level, code = \ cm.add_command( args ) else: added, channel, command, flags, user_level, code = \ cm.add_simple_command( args ) if added: message = u"{0}, added command {1} for user level " \ u"{2}".format( nick, command, user_level ) else: message = u"{0}, removed command {1}".format( nick, command, user_level) self.set_command(channel, command, flags, user_level, code) self._message(channel, message) elif command == u"blacklist": message = self._add_to_blacklist(channel, nick, args) self._message(channel, message) elif command == u"whitelist": message = self._add_to_whitelist(channel, nick, args) self._message(channel, message) elif command == u"unblacklist": message = self._remove_from_blacklist(channel, nick, args) self._message(channel, message) elif command == u"unwhitelist": message = self._remove_from_whitelist(channel, nick, args) self._message(channel, message) return True except BaseException as e: message = u"{0}, {1} error: {2}" exception_text = str(e) exception_text = exception_text.replace(u"<", "") exception_text = exception_text.replace(u">", "") self._message( channel, message.format(nick, e.__class__.__name__, exception_text)) self.logger.error(u"I caught a booboo .. waah!", exc_info=True) return False def set_command(self, channel, command, flags, user_level, code): """ Save a new custom command or update existing one in the database :param channel: The channel the command is for :param command: What is the command called :param flags: Command flags :param user_level: The minimum user level to run the command :param code: The Lua code for the custom command :return: None """ self.bot_console(channel, "Updating command {0}".format(command)) self._set_command(channel, command, flags, user_level, code) def update_global_value(self, channel, key, value): """ Set a global persistent value on the channel :param channel: The channel the value is for :param key: The key for the value :param value: The value to store :return: None """ self._update_channel_data(channel, key, value) def timeout(self, channel, nick, seconds): """ Timeout the given user for the given amount of seconds :param channel: :param nick: :param seconds: :return: """ message = ".timeout {nick} {seconds}".format(nick=nick, seconds=seconds) self.bot_console(channel, "Timing out {0}".format(nick)) self._message(channel, message) def get_regulars(self, channel): model = self._get_model(channel, "regulars") regulars = list(model.select()) result = [] for regular in regulars: result.append(regular.nick) return result def send_regulars_to_ui(self, channel): q = self.ui_queues["in"] regulars = self.wrapper.get_regulars(channel) for regular in regulars: q.put(AddRegularToListMsg(regular)) # # Internal API # def _message(self, channel, message): """ Deliver a message to the channel :param channel: The channel the message is to be delivered on :param message: The message text :return: None """ self.logger.debug(u"Sending message to {0}: {1}".format( channel, message)) self.bot_console(channel, "Saying: {0}".format(message)) self.ircWrapper.message(channel, message) def _is_core_command(self, command): """ Check if the given command is implemented in the "core" instead of e.g. being a custom one. :param command: The name of the command :return: True or False >>> from bot.bot import Bot >>> b = Bot() >>> b._is_core_command(u"def") True >>> b._is_core_command(u"get_fucked") False """ return command in [ u"addquote", u"delquote", u"blacklist", u"whitelist", u"unblacklist", u"unwhitelist", u"quote", u"reg", u"def", u"com" ] def _get_user_level(self, channel, nick): """ Determine the nick's user level on the channel :param channel: Which channel :param nick: Whose user level :return: String "user", "reg", "mod", or "owner" """ level = "user" if self._is_owner(nick): level = "owner" elif self._is_mod(channel, nick): level = "mod" elif self._is_regular(channel, nick): level = "reg" return level def _is_allowed_to_run_command(self, channel, nick, command): """ Check if the given user has the permissions to run the given core command. :param channel: The channel the command was run on :param nick: Who is running the command :param command: The command being run :return: True or False """ user_level = self._get_user_level(channel, nick) if user_level in ("mod", "owner"): # Mods and owners can run any and all core commands return True elif command in (u"addquote", u"delquote", u"quote"): if user_level == "reg": return True return False def _is_mod(self, channel, nick): """ Check if the given nick is a moderator on the given channel :param channel: The name of the channel :param nick: The nick :return: True of False """ return self.ircWrapper.is_oper(channel, nick) def _is_regular(self, channel, nick): """ Check if the given nick is a regular on the given channel :param channel: The name of the channel :param nick: The nick :return: True of False """ model = self._get_model(channel, "regulars") return model.filter(nick=nick).exists() def _is_owner(self, nick): """ Check if the given nick belongs to a bot owner :param nick: The nick :return: True or False """ return nick in self.settings.OWNER_USERS # # Chat commands # def _handle_custom_command(self, channel, nick, command, args, timestamp): """ Handle execution of custom commands triggered via chat :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param command: The command to be triggered :param args: The words on the line after the command :param timestamp: The unixtime for when the event happened :return: None """ user_level = self._get_user_level(channel, nick) cm = self.command_managers[channel] message = None try: cm.run_command(nick, user_level, command, args, timestamp) except CommandPermissionError: message = u"{0}, you don't have permissions to run that " \ u"command".format(nick) except CommandCooldownError: self.logger.debug( u"Ignoring call to {0} due to cooldown".format(command)) except LuaError as e: message = u"{0}, oops, got Lua error: {1}".format(nick, str(e)) if message: self._message(channel, message) def _manage_regulars(self, channel, nick, args): """ Handler for the "reg" -command, allows management of regulars :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ ok = True if len(args) != 2: ok = False action = args[0].lower() regular = args[1].lower() if not action in (u'add', u'del'): ok = False if not ok: self.logger.warn(u"Manage regulars got invalid args?") message = u"{0}, that doesn't look like a valid command?" self._message(channel, message.format(nick)) return if action == u'add': if self._is_regular(channel, regular): self.logger.info( u"Trying to add {0} to {1} regulars, but they were " u"already one.".format(regular, channel)) message = u"{0}, {1} is already a regular?" self._message(channel, message.format(nick, regular)) return self._add_regular(channel, regular) message = u"{0}, Added new regular: {1}" self._message(channel, message.format(nick, regular)) elif action == u'del': if not self._is_regular(channel, regular): self.logger.info( u"Trying to remove {0} from {1} regulars, but they " u"weren't " u"a regular there.".format(regular, channel)) message = u"{0}, {1} is not a regular?" self._message(channel, message.format(nick, regular)) return self._remove_regular(channel, regular) message = u"{0}, Removed regular: {1}" self._message(channel, message.format(nick, regular)) def _show_quote(self, channel, nick, args): """ Handler for the "quote" -command, shows a quote on the channel :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ model = self._get_model(channel, u"quotes") quote_id, quote = model.get_random_quote() if quote: message = u"Quote #{0}: {1}".format(quote_id, quote) self._message(channel, message) self.logger.info(u"Showed quote for channel {0}: {1}".format( channel, quote)) else: message = u"No quotes in the database. Maybe you should add one?" self._message(channel, message) self.logger.info(u"No quotes for channel {0}".format(channel)) def _add_quote(self, channel, nick, args, timestamp=None): """ Handler for the "addquote" -command, adds a quote to the database :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ quote_text = " ".join(args) if len(quote_text) == 0: self.logger.info(u"Got 0 length addquote call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no quote?" self._message(channel, message.format(nick)) return if not timestamp: timestamp = datetime.now() model = self._get_model(channel, "quotes") quote = model.create(quote=quote_text, year=int(timestamp.strftime("%Y")), month=int(timestamp.strftime("%m")), day=int(timestamp.strftime("%d"))) message = u"{0}, New quote added." self._message(channel, message.format(nick)) self.logger.info(u"Added quote for {0}: {1}".format(channel, quote)) self.bot_console(channel, "Added quote: {0}".format(quote)) def _del_quote(self, channel, nick, args): """ Handler for the "delquote" command, removes a quote from the database :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ if len(args) == 0: self.logger.info(u"Got 0 length delquote call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no quote ID?" self._message(channel, message.format(nick)) return quote_id = args[0] model = self._get_model(channel, "quotes") quote = model.filter(id=quote_id).first() if quote: quote.delete_instance() message = u"{0}, Quote removed.".format(nick) self.logger.info(u"Removed quote {0} for {1}".format( quote_id, channel)) self.bot_console(channel, "Deleted quote: {0}".format(quote_id)) else: message = u"{0}, no quote found with ID {1}".format(nick, quote_id) self._message(channel, message) # # Internal helper methods # def _set_command(self, channel, command, flags, user_level, code): """ Save a command on the channel's database :param channel: Which channel :param command: What command :param flags: Command flags :param user_level: Minimum user level to access this command :param code: The Lua code for the command :return: None """ model = self._get_model(channel, "commands") cmd = model.filter(command=command).first() if not cmd: cmd = model() cmd.command = command cmd.flags = json.dumps(flags) cmd.user_level = user_level cmd.code = code cmd.save() self.logger.info(u"Updated command {0} with user level {1}".format( command, user_level)) def _add_regular(self, channel, nick): """ Add a regular to the channel :param channel: Which channel :param nick: The nick of the new regular :return: None """ model = self._get_model(channel, "regulars") model.create(nick=nick) self.logger.info(u"Added regular {0} to {1}".format(nick, channel)) self.bot_console(channel, "Added regular: {0}".format(nick)) self.ui_queues["in"].put(AddRegularToListMsg(nick)) def _remove_regular(self, channel, nick): """ Remove a regular from the channel :param channel: Which channel :param nick: The nick of the old regular :return: None """ model = self._get_model(channel, "regulars") regular = model.filter(nick=nick).first() if regular: regular.delete_instance() self.logger.info(u"Removed regular {0} from {1}".format( nick, channel)) self.bot_console(channel, "Removed regular: {0}".format(nick)) self.ui_queues["in"].put(RemoveRegularFromListMsg(nick)) def _add_to_blacklist(self, channel, nick, args): """ Add an item to the blacklist :param channel: :param nick: :param args: :return: """ parser = ArgumentParser() parser.add_argument("-b", "--banTime", default="10m") parser.add_argument("match", nargs='*') options = parser.parse_args(args) model = self._get_model(channel, "blacklist") rule = model() rule.match = " ".join(options.match) rule.banTime = options.banTime rule.save() self.blacklist_managers[channel].add_blacklist(rule) message = u"{nick}, added blacklist rule {match} with ID {id}".format( nick=nick, match=rule.match, id=rule.id) self.bot_console(channel, "Added to blacklist: {0}".format(rule.match)) return message def _add_to_whitelist(self, channel, nick, args): """ Add an item to the whitelist :param channel: :param nick: :param args: :return: """ model = self._get_model(channel, "whitelist") rule = model() rule.match = " ".join(args) rule.save() self.blacklist_managers[channel].add_whitelist(rule) message = u"{nick}, added whitelist rule {match} with ID {id}".format( nick=nick, match=rule.match, id=rule.id) self.bot_console(channel, "Added to whitelist: {0}".format(rule.match)) return message def _remove_from_blacklist(self, channel, nick, args): if len(args) == 0: self.logger.info(u"Got 0 length unblacklist call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no ID?" self._message(channel, message.format(nick)) return row_id = args[0] model = self._get_model(channel, "blacklist") item = model.filter(id=row_id).first() if item: item.delete_instance() self.blacklist_managers[channel].remove_blacklist(row_id) message = u"{0}, blacklist item removed.".format(nick) self.bot_console(channel, "Removed from blacklist: {0}".format(row_id)) self.logger.info(u"Removed blacklist item {0} for {1}".format( row_id, channel)) else: message = u"{0}, no blacklist item found with ID {1}".format( nick, row_id) return message def _remove_from_whitelist(self, channel, nick, args): if len(args) == 0: self.logger.info(u"Got 0 length unwhitelist call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no ID?" self._message(channel, message.format(nick)) return row_id = args[0] model = self._get_model(channel, "whitelist") item = model.filter(id=row_id).first() if item: item.delete_instance() self.blacklist_managers[channel].remove_whitelist(row_id) message = u"{0}, whitelist item removed.".format(nick) self.logger.info(u"Removed whitelist item {0} for {1}".format( row_id, channel)) self.bot_console(channel, "Removed from whitelist: {0}".format(row_id)) else: message = u"{0}, no whitelist item found with ID {1}".format( nick, row_id) return message def _update_channel_data(self, channel, key, value): """ Save a single value to the channel's database :param channel: Which channel :param key: The name of the value :param value: The data to store :return: None """ model = self._get_model(channel, "data") data = model.filter(key=key).first() if not data: data = model() data.key = key data.value = json.dumps(value) data.save() def _load_channel_data(self, channel): """ Load all the channel's data values :param channel: Which channel :return: Python dict of all the stored values """ model = self._get_model(channel, "data") entries = list(model.select()) data = {} for entry in entries: data[entry.key] = json.loads(entry.value) return data def _initialize_command_managers(self): """ Initialize all the command managers for all the channels, load our global Lua files in their Lua interpreters, and load the channel data and commands. :return: None """ lua_files = self._find_lua_files() for channel in self.settings.CHANNEL_LIST: channel_data = self._load_channel_data(channel) cm = CommandManager(channel, self.wrapper, self.settings, channel_data, self.logger) for filename in lua_files: with open(filename, 'r') as handle: code = handle.read() self.logger.debug(u"Loading Lua for {0} from {1}".format( channel, filename)) cm.load_lua(code) model = self._get_model(channel, "commands") commands = list(model.select()) for command in commands: cm.load_command(command.command, json.loads(command.flags), command.user_level, command.code, set=False) self.command_managers[channel] = cm def _initialize_blacklists(self): """ Set up blacklist managers for all the channels :return: """ for channel in self.settings.CHANNEL_LIST: manager = BlacklistManager(logger=self.logger) blacklist_model = self._get_model(channel, "blacklist") whitelist_model = self._get_model(channel, "whitelist") blacklist = list(blacklist_model.select()) whitelist = list(whitelist_model.select()) manager.set_data(blacklist, whitelist) self.blacklist_managers[channel] = manager def _find_lua_files(self): """ Locate all Lua files we want to be globally included in our Lua runtime :return: Python list of the paths to the Lua files to be included """ return glob(self.settings.LUA_INCLUDE_GLOB) def _initialize_models(self): """ Set up our database connection and load up the model classes :return: None """ self.db = Database(self.settings) self.db.run_migrations() for channel in self.settings.CHANNEL_LIST: self.channel_models[channel] = self.db.get_models(channel) def _get_model(self, channel, table): """ Get the model instance for the given channel table :param channel: Which channel :param table: The name of the table, "regulars", "data", "commands", or "quotes" :return: A peewee model for the table """ return self.channel_models[channel][table] def _send_initial_ui_data(self): channels = [] for channel in self.settings.CHANNEL_LIST: channels.append(channel) self.ui_queues["in"].put(SetChannelsMsg(channels))
#!/usr/bin/env python """ Tool to extract all the quotes from the database. Relies on settings.py to have the correct settings for database, and channels. """ from bot.database import Database from argparse import ArgumentParser import settings if __name__ == "__main__": ap = ArgumentParser(description=__doc__) ap.add_argument("channel", help="The channel to extract quotes from") options = ap.parse_args() db = Database(settings) models = db.get_models(options.channel) for row in models["quotes"].select(): print(row.quote)
#!/usr/bin/env python """ Tool to extract all the quotes from the database. Relies on settings.py to have the correct settings for database, and channels. """ from bot.database import Database from argparse import ArgumentParser import settings if __name__ == "__main__": ap = ArgumentParser(description=__doc__) ap.add_argument( "channel", help="The channel to extract quotes from" ) options = ap.parse_args() db = Database(settings) models = db.get_models(options.channel) for row in models["quotes"].select(): print(row.quote)
class Bot(object): """A bot instance""" CONSOLE_BOT_PREFIX = " --- " def __init__(self, settings=None, ui_queues=None, wrapper=None, irc_wrapper=None, logger=None, wrap_irc=True): self.settings = settings self.wrapper = wrapper self.logger = logger self.ui_queues = ui_queues if irc_wrapper: iw = irc_wrapper( logger, self.wrapper, settings, settings.CHANNEL_LIST, settings.USER, settings.HOST, settings.OAUTH_TOKEN, settings.PORT, settings.COMMAND_PREFIX ) if wrap_irc: self.ircWrapper = ThreadCallRelay() self.ircWrapper.set_call_object(iw) iw.set_call_relay(self.ircWrapper) else: self.ircWrapper = iw else: self.ircWrapper = None self.command_managers = {} self.blacklist_managers = {} self.channel_models = {} self.db = None # # Public API # def run(self): """ Run the bot until we want to stop :return: None """ self.logger.info(u"Starting bot...") self._initialize_models() self._initialize_command_managers() self._initialize_blacklists() self._send_initial_ui_data() self.logger.info(u"Starting IRC connection") self.ircWrapper.start() for channel in self.settings.CHANNEL_LIST: self.console(channel, "Bot started") # Run until we want to exit self.wrapper.loop() self._stop() def _stop(self): """ Stop everything we're doing :return: """ if self.ircWrapper: self.ircWrapper.stop() for key in self.command_managers: self.command_managers[key].stop_timers() def get_settings(self): """ Get the bot settings, needed due to ThreadCallRelay :return: """ return self.settings def get_irc(self): """ Get the IRC wrapper object :return: """ return self.ircWrapper def console(self, channel, message): self.ui_queues["in"].put(ConsoleMsg(channel, message)) def bot_console(self, channel, message): self.ui_queues["in"].put(ConsoleMsg( channel, self.CONSOLE_BOT_PREFIX + " " + message )) def chat_message(self, channel, nick, text, timestamp): """ Process a non-command line from the chat :param channel: The channel where the command was issued on :param nick: The nick of the user that issued the command :param text: The text content of the message :param timestamp: The unixtime for when the event happened :return: """ self.console(channel, "<{0}> {1}".format(nick, text)) user_level = self._get_user_level(channel, nick) if user_level not in ("mod", "owner"): mgr = self.blacklist_managers[channel] res, rule_id, ban_time = mgr.is_blacklisted(text) if res: self.logger.info( u"{nick} will be timed out for {time} due to blacklist " u"rule #{id}".format( nick=nick, time=human_readable_time(ban_time), id=rule_id ) ) self.timeout(channel, nick, ban_time) message = u"{nick}, you triggered blacklist rule #{id}, " \ u"you were timed out for {time}".format( nick=nick, id=rule_id, time=human_readable_time(ban_time) ) self._message(channel, message) def irc_command(self, channel, nick, command, args, timestamp): """ Process a command from the chat :param channel: The channel where the command was issued on :param nick: The nick of the user that issued the command :param command: The command issued :param args: All the words on the line after the command :param timestamp: The unixtime for when the event happened :return: If this was a valid command that was executed """ try: self.logger.debug(u"Got command {0} from {1} in {2}, with args: " u"{3}".format(command, nick, channel, " ".join(args))) if not self._is_core_command(command): cm = self.command_managers[channel] if cm.is_valid_command(command): self._handle_custom_command( channel, nick, command, args, timestamp ) return False if not self._is_allowed_to_run_command(channel, nick, command): self.logger.info(u"Command access denied") message = u"{0}, sorry, but you are not allowed to use that " \ u"command." self._message(channel, message.format(nick)) return False if command == u"addquote": self._add_quote(channel, nick, args) elif command == u"delquote": self._del_quote(channel, nick, args) elif command == u"quote": self._show_quote(channel, nick, args) elif command == u"reg": self._manage_regulars(channel, nick, args) elif command == u"def" or command == u"com": cm = self.command_managers[channel] if command == u"def": added, channel, command, flags, user_level, code = \ cm.add_command( args ) else: added, channel, command, flags, user_level, code = \ cm.add_simple_command( args ) if added: message = u"{0}, added command {1} for user level " \ u"{2}".format( nick, command, user_level ) else: message = u"{0}, removed command {1}".format( nick, command, user_level ) self.set_command( channel, command, flags, user_level, code ) self._message(channel, message) elif command == u"blacklist": message = self._add_to_blacklist(channel, nick, args) self._message(channel, message) elif command == u"whitelist": message = self._add_to_whitelist(channel, nick, args) self._message(channel, message) elif command == u"unblacklist": message = self._remove_from_blacklist(channel, nick, args) self._message(channel, message) elif command == u"unwhitelist": message = self._remove_from_whitelist(channel, nick, args) self._message(channel, message) return True except BaseException as e: message = u"{0}, {1} error: {2}" exception_text = str(e) exception_text = exception_text.replace(u"<", "") exception_text = exception_text.replace(u">", "") self._message(channel, message.format( nick, e.__class__.__name__, exception_text )) self.logger.error(u"I caught a booboo .. waah!", exc_info=True) return False def set_command(self, channel, command, flags, user_level, code): """ Save a new custom command or update existing one in the database :param channel: The channel the command is for :param command: What is the command called :param flags: Command flags :param user_level: The minimum user level to run the command :param code: The Lua code for the custom command :return: None """ self.bot_console(channel, "Updating command {0}".format(command)) self._set_command(channel, command, flags, user_level, code) def update_global_value(self, channel, key, value): """ Set a global persistent value on the channel :param channel: The channel the value is for :param key: The key for the value :param value: The value to store :return: None """ self._update_channel_data(channel, key, value) def timeout(self, channel, nick, seconds): """ Timeout the given user for the given amount of seconds :param channel: :param nick: :param seconds: :return: """ message = ".timeout {nick} {seconds}".format( nick=nick, seconds=seconds ) self.bot_console(channel, "Timing out {0}".format(nick)) self._message(channel, message) def get_regulars(self, channel): model = self._get_model(channel, "regulars") regulars = list(model.select()) result = [] for regular in regulars: result.append(regular.nick) return result def send_regulars_to_ui(self, channel): q = self.ui_queues["in"] regulars = self.wrapper.get_regulars(channel) for regular in regulars: q.put(AddRegularToListMsg(regular)) # # Internal API # def _message(self, channel, message): """ Deliver a message to the channel :param channel: The channel the message is to be delivered on :param message: The message text :return: None """ self.logger.debug(u"Sending message to {0}: {1}".format( channel, message )) self.bot_console(channel, "Saying: {0}".format(message)) self.ircWrapper.message(channel, message) def _is_core_command(self, command): """ Check if the given command is implemented in the "core" instead of e.g. being a custom one. :param command: The name of the command :return: True or False >>> from bot.bot import Bot >>> b = Bot() >>> b._is_core_command(u"def") True >>> b._is_core_command(u"get_fucked") False """ return command in [ u"addquote", u"delquote", u"blacklist", u"whitelist", u"unblacklist", u"unwhitelist", u"quote", u"reg", u"def", u"com" ] def _get_user_level(self, channel, nick): """ Determine the nick's user level on the channel :param channel: Which channel :param nick: Whose user level :return: String "user", "reg", "mod", or "owner" """ level = "user" if self._is_owner(nick): level = "owner" elif self._is_mod(channel, nick): level = "mod" elif self._is_regular(channel, nick): level = "reg" return level def _is_allowed_to_run_command(self, channel, nick, command): """ Check if the given user has the permissions to run the given core command. :param channel: The channel the command was run on :param nick: Who is running the command :param command: The command being run :return: True or False """ user_level = self._get_user_level(channel, nick) if user_level in ("mod", "owner"): # Mods and owners can run any and all core commands return True elif command in (u"addquote", u"delquote", u"quote"): if user_level == "reg": return True return False def _is_mod(self, channel, nick): """ Check if the given nick is a moderator on the given channel :param channel: The name of the channel :param nick: The nick :return: True of False """ return self.ircWrapper.is_oper(channel, nick) def _is_regular(self, channel, nick): """ Check if the given nick is a regular on the given channel :param channel: The name of the channel :param nick: The nick :return: True of False """ model = self._get_model(channel, "regulars") return model.filter(nick=nick).exists() def _is_owner(self, nick): """ Check if the given nick belongs to a bot owner :param nick: The nick :return: True or False """ return nick in self.settings.OWNER_USERS # # Chat commands # def _handle_custom_command(self, channel, nick, command, args, timestamp): """ Handle execution of custom commands triggered via chat :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param command: The command to be triggered :param args: The words on the line after the command :param timestamp: The unixtime for when the event happened :return: None """ user_level = self._get_user_level(channel, nick) cm = self.command_managers[channel] message = None try: cm.run_command(nick, user_level, command, args, timestamp) except CommandPermissionError: message = u"{0}, you don't have permissions to run that " \ u"command".format(nick) except CommandCooldownError: self.logger.debug(u"Ignoring call to {0} due to cooldown".format( command )) except LuaError as e: message = u"{0}, oops, got Lua error: {1}".format( nick, str(e) ) if message: self._message(channel, message) def _manage_regulars(self, channel, nick, args): """ Handler for the "reg" -command, allows management of regulars :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ ok = True if len(args) != 2: ok = False action = args[0].lower() regular = args[1].lower() if not action in (u'add', u'del'): ok = False if not ok: self.logger.warn(u"Manage regulars got invalid args?") message = u"{0}, that doesn't look like a valid command?" self._message(channel, message.format(nick)) return if action == u'add': if self._is_regular(channel, regular): self.logger.info( u"Trying to add {0} to {1} regulars, but they were " u"already one.".format( regular, channel ) ) message = u"{0}, {1} is already a regular?" self._message(channel, message.format(nick, regular)) return self._add_regular(channel, regular) message = u"{0}, Added new regular: {1}" self._message(channel, message.format(nick, regular)) elif action == u'del': if not self._is_regular(channel, regular): self.logger.info( u"Trying to remove {0} from {1} regulars, but they " u"weren't " u"a regular there.".format( regular, channel ) ) message = u"{0}, {1} is not a regular?" self._message(channel, message.format(nick, regular)) return self._remove_regular(channel, regular) message = u"{0}, Removed regular: {1}" self._message(channel, message.format(nick, regular)) def _show_quote(self, channel, nick, args): """ Handler for the "quote" -command, shows a quote on the channel :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ model = self._get_model(channel, u"quotes") quote_id, quote = model.get_random_quote() if quote: message = u"Quote #{0}: {1}".format(quote_id, quote) self._message(channel, message) self.logger.info(u"Showed quote for channel {0}: {1}".format( channel, quote )) else: message = u"No quotes in the database. Maybe you should add one?" self._message(channel, message) self.logger.info(u"No quotes for channel {0}".format(channel)) def _add_quote(self, channel, nick, args, timestamp=None): """ Handler for the "addquote" -command, adds a quote to the database :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ quote_text = " ".join(args) if len(quote_text) == 0: self.logger.info(u"Got 0 length addquote call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no quote?" self._message(channel, message.format(nick)) return if not timestamp: timestamp = datetime.now() model = self._get_model(channel, "quotes") quote = model.create( quote=quote_text, year=int(timestamp.strftime("%Y")), month=int(timestamp.strftime("%m")), day=int(timestamp.strftime("%d")) ) message = u"{0}, New quote added." self._message(channel, message.format(nick)) self.logger.info(u"Added quote for {0}: {1}".format(channel, quote)) self.bot_console(channel, "Added quote: {0}".format(quote)) def _del_quote(self, channel, nick, args): """ Handler for the "delquote" command, removes a quote from the database :param channel: The channel the command was triggered on :param nick: The nick that triggered it :param args: The words on the line after the command :return: None """ if len(args) == 0: self.logger.info(u"Got 0 length delquote call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no quote ID?" self._message(channel, message.format(nick)) return quote_id = args[0] model = self._get_model(channel, "quotes") quote = model.filter(id=quote_id).first() if quote: quote.delete_instance() message = u"{0}, Quote removed.".format(nick) self.logger.info( u"Removed quote {0} for {1}".format(quote_id, channel) ) self.bot_console(channel, "Deleted quote: {0}".format(quote_id)) else: message = u"{0}, no quote found with ID {1}".format(nick, quote_id) self._message(channel, message) # # Internal helper methods # def _set_command(self, channel, command, flags, user_level, code): """ Save a command on the channel's database :param channel: Which channel :param command: What command :param flags: Command flags :param user_level: Minimum user level to access this command :param code: The Lua code for the command :return: None """ model = self._get_model(channel, "commands") cmd = model.filter(command=command).first() if not cmd: cmd = model() cmd.command = command cmd.flags = json.dumps(flags) cmd.user_level = user_level cmd.code = code cmd.save() self.logger.info(u"Updated command {0} with user level {1}".format( command, user_level )) def _add_regular(self, channel, nick): """ Add a regular to the channel :param channel: Which channel :param nick: The nick of the new regular :return: None """ model = self._get_model(channel, "regulars") model.create( nick=nick ) self.logger.info(u"Added regular {0} to {1}".format(nick, channel)) self.bot_console(channel, "Added regular: {0}".format(nick)) self.ui_queues["in"].put(AddRegularToListMsg(nick)) def _remove_regular(self, channel, nick): """ Remove a regular from the channel :param channel: Which channel :param nick: The nick of the old regular :return: None """ model = self._get_model(channel, "regulars") regular = model.filter(nick=nick).first() if regular: regular.delete_instance() self.logger.info(u"Removed regular {0} from {1}".format( nick, channel )) self.bot_console(channel, "Removed regular: {0}".format(nick)) self.ui_queues["in"].put(RemoveRegularFromListMsg(nick)) def _add_to_blacklist(self, channel, nick, args): """ Add an item to the blacklist :param channel: :param nick: :param args: :return: """ parser = ArgumentParser() parser.add_argument("-b", "--banTime", default="10m") parser.add_argument("match", nargs='*') options = parser.parse_args(args) model = self._get_model(channel, "blacklist") rule = model() rule.match = " ".join(options.match) rule.banTime = options.banTime rule.save() self.blacklist_managers[channel].add_blacklist(rule) message = u"{nick}, added blacklist rule {match} with ID {id}".format( nick=nick, match=rule.match, id=rule.id ) self.bot_console(channel, "Added to blacklist: {0}".format(rule.match)) return message def _add_to_whitelist(self, channel, nick, args): """ Add an item to the whitelist :param channel: :param nick: :param args: :return: """ model = self._get_model(channel, "whitelist") rule = model() rule.match = " ".join(args) rule.save() self.blacklist_managers[channel].add_whitelist(rule) message = u"{nick}, added whitelist rule {match} with ID {id}".format( nick=nick, match=rule.match, id=rule.id ) self.bot_console(channel, "Added to whitelist: {0}".format(rule.match)) return message def _remove_from_blacklist(self, channel, nick, args): if len(args) == 0: self.logger.info(u"Got 0 length unblacklist call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no ID?" self._message(channel, message.format(nick)) return row_id = args[0] model = self._get_model(channel, "blacklist") item = model.filter(id=row_id).first() if item: item.delete_instance() self.blacklist_managers[channel].remove_blacklist(row_id) message = u"{0}, blacklist item removed.".format(nick) self.bot_console(channel, "Removed from blacklist: {0}".format( row_id )) self.logger.info(u"Removed blacklist item {0} for {1}".format( row_id, channel )) else: message = u"{0}, no blacklist item found with ID {1}".format( nick, row_id ) return message def _remove_from_whitelist(self, channel, nick, args): if len(args) == 0: self.logger.info(u"Got 0 length unwhitelist call from {0} in " u"{1}?".format(nick, channel)) message = u"{0}, ehh .. you gave me no ID?" self._message(channel, message.format(nick)) return row_id = args[0] model = self._get_model(channel, "whitelist") item = model.filter(id=row_id).first() if item: item.delete_instance() self.blacklist_managers[channel].remove_whitelist(row_id) message = u"{0}, whitelist item removed.".format(nick) self.logger.info(u"Removed whitelist item {0} for {1}".format( row_id, channel )) self.bot_console(channel, "Removed from whitelist: {0}".format( row_id )) else: message = u"{0}, no whitelist item found with ID {1}".format( nick, row_id ) return message def _update_channel_data(self, channel, key, value): """ Save a single value to the channel's database :param channel: Which channel :param key: The name of the value :param value: The data to store :return: None """ model = self._get_model(channel, "data") data = model.filter(key=key).first() if not data: data = model() data.key = key data.value = json.dumps(value) data.save() def _load_channel_data(self, channel): """ Load all the channel's data values :param channel: Which channel :return: Python dict of all the stored values """ model = self._get_model(channel, "data") entries = list(model.select()) data = {} for entry in entries: data[entry.key] = json.loads(entry.value) return data def _initialize_command_managers(self): """ Initialize all the command managers for all the channels, load our global Lua files in their Lua interpreters, and load the channel data and commands. :return: None """ lua_files = self._find_lua_files() for channel in self.settings.CHANNEL_LIST: channel_data = self._load_channel_data(channel) cm = CommandManager( channel, self.wrapper, self.settings, channel_data, self.logger ) for filename in lua_files: with open(filename, 'r') as handle: code = handle.read() self.logger.debug(u"Loading Lua for {0} from {1}".format( channel, filename )) cm.load_lua(code) model = self._get_model(channel, "commands") commands = list(model.select()) for command in commands: cm.load_command( command.command, json.loads(command.flags), command.user_level, command.code, set=False ) self.command_managers[channel] = cm def _initialize_blacklists(self): """ Set up blacklist managers for all the channels :return: """ for channel in self.settings.CHANNEL_LIST: manager = BlacklistManager(logger=self.logger) blacklist_model = self._get_model(channel, "blacklist") whitelist_model = self._get_model(channel, "whitelist") blacklist = list(blacklist_model.select()) whitelist = list(whitelist_model.select()) manager.set_data(blacklist, whitelist) self.blacklist_managers[channel] = manager def _find_lua_files(self): """ Locate all Lua files we want to be globally included in our Lua runtime :return: Python list of the paths to the Lua files to be included """ return glob(self.settings.LUA_INCLUDE_GLOB) def _initialize_models(self): """ Set up our database connection and load up the model classes :return: None """ self.db = Database(self.settings) self.db.run_migrations() for channel in self.settings.CHANNEL_LIST: self.channel_models[channel] = self.db.get_models(channel) def _get_model(self, channel, table): """ Get the model instance for the given channel table :param channel: Which channel :param table: The name of the table, "regulars", "data", "commands", or "quotes" :return: A peewee model for the table """ return self.channel_models[channel][table] def _send_initial_ui_data(self): channels = [] for channel in self.settings.CHANNEL_LIST: channels.append(channel) self.ui_queues["in"].put(SetChannelsMsg(channels))