diff --git a/IRCLineHandler.py b/IRCLineHandler.py index 602b39b0..fbb153ad 100644 --- a/IRCLineHandler.py +++ b/IRCLineHandler.py @@ -8,32 +8,43 @@ RE_CHANTYPES = re.compile(r"\bCHANTYPES=(\W+)(?:\b|$)") handlers = {} descriptions = {} +default_events = {} current_description = None +current_default_event = False handle_lock = threading.Lock() line, line_split, bot, server = None, None, None, None -def handler(f=None, description=None): - global current_description - if description: +def handler(f=None, description=None, default_event=False): + global current_description, current_default_event + if not f: current_description = description + current_default_event = default_event return handler name = f.__name__.split("handle_")[1].upper() handlers[name] = f - if current_description: - descriptions[name] = current_description - current_description = None + + descriptions[name] = current_description + default_events[name] = current_default_event + current_description, current_default_event = None, False def handle(_line, _line_split, _bot, _server): global line, line_split, bot, server handler_function = None + if len(_line_split) > 1: + name = _line_split[1] if _line_split[0][0] == ":": - if _line_split[1] in handlers: - handler_function = handlers[_line_split[1]] - elif _line_split[1].isdigit(): - _bot.events.on("received").on("numeric").on( - _line_split[1]).call(line=_line, - line_split=_line_split, server=_server, - number=_line_split[1]) + if name in handlers: + handler_function = handlers[name] + if default_events.get(name, False) or not name in handlers: + if name.isdigit(): + _bot.events.on("received").on("numeric").on( + name).call(line=_line, + line_split=_line_split, server=_server, + number=name) + else: + _bot.events.on("received").on(name).call( + line=_line, line_split=_line_split, server=_server, + command=name) elif _line_split[0] in handlers: handler_function = handlers[_line_split[0]] if handler_function: @@ -48,12 +59,10 @@ def handle_PING(): server.send_pong(Utils.remove_colon(line_split[1])) bot.events.on("received").on("ping").call(line=line, line_split=line_split, server=server, nonce=nonce) -@handler(description="the first line sent to a registered client") +@handler(description="the first line sent to a registered client", default_event=True) def handle_001(): server.set_own_nickname(line_split[2]) server.send_whois(server.nickname) - bot.events.on("received").on("numeric").on("001").call( - line=line, line_split=line_split, server=server) @handler(description="the extra supported things line") def handle_005(): isupport_line = Utils.arbitrary(line_split, 3) @@ -74,8 +83,8 @@ def handle_005(): server.channel_types = list(match.group(1)) bot.events.on("received").on("numeric").on("005").call( line=line, line_split=line_split, server=server, - isupport=isupport_line) -@handler(description="whois respose (nickname, username, realname, hostname)") + isupport=isupport_line, number="005") +@handler(description="whois respose (nickname, username, realname, hostname)", default_event=True) def handle_311(): nickname = line_split[3] if server.is_own_nickname(nickname): @@ -86,12 +95,12 @@ def handle_311(): hostname = line_split[5] target.username = username target.hostname = hostname -@handler(description="on-join channel topic line") +@handler(description="on-join channel topic line", default_event=True) def handle_332(): channel = server.get_channel(line_split[3]) topic = Utils.arbitrary(line_split, 4) channel.set_topic(topic) -@handler(description="on-join channel topic set by/at") +@handler(description="on-join channel topic set by/at", default_event=True) def handle_333(): channel = server.get_channel(line_split[3]) topic_setter_hostmask = line_split[4] @@ -101,7 +110,7 @@ def handle_333(): ) else None channel.set_topic_setter(nickname, username, hostname) channel.set_topic_time(topic_time) -@handler(description="on-join user list with status symbols") +@handler(description="on-join user list with status symbols", default_event=True) def handle_353(): channel = server.get_channel(line_split[4]) nicknames = line_split[5:] @@ -157,7 +166,7 @@ def handle_PART(): bot.events.on("self").on("part").call(line=line, line_split=line_split, server=server, channel=channel, reason=reason) -@handler(description="unknown command sent by us, oops!") +@handler(description="unknown command sent by us, oops!", default_event=True) def handle_421(): print("warning: unknown command '%s'." % line_split[3]) @handler(description="a user has disconnected!") @@ -172,6 +181,31 @@ def handle_QUIT(): user=user) else: server.disconnect() + +@handler(description="The server is telling us about its capabilities!") +def handle_CAP(): + _line = line + _line_split = line_split + if line.startswith(":"): + _line = " ".join(line_split[1:]) + _line_split = _line.split() + + capability_list = Utils.arbitrary(_line_split, 3).split() + bot.events.on("received").on("cap").call(line=line, + line_split=line_split, server=server, + nickname=_line_split[1], subcommand=_line_split[2], + capabilities=capability_list) + +@handler(description="The server is asking for authentication") +def handle_AUTHENTICATE(): + _line_split = line_split + if line.startswith(":"): + _line_split = line_split[1:] + bot.events.on("received").on("authenticate").call(line=line, + line_split=line_split, server=server, + message=Utils.arbitrary(_line_split, 1) + ) + @handler(description="someone has changed their nickname") def handle_NICK(): nickname, username, hostname = Utils.seperate_hostmask(line_split[0]) @@ -269,12 +303,12 @@ def handle_PRIVMSG(): user=user, message=message, message_split=message_split, action=action) user.log.add_line(user.nickname, message, action) -@handler(description="response to a WHO command for user information") +@handler(description="response to a WHO command for user information", default_event=True) def handle_352(): user = server.get_user(line_split[7]) user.username = line_split[4] user.hostname = line_split[5] -@handler(description="response to an empty mode command") +@handler(description="response to an empty mode command", default_event=True) def handle_324(): channel = server.get_channel(line_split[3]) modes = line_split[4] @@ -282,14 +316,14 @@ def handle_324(): for mode in modes[1:]: if mode in server.channel_modes: channel.add_mode(mode) -@handler(description="channel creation unix timestamp") +@handler(description="channel creation unix timestamp", default_event=True) def handle_329(): channel = server.get_channel(line_split[3]) channel.creation_timestamp = int(line_split[4]) -@handler(description="nickname already in use") +@handler(description="nickname already in use", default_event=True) def handle_433(): pass -@handler(description="we need a registered nickname for this channel") +@handler(description="we need a registered nickname for this channel", default_event=True) def handle_477(): bot.add_timer("rejoin", 5, channel_name=line_split[3], key=server.attempted_join[line_split[3].lower()], diff --git a/IRCServer.py b/IRCServer.py index 7154a326..16659381 100644 --- a/IRCServer.py +++ b/IRCServer.py @@ -52,8 +52,14 @@ class Server(object): return self.cached_fileno if fileno == -1 else fileno def connect(self): self.socket.connect((self.target_hostname, self.port)) + if self.password: self.send_pass(self.password) + # In principle, this belongs in the NS module. In reality, it's more practical to put this + # One-off case here for SASL + if "Nickserv" in self.bot.modules.modules and self.get_setting("nickserv-password"): + self.send_capability_request("sasl") + self.send_user(self.original_username, self.original_realname) self.send_nick(self.original_nickname) self.connected = True @@ -172,6 +178,14 @@ class Server(object): self.send("USER %s - - :%s" % (username, realname)) def send_nick(self, nickname): self.send("NICK %s" % nickname) + + def send_capability_request(self, capname): + self.send("CAP REQ :%s" % capname) + def send_capability_end(self): + self.send("CAP END") + def send_authenticate(self, text): + self.send("AUTHENTICATE %s" % text) + def send_pass(self, password): self.send("PASS %s" % password) def send_ping(self, nonce="hello"): diff --git a/ModuleManager.py b/ModuleManager.py index 65ea8cbe..3de549d1 100644 --- a/ModuleManager.py +++ b/ModuleManager.py @@ -14,6 +14,10 @@ class ModuleManager(object): def _load_module(self, filename): name = self.module_name(filename) + + whitelist = self.bot.config.get("module_whitelist", []) + if whitelist and name not in whitelist: return + with open(filename) as module_file: while True: line = module_file.readline().strip() @@ -61,6 +65,7 @@ class ModuleManager(object): if name in self.waiting_requirement: for filename in self.waiting_requirement: self.load_module(filename) + sys.stderr.write("module '%s' loaded.\n" % filename) else: sys.stderr.write("module '%s' not loaded.\n" % filename) def load_modules(self): diff --git a/modules/channel_save.py b/modules/channel_save.py index cb950ed2..713a7cb5 100644 --- a/modules/channel_save.py +++ b/modules/channel_save.py @@ -1,18 +1,29 @@ class Module(object): - def __init__(self, bot): - bot.events.on("self").on("join").hook(self.on_join) - bot.events.on("received").on("numeric").on("366").hook( - self.on_connect) + def __init__(self, bot): + bot.events.on("self").on("part").hook(self.on_self_part) + bot.events.on("self").on("join").hook(self.on_join) + bot.events.on("received").on("numeric").on("366").hook( + self.on_identify_trigger) + bot.events.on("received").on("numeric").on("001").hook( + self.on_identify_trigger) - def on_join(self, event): - channels = set(event["server"].get_setting("autojoin", [])) - channels.add(event["channel"].name) - event["server"].set_setting("autojoin", list(channels)) + def on_self_part(self, event): + pass - def on_connect(self, event): - if event["line_split"][3].lower() == "#bitbot": - channels = event["server"].get_setting("autojoin", []) - for channel in channels: - event["server"].send_join(channel) + def on_join(self, event): + channels = set(event["server"].get_setting("autojoin", [])) + channels.add(event["channel"].name) + event["server"].set_setting("autojoin", list(channels)) + + def on_identify_trigger(self, event): + if event["number"]=="001" and not event["server"].sasl_success: return + if event["line_split"][3].lower() == "#bitbot" or event["number"]=="001": + channels = event["server"].get_setting("autojoin", []) + chan_keys = event["server"].get_setting("channel_keys", {}) + for channel in channels: + if channel in chan_keys: + event["server"].send_join(channel, key=chan_keys[channel]) + else: + event["server"].send_join(channel) diff --git a/modules/nickserv.py b/modules/nickserv.py index 6f1b87b3..149dc961 100644 --- a/modules/nickserv.py +++ b/modules/nickserv.py @@ -1,18 +1,28 @@ - +import base64 class Module(object): def __init__(self, bot): + bot.events.on("new").on("server").hook(self.on_new_server) bot.events.on("received").on("numeric").on("001" ).hook(self.on_connect) bot.events.on("received").on("command").on("setnickserv" ).hook(self.set_nickserv, min_args=1, permission="setnickserv", help="Set bot's nickserv password", usage="", private_only=True) + bot.events.on("received").on("cap").hook(self.on_cap) + bot.events.on("received").on("authenticate").hook(self.on_authenticate) + for code in ["902", "903", "904", "905", "906", "907", "908"]: + bot.events.on("received").on("numeric").on(code).hook(self.on_90x) + + def on_new_server(self, event): + event["server"].attempted_auth = False + event["server"].sasl_success = False def on_connect(self, event): nickserv_password = event["server"].get_setting( "nickserv-password") - if nickserv_password: + if nickserv_password and not event["server"].sasl_success: + event["server"].attempted_auth = True event["server"].send_message("nickserv", "identify %s" % nickserv_password) @@ -20,3 +30,27 @@ class Module(object): nickserv_password = event["args"] event["server"].set_setting("nickserv-password", nickserv_password) event["stdout"].write("Nickserv password saved") + + def on_cap(self, event): + if event["subcommand"] == "NAK": + event["server"].send_capability_end() + elif event["subcommand"] == "ACK": + event["server"].send_authenticate("PLAIN") + else: + pass + + def on_authenticate(self, event): + if event["message"] != "+": + event["server"].send_authenticate("*") + else: + nick = event["server"].original_nickname + password = event["server"].get_setting("nickserv-password") + event["server"].attempted_auth = True + event["server"].send_authenticate( + base64.b64encode(("%s\0%s\0%s" % (nick, nick, password)).encode("utf8")).decode("utf8") + ) + + def on_90x(self, event): + if event["number"]=="903": + event["server"].sasl_success = True + event["server"].send_capability_end()