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
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
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))
def main(): multiprocessing.freeze_support() ui_choices = get_ui_choices() default_ui = "Qt" if is_frozen() else "None" ap = argparse.ArgumentParser() ap.add_argument("--ui", default=default_ui, choices=ui_choices) options = ap.parse_args() ui, in_queue, out_queue = get_ui(options.ui) if not ui: raise RuntimeError("Couldn't instantiate UI {}".format(ui)) process = multiprocessing.Process( target=start_ui, args=(ui,) ) if options.ui != "None": process.start() if settings.LOG_FILE: set_log_file(settings.LOG_FILE) # Set LUA_PATH environment variable so our Lua code can find the libraries os.environ["LUA_PATH"] = settings.LUA_PATH ui_queues = { "in": in_queue, "out": out_queue } wrapper = ThreadCallRelay() bot = Bot(settings, ui_queues=ui_queues, wrapper=wrapper, irc_wrapper=IRCWrapper, logger=log) wrapper.set_call_object(bot) def run(): bot.run() thread = Thread(target=run) thread.daemon = True thread.start() try: if USING_EXAMPLE: msg = "It seems you haven't edited all the important " \ "settings in settings.py. Please edit at least USER, " \ "OAUTH_TOKEN, and CHANNEL_LIST." for channel in settings.CHANNEL_LIST: ui_queues["in"].put(ConsoleMsg(channel, msg)) if options.ui == "None": while True: time.sleep(1) else: running = True while running: msg = out_queue.get() if msg.__class__ == StopMsg: running = False wrapper.stop() else: msg.set_bot(bot) msg.process() finally: in_queue.put(StopMsg()) wrapper.stop() if options.ui != "None": process.join()
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))