Beispiel #1
0
class BuhIRC:
    def __init__(self, config_file):
        self.config_file = config_file
        self.config = json.load(open(self.config_file))
        self.me = self.config["me"]
        self.net = self.config["network"]
        self.module_manager = ModuleManager(self)
        self.hook_manager = HookManager(self)
        self.perms = Permissions(self)
        self.connection = IRCConnection(self.net["address"], self.net["port"], self.net["ssl"], self.config["proxies"].get(self.net.get("proxy", "none"), None), self.net.get("flood_interval", 0.0))
        self.running = True
        self.state = {}  # Dict used to hold stuff like last line received and last message etc...
        self.db = Database("etc/buhirc.db")
        self.db.connect()
        logging.basicConfig(level=getattr(logging, self.config["misc"]["loglevel"]), format='[%(asctime)s] %(levelname)s: %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
        self.requests_session = requests.session()
        if self.config["misc"].get("http_proxy", "none") != "none":
            proxy = self.config["proxies"].get(self.config["misc"]["http_proxy"], "none")
            if proxy != "none":
                self.requests_session.proxies = {"http": proxy, "https": proxy}

        self.flood_verbs = [x.lower() for x in self.net.get("flood_verbs", [])]
        self.help = {}

    def run(self):
        self.connection.connect()
        if self.config["network"]["sasl"]["use"]:
            self.raw("CAP REQ :sasl")
        self.raw("NICK %s" % self.me["nicks"][0])  # Nicks thing is a temp hack
        self.raw("USER %s * * :%s" % (self.me["ident"], self.me["gecos"]))

        for module in self.config["modules"]:
            self.module_manager.load_module(module)

        while self.running:
            if not self.loop():
                self.stop()

    def raw(self, line):
        """
        Send a raw IRC line to the server.
        @param line: The raw line to send, without a trailing carriage return or newline.
        """
        logging.debug("[IRC] <- %s" % line)
        ln = Line.parse(line)
        force = True  # Whether we bypass flood protection or not.
        if ln.command.lower() in self.flood_verbs:
            force = False

        self.connection.write_line(line, force)

    def parse_line(self, ln):
        logging.debug("[IRC] -> %s" % ln.linestr)
        if ln.command == "PING":
            self.raw(ln.linestr.replace("PING", "PONG"))
        elif ln.command == "376":
            for channel in self.net["channels"]:
                self.join(channel)
        elif ln.command == "CAP":
            if ln.params[1] == "ACK" and ln.params[-1] == "sasl" and self.net["sasl"]["use"]:
                self.raw("AUTHENTICATE PLAIN")
        elif ln.command == "AUTHENTICATE":
            magic = "%s\x00%s\x00%s" % (self.net["sasl"]["username"], self.net["sasl"]["username"], self.net["sasl"]["password"])
            magic = base64.b64encode(magic.encode("ascii"))
            self.raw("AUTHENTICATE %s" % magic)
        elif ln.command == "903":
            self.raw("CAP END")
        elif ln.command == "904":
            logging.warning("SASL authentication failed, continuing login anyways...")
            self.raw("CAP END")

    def loop(self):
        if not self.connection.loop():
            return False

        for line in self.connection.buffer:
            ln = Line.parse(line)
            self.state["last_line"] = ln
            self.parse_line(ln)
            self.hook_manager.run_irc_hooks(ln)

        return True

    def stop(self):
        self.raw("QUIT :Bye!")
        self.connection.disconnect()
        self.running = False

    def rehash(self):
        """
        Rehash (reread and reparse) the bot's configuration file.
        """
        self.config = json.load(open(self.config_file))

    # Helper functions
    def hook_command(self, cmd, callback, help_text=None):
        """
        Register a command hook to the bot.
        @param cmd: Command name to hook.
        @param callback: Event callback function to call when this command is ran.
        @param help_text: Help text for this command, no help if not specified.
        @return: ID of the new hook. (Used for removal later)
        """
        cmd = cmd.lower()

        if help_text:
            self.help[cmd] = help_text

        return self.hook_manager.add_hook(Hook("command_%s" % cmd, callback))

    def hook_numeric(self, numeric, callback):
        """
        Register a raw numeric hook to the bot.
        @param numeric: The raw IRC numeric (or command, such as PRIVMSG) to hook.
        @param callback: Event callback function to call when this numeric/command is received from the server.
        @return: ID of the new hook. (Used for removal later)
        """
        return self.hook_manager.add_hook(Hook("irc_raw_%s" % numeric, callback))

    def list_commands(self):
        """
        Get a list of all commands the bot knows about.
        @return: list of command names
        """
        return [hook[8:] for hook in self.hook_manager.hooks.keys() if hook.startswith("command_")]  # len("command_") == 8

    def unhook_something(self, the_id):
        """
        Unhook any sort of hook. (Command, numeric, or event.)
        @param the_id: The ID of the hook to remove, returned by a hook adding function.
        """
        self.hook_manager.remove_hook(the_id)

    def is_admin(self, hostmask=None):
        """
        Check if a hostmask is a bot admin.
        @param hostmask: The hostmask to check.
        @return: True if admin, False if not.
        """
        if hostmask is None:
            hostmask = self.state["last_line"].hostmask

        return self.perms.check_permission(hostmask, "admin")

    def check_condition(self, condition, false_message="Sorry, you may not do that.", reply_func=None):
        """
        Check a condition and return it, calling reply_func with false_message if the condition is False.
        @param condition: The condition to check.
        @param false_message: The message to be passed to reply_func
        @param reply_func: The function to call with false_message as argument if condition is False.
        @return:
        """
        if reply_func is None:
            reply_func = self.reply

        if condition:
            return True

        reply_func(false_message)
        return False

    def check_permission(self, permission="admin", error_reply="Sorry, you do not have permission to do that!",
                         reply_func=None):
        """
        Check a bot permission against the hostmask of the last line received, and return whether it matches.
        Calls reply_func with error_reply as argument if condition is False
        @param permission: The permission to check.
        @param error_reply: The message to be passed to reply_func
        @param reply_func: The function to call with error_reply as argument if condition is False.
        @return:
        """
        if reply_func is None:
            reply_func = self.reply_notice

        return self.check_condition(self.perms.check_permission(self.state["last_line"].hostmask, permission),
                                    error_reply, reply_func)

    # IRC-related stuff begins here
    def _msg_like(self, verb, target, message):
        self.raw("%s %s :%s" % (verb, target, message))

    def privmsg(self, target, message):
        """
        Send a PRIVMSG (channel or user message) to a user/channel.
        @param target: The target to send this message to. (Can be nickname or channel.)
        @param message: The actual message to send.
        """
        self._msg_like("PRIVMSG", target, message)

    def act(self, target, action):
        """
        Send a CTCP ACTION (/me) to a user/channel.
        @param target: The target to send this ACTION to. (Can be nickname or channel.)
        @param action: The actual action to send.
        """
        self.privmsg(target, "\x01ACTION %s\x01" % action)

    def notice(self, target, message):
        """
        Send a NOTICE to a user/channel.
        @param target: The user or channel to send this notice to.
        @param message: The actual notice text.
        """
        self._msg_like("NOTICE", target, message)

    def join(self, channel):
        """
        Send a raw channel JOIN message to the server. (Join a channel)
        @param channel: The channel to join. (Key can be passed in the same argument, separated by a space.)
        """
        self.raw("JOIN %s" % channel)

    def part(self, channel):
        """
        Send a raw channel PART to the server. (Leave a channel)
        @param channel: The channel to leave.
        """
        self.raw("PART %s" % channel)

    # IRC-related stuff that involves state.
    def reply(self, message):
        """
        Send a PRIVMSG (channel or user message) to the last channel or user we received a message in.
        @param message: The reply message to send.
        """
        ln = self.state["last_line"]
        reply_to = ln.hostmask.nick

        if ln.params[0][0] == "#":
            reply_to = ln.params[0]

        self.privmsg(reply_to, message)

    def reply_act(self, action):
        """
        Send a CTCP ACTION (/me) to the last channel or user we received a message in.
        @param action: The action to send.
        """
        self.reply("\x01ACTION %s\x01" % action)

    def reply_notice(self, message):
        """
        Send a NOTICE to the last channel or user we received a message in.
        @param message: The notice text to send.
        """
        ln = self.state["last_line"]
        self.notice(ln.hostmask.nick, message)

    # Web stuff.
    def http_get(self, url, **kwargs):
        """
        Perform an HTTP GET using requests.
        @param url: The URL to GET.
        @param kwargs: Any arguments to pass to requests.get()
        @return: requests.Response object.
        """
        return self.requests_session.get(url, **kwargs)

    def http_post(self, url, **kwargs):
        """
        Perform an HTTP POST using requests.
        @param url: The URL to POST to.
        @param kwargs: Any arguments to pass to requests.get()
        @return: requests.Response object.
        """
        return self.requests_session.post(url, **kwargs)