from twisted.internet.protocol import ReconnectingClientFactory from twisted.words.protocols.irc import IRCClient from twisted.internet.defer import Deferred from twisted.internet.task import LoopingCall from twisted.internet import reactor, task from twisted.words.protocols.irc import ( symbolic_to_numeric, numeric_to_symbolic, lowDequote, IRCBadMessage, ) import sys from string import digits from random import randint from copy import deepcopy from datetime import datetime from modules import userinfo from modules import counters from modules import monitor from modules import chankeep from modules import regproc from core.relay import sendRelayNotification from utils.dedup import dedup from utils.get import getRelay import main from utils.logging.log import * from utils.logging.debug import * from utils.logging.send import * from utils.parsing import parsen from twisted.internet.ssl import DefaultOpenSSLContextFactory def deliverRelayCommands(num, relayCommands, user=None, stage2=None): keyFN = main.certPath + main.config["Key"] certFN = main.certPath + main.config["Certificate"] contextFactory = DefaultOpenSSLContextFactory(keyFN.encode("utf-8", "replace"), certFN.encode("utf-8", "replace")) bot = IRCBotFactory(net=None, num=num, relayCommands=relayCommands, user=user, stage2=stage2) host, port = getRelay(num) rct = reactor.connectSSL(host, port, bot, contextFactory) # Copied from the Twisted source so we can fix a bug def parsemsg(s): """ Breaks a message from an IRC server into its prefix, command, and arguments. @param s: The message to break. @type s: L{bytes} @return: A tuple of (prefix, command, args). @rtype: L{tuple} """ prefix = "" trailing = [] if not s: raise IRCBadMessage("Empty line.") if s[0:1] == ":": prefix, s = s[1:].split(" ", 1) if s.find(" :") != -1: s, trailing = s.split(" :", 1) args = s.split(" ") # Twisted bug fixed by adding an argument to split() args.append(trailing) else: args = s.split(" ") # And again command = args.pop(0) return prefix, command, args class IRCRelay(IRCClient): def __init__(self, num, relayCommands, user, stage2): self.isconnected = False self.buffer = "" if user == None: self.user = main.config["Relay"]["User"] else: self.user = user.lower() password = main.config["Relay"]["Password"] self.nickname = "relay" self.realname = "relay" self.username = self.user self.password = self.user + ":" + password self.relayCommands = relayCommands self.num = num self.stage2 = stage2 self.loop = None def privmsg(self, user, channel, msg): nick, ident, host = parsen(user) for i in main.ZNCErrors: if i in msg: error("ZNC issue:", msg) if nick[0] == main.config["Tweaks"]["ZNC"]["Prefix"]: nick = nick[1:] if nick in self.relayCommands.keys(): sendAll("[%s] %s -> %s" % (self.num, nick, msg)) def irc_ERR_PASSWDMISMATCH(self, prefix, params): log("%s: relay password mismatch" % self.num) sendAll("%s: relay password mismatch" % self.num) def sendStage2(self): # [["user", {"sasl": ["message1", "message2"]}], []] if not len(self.stage2) == 0: user = self.stage2[0].pop(0) commands = self.stage2[0].pop(0) del self.stage2[0] deliverRelayCommands(self.num, commands, user, self.stage2) def signedOn(self): if not self.isconnected: self.isconnected = True # log("signed on as a relay: %s" % self.num) sleeptime = 0 increment = 0.8 for i in self.relayCommands.keys(): for x in self.relayCommands[i]: reactor.callLater( sleeptime, self.msg, main.config["Tweaks"]["ZNC"]["Prefix"] + i, x, ) sleeptime += increment increment += 0.8 if not self.stage2 == None: reactor.callLater(sleeptime, self.sendStage2) reactor.callLater(sleeptime + 5, self.transport.loseConnection) return class IRCBot(IRCClient): def __init__(self, net, num): self.isconnected = False self.channels = [] self.net = net self.authenticated = not regproc.needToRegister(self.net) self.num = num self.buffer = "" self.name = net + str(num) alias = main.alias[num] relay = main.network[self.net].relays[num] self.nickname = alias["nick"] self.realname = alias["realname"] self.username = alias["nick"].lower() + "/" + relay["net"] self.password = main.config["Relay"]["Password"] self.userinfo = None # self.fingerReply = None # self.versionName = None # Don't give out information self.versionNum = None # self.versionEnv = None # self.sourceURL = None # self._getWho = {} # LoopingCall objects -- needed to be able to stop them self._tempWho = {} # temporary storage for gathering WHO info self._tempNames = {} # temporary storage for gathering NAMES info self._tempList = ([], []) # temporary storage for gathering LIST info self.listOngoing = False # we are currently receiving a LIST self.listRetried = False # we asked and got nothing so asked again self.listAttempted = False # we asked for a list self.listSimple = False # after asking again we got the list, so use the simple # syntax from now on self.wantList = False # we want to send a LIST, but not all relays are active yet self.chanlimit = 0 self.prefix = {} self.servername = None self._regAttempt = None self._negativePass = None def lineReceived(self, line): if bytes != str and isinstance(line, bytes): # decode bytes from transport to unicode line = line.decode("utf-8", "replace") line = lowDequote(line) try: prefix, command, params = parsemsg(line) if command in numeric_to_symbolic: command = numeric_to_symbolic[command] try: self.handleCommand(command, prefix, params) except Exception as err: error(err) except IRCBadMessage: self.badMessage(line, *sys.exc_info()) def joinChannels(self, channels): sleeptime = 0.0 increment = 0.8 for i in channels: if not i in self.channels: if self.net in main.blacklist.keys(): if i in main.blacklist[self.net]: debug("Not joining blacklisted channel %s on %s - %i" % (i, self.net, self.num)) continue debug(self.net, "-", self.num, ": joining", i, "in", sleeptime, "seconds") reactor.callLater(sleeptime, self.join, i) sleeptime += increment if sleeptime == 10: sleeptime = 0.0 increment = 0.7 increment += 0.1 else: error("%s - Cannot join channel we are already on: %s - %i" % (i, self.net, self.num)) def checkChannels(self): if not chankeep.allRelaysActive(self.net): debug("Skipping channel check as we have inactive relays: %s - %i" % (self.net, self.num)) return if self.net in main.TempChan.keys(): if self.num in main.TempChan[self.net].keys(): self.joinChannels(main.TempChan[self.net][self.num]) del main.TempChan[self.net][self.num] if not main.TempChan[self.net]: del main.TempChan[self.net] def event(self, **cast): if not "ts" in cast.keys(): cast["ts"] = str(datetime.now().isoformat()) # remove odd stuff for i in list(cast.keys()): # Make a copy of the .keys() as Python 3 cannot handle iterating over if cast[i] == "": # a dictionary that changes length with each iteration del cast[i] # remove server stuff if "muser" in cast.keys(): if cast["muser"] == self.servername: cast["type"] = "conn" if "channel" in cast.keys(): if cast["channel"] == "*": cast["type"] = "conn" ## # expand out the hostmask if not {"nick", "ident", "host"}.issubset(set(cast.keys())): if "muser" in cast.keys(): cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"]) # handle ZNC stuff if {"nick", "ident", "host", "msg"}.issubset(set(cast)): if cast["ident"] == "znc" and cast["host"] == "znc.in": cast["type"] = "znc" cast["num"] = self.num del cast["nick"] del cast["ident"] del cast["host"] del cast["channel"] del cast["muser"] if "Disconnected from IRC" in cast["msg"]: log("ZNC disconnected on %s - %i" % (self.net, self.num)) self.isconnected = False self.authenticated = False if "Connected!" in cast["msg"]: log("ZNC connected on %s - %i" % (self.net, self.num)) self.isconnected = True if "could not be joined, disabling it" in cast["msg"]: error(cast["msg"]) # # don't reprocess the same message twice # if the type is in that list, it's already been here, don't run it again if not cast["type"] in {"query", "self", "highlight", "znc", "who", "conn"}: cast["num"] = self.num if "channel" in cast.keys(): if cast["type"] == "mode": if cast["channel"].lower() == self.nickname.lower(): # castDup = deepcopy(cast) cast["mtype"] = cast["type"] cast["type"] = "self" # self.event(**castDup) if cast["modearg"]: # check if modearg is non-NoneType if self.nickname.lower() == cast["modearg"].lower(): castDup = deepcopy(cast) castDup["mtype"] = cast["type"] castDup["type"] = "highlight" self.event(**castDup) else: if cast["channel"].lower() == self.nickname.lower(): cast["mtype"] = cast["type"] cast["type"] = "query" # self.event(**castDup) # Don't call self.event for this one because queries are not events on a # channel, but we still want to see them # TODO: better way to do this # as we changed the types above, check again if not cast["type"] in {"query", "self", "highlight", "znc", "who", "conn"}: # we have been kicked if "user" in cast.keys(): if cast["user"].lower() == self.nickname.lower(): castDup = deepcopy(cast) castDup["mtype"] = cast["type"] castDup["type"] = "self" self.event(**castDup) # we sent a message/left/joined/kick someone/quit if "nick" in cast.keys(): if cast["nick"].lower() == self.nickname.lower(): castDup = deepcopy(cast) castDup["mtype"] = cast["type"] castDup["type"] = "self" # we have been mentioned in a msg/notice/action/part/quit/topic message if "msg" in cast.keys(): # Don't highlight queries if not cast["msg"] == None: if self.nickname.lower() in cast["msg"].lower(): castDup = deepcopy(cast) castDup["mtype"] = cast["type"] castDup["type"] = "highlight" self.event(**castDup) if not "net" in cast.keys(): cast["net"] = self.net if not "num" in cast.keys(): cast["num"] = self.num if not self.authenticated: regproc.registerTest(cast) counters.event(self.net, cast["type"]) monitor.event(self.net, cast) def privmsg(self, user, channel, msg): self.event(type="msg", muser=user, channel=channel, msg=msg) def noticed(self, user, channel, msg): self.event(type="notice", muser=user, channel=channel, msg=msg) def action(self, user, channel, msg): self.event(type="action", muser=user, channel=channel, msg=msg) def get(self, var): try: result = getattr(self, var) except AttributeError: result = None return result def setNick(self, nickname): self._attemptedNick = nickname self.sendLine("NICK %s" % nickname) self.nickname = nickname def alterCollidedNick(self, nickname): newnick = nickname + "_" return newnick def nickChanged(self, olduser, newnick): self.nickname = newnick self.event(type="self", mtype="nick", muser=olduser, user=newnick) def irc_ERR_NICKNAMEINUSE(self, prefix, params): self._attemptedNick = self.alterCollidedNick(self._attemptedNick) self.setNick(self._attemptedNick) def irc_ERR_PASSWDMISMATCH(self, prefix, params): log("%s - %i: password mismatch as %s" % (self.net, self.num, self.username)) sendAll("%s - %i: password mismatch as %s" % (self.net, self.num, self.username)) def _who(self, channel): d = Deferred() if channel not in self._tempWho: self._tempWho[channel] = ([], []) self._tempWho[channel][0].append(d) self.sendLine("WHO %s" % channel) return d def who(self, channel): self._who(channel).addCallback(self.got_who) def irc_RPL_WHOREPLY(self, prefix, params): channel = params[1] ident = params[2] host = params[3] server = params[4] nick = params[5] status = params[6] realname = params[7] if channel not in self._tempWho: return n = self._tempWho[channel][1] n.append([nick, nick, host, server, status, realname]) self.event( type="who", nick=nick, ident=ident, host=host, realname=realname, channel=channel, server=server, status=status, ) def irc_RPL_ENDOFWHO(self, prefix, params): channel = params[1] if channel not in self._tempWho: return callbacks, info = self._tempWho[channel] for cb in callbacks: cb.callback((channel, info)) del self._tempWho[channel] def got_who(self, whoinfo): userinfo.initialUsers(self.net, whoinfo[0], whoinfo[1]) def sanit(self, data): if len(data) >= 1: if data[0] in self.prefix.keys(): return ( self.prefix[data[0]], data[1:], ) # would use a set but it's possible these are the same return (None, data) else: return (None, False) def names(self, channel): d = Deferred() if channel not in self._tempNames: self._tempNames[channel] = ([], []) self._tempNames[channel][0].append(d) self.sendLine("NAMES %s" % channel) return d def irc_RPL_NAMREPLY(self, prefix, params): channel = params[2] nicklist = params[3].split(" ") if channel not in self._tempNames: return n = self._tempNames[channel][1] n.append(nicklist) def irc_RPL_ENDOFNAMES(self, prefix, params): channel = params[1] if channel not in self._tempNames: return callbacks, namelist = self._tempNames[channel] for cb in callbacks: cb.callback((channel, namelist)) del self._tempNames[channel] def got_names(self, nicklist): newNicklist = [] for i in nicklist[1]: for x in i: mode, nick = self.sanit(x) if nick: newNicklist.append((mode, nick)) userinfo.initialNames(self.net, nicklist[0], newNicklist) def myInfo(self, servername, version, umodes, cmodes): self.servername = servername def _list(self, noargs): d = Deferred() self._tempList = ([], []) self._tempList[0].append(d) if self.listSimple: self.sendLine("LIST") return d # return early if we know what to do if noargs: self.sendLine("LIST") else: self.sendLine("LIST >0") return d def list(self, noargs=False, nocheck=False): if not self.authenticated: debug("Will not send LIST, unauthenticated: %s - %i" % (self.net, self.num)) return if self.listAttempted: debug("List request dropped, already asked for LIST - %s - %i" % (self.net, self.num)) return else: self.listAttempted = True if self.listOngoing: debug("LIST request dropped, already ongoing - %s - %i" % (self.net, self.num)) return else: if nocheck: allRelays = True # override the system - if this is else: # specified, we already did this allRelays = chankeep.allRelaysActive(self.net) if not allRelays: self.wantList = True debug("Not all relays were active for LIST request") return self._list(noargs).addCallback(self.got_list) def irc_RPL_LISTSTART(self, prefix, params): self.listAttempted = False self.listOngoing = True self.wantList = False def irc_RPL_LIST(self, prefix, params): channel = params[1] users = params[2] topic = params[3] self._tempList[1].append([channel, users, topic]) def irc_RPL_LISTEND(self, prefix, params): if not len(self._tempList[0]) > 0: # there are no callbacks, can't do anything there debug("We didn't ask for this LIST, discarding") self._tempList[0].clear() self._tempList[1].clear() return callbacks, info = self._tempList self.listOngoing = False for cb in callbacks: cb.callback((info)) noResults = False if len(self._tempList[1]) == 0: noResults = True self._tempList[0].clear() self._tempList[1].clear() if noResults: if self.listRetried: warn("LIST still empty after retry: %s - %i" % (self.net, self.num)) self.listRetried = False return else: self.list(True) self.listRetried = True else: if self.listRetried: self.listRetried = False debug("List received after retry - defaulting to simple list syntax: %s - %i" % (self.net, self.num)) self.listSimple = True def got_list(self, listinfo): if len(listinfo) == 0: # probably ngircd not supporting LIST >0 return chankeep.initialList(self.net, self.num, listinfo, self.chanlimit) def recheckList(self): allRelays = chankeep.allRelaysActive(self.net) if allRelays: name = self.net + "1" if main.IRCPool[name].wantList == True: main.IRCPool[name].list(nocheck=True) debug("Asking for a list for %s after final relay %i connected" % (self.net, self.num)) if self.num == 1: # Only one instance should do a list if self.chanlimit: if allRelays: self.list() else: self.wantList = True else: debug("Aborting LIST due to bad chanlimit") self.checkChannels() def seed_chanlimit(self, chanlimit): if not main.network[self.net].relays[self.num][ "registered" ]: # TODO: add check for register request sent, only send it once if main.config["AutoReg"]: if not self.authenticated: self._regAttempt = reactor.callLater(5, regproc.registerAccount, self.net, self.num) # regproc.registerAccount(self.net, self.num) try: self.chanlimit = int(chanlimit) except TypeError: warn("Invalid chanlimit: %s" % i) if self.chanlimit == 0: self.chanlimit = 200 # don't take the piss if it's not limited if not regproc.needToRegister(self.net): # if we need to register, only recheck on auth confirmation self.recheckList() def seed_prefix(self, prefix): prefix = prefix.replace(")", "") prefix = prefix.replace("(", "") length = len(prefix) half = int(length / 2) prefixToMode = dict(zip(prefix[half:], prefix[:half])) self.prefix = prefixToMode def isupport(self, options): interested = {"CHANLIMIT", "MAXCHANNELS", "PREFIX"} newOptions = {x for x in options if any(y in x for y in interested)} if len(newOptions) == 0: return if not self.isconnected: log("endpoint connected: %s - %i" % (self.net, self.num)) self.isconnected = True for i in newOptions: if i.startswith("PREFIX"): if "=" in i: split = i.split("=") if len(split) == 2: prefix = split[1] self.seed_prefix(prefix) elif i.startswith("CHANLIMIT"): if ":" in i: split = i.split(":") if len(split) >= 2: chanlimit = split[1] self.seed_chanlimit(chanlimit) elif i.startswith("MAXCHANNELS"): if "=" in i: split = i.split("=") if len(split) == 2: chanlimit = split[1] self.seed_chanlimit(chanlimit) # We need to override these functions as Twisted discards # the hostname and other useful information in the functions # that these call by default def irc_JOIN(self, prefix, params): nick = prefix.split("!")[0] channel = params[-1] if nick == self.nickname: self.joined(channel) else: self.userJoined(prefix, channel) def irc_PART(self, prefix, params): nick = prefix.split("!")[0] channel = params[0] if len(params) >= 2: message = params[1] else: message = None if nick == self.nickname: self.left(prefix, channel, message) else: self.userLeft(prefix, channel, message) def irc_QUIT(self, prefix, params): nick = prefix.split("!")[0] self.userQuit(prefix, params[0]) def irc_NICK(self, prefix, params): nick = prefix.split("!", 1)[0] if nick == self.nickname: self.nickChanged(prefix, params[0]) else: self.userRenamed(prefix, params[0]) def irc_KICK(self, prefix, params): channel = params[0] kicked = params[1] message = params[-1] # Checks on whether it was us that was kicked are done in userKicked self.userKicked(kicked, channel, prefix, message) def irc_TOPIC(self, prefix, params): channel = params[0] newtopic = params[1] self.topicUpdated(prefix, channel, newtopic) # End of Twisted hackery def regPing(self, negativepass=None): if self.authenticated: return sinst = regproc.substitute(self.net, self.num) if not sinst: error(f"Registration ping failed for {self.net} - {self.num}") return if not self._negativePass == True: if negativepass == False: self._negativePass = False return if negativepass == True: if self._negativePass == None: self._negativePass = True debug("Positive registration check - %s - %i" % (self.net, self.num)) if sinst["ping"]: debug("Sending ping - %s - %i" % (self.net, self.num)) self.msg(sinst["entity"], sinst["pingmsg"]) return else: debug("Negative registration for %s - %i" % (self.net, self.num)) return if sinst["check"]: if sinst["negative"]: self._negativePass = None self.msg(sinst["entity"], sinst["negativemsg"]) return else: self._negativePass = True if sinst["ping"]: self.msg(sinst["entity"], sinst["pingmsg"]) return else: self.authenticated = True def signedOn(self): log("signed on: %s - %i" % (self.net, self.num)) ctime = str(datetime.now().isoformat()) sendRelayNotification( { "type": "conn", "net": self.net, "num": self.num, "status": "signedon", "ts": ctime, } ) if not self.authenticated: reactor.callLater(10, self.regPing) def joined(self, channel): if not channel in self.channels: self.channels.append(channel) self.names(channel).addCallback(self.got_names) if main.config["Toggles"]["Who"]: lc = LoopingCall(self.who, channel) self._getWho[channel] = lc intrange = main.config["Tweaks"]["Delays"]["WhoRange"] minint = main.config["Tweaks"]["Delays"]["WhoLoop"] interval = randint(minint, minint + intrange) lc.start(interval) def botLeft(self, channel): if channel in self.channels: self.channels.remove(channel) if channel in self._getWho.keys(): lc = self._getWho[channel] lc.stop() del self._getWho[channel] userinfo.delChannels(self.net, [channel]) # < we do not need to deduplicate this # log("Can no longer cover %s, removing records" % channel)# as it will only be matched once -- # other bots have different nicknames so def left(self, user, channel, message): # even if they saw it, they wouldn't react self.event(type="part", muser=user, channel=channel, msg=message) self.botLeft(channel) def userJoined(self, user, channel): self.event(type="join", muser=user, channel=channel) def userLeft(self, user, channel, message): self.event(type="part", muser=user, channel=channel, msg=message) def userQuit(self, user, quitMessage): self.chanlessEvent({"type": "quit", "muser": user, "msg": quitMessage}) def userKicked(self, kickee, channel, kicker, message): if kickee.lower() == self.nickname.lower(): self.botLeft(channel) self.event(type="kick", muser=kicker, channel=channel, msg=message, user=kickee) def chanlessEvent(self, cast): cast["ts"] = str(datetime.now().isoformat()) cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"]) if dedup(self.name, cast): # Needs to be kept self.name until the dedup # function is converted to the new net, num # format return # stop right there sir! chans = userinfo.getChanList(self.net, cast["nick"]) if chans == None: error("No channels returned for chanless event: %s" % cast) # self.event(**cast) -- no, should NEVER happen return # getChansSingle returns all channels of the user, we only want to use # ones we have common with them realChans = set(chans).intersection(set(self.channels)) for i in realChans: cast["channel"] = i self.event(**cast) def userRenamed(self, oldname, newname): self.chanlessEvent({"type": "nick", "muser": oldname, "user": newname}) def topicUpdated(self, user, channel, newTopic): self.event(type="topic", muser=user, channel=channel, msg=newTopic) def modeChanged(self, user, channel, toset, modes, args): argList = list(args) modeList = [i for i in modes] for a, m in zip(argList, modeList): self.event( type="mode", muser=user, channel=channel, mode=m, status=toset, modearg=a, ) class IRCBotFactory(ReconnectingClientFactory): def __init__(self, net, num=None, relayCommands=None, user=None, stage2=None): if net == None: self.num = num self.net = None self.name = "relay - %i" % num self.relay = True else: self.name = net + str(num) self.num = num self.net = net self.relay = False self.client = None self.maxDelay = main.config["Tweaks"]["Delays"]["MaxDelay"] self.initialDelay = main.config["Tweaks"]["Delays"]["InitialDelay"] self.factor = main.config["Tweaks"]["Delays"]["Factor"] self.jitter = main.config["Tweaks"]["Delays"]["Jitter"] self.relayCommands, self.user, self.stage2 = relayCommands, user, stage2 def buildProtocol(self, addr): if self.relay == False: entry = IRCBot(self.net, self.num) main.IRCPool[self.name] = entry else: entry = IRCRelay(self.num, self.relayCommands, self.user, self.stage2) self.client = entry return entry def clientConnectionLost(self, connector, reason): if not self.relay: userinfo.delChannels(self.net, self.client.channels) if not self.client == None: self.client.isconnected = False self.client.authenticated = False self.client.channels = [] error = reason.getErrorMessage() if not self.relay: log("%s - %i: connection lost: %s" % (self.net, self.num, error)) sendAll("%s - %i: connection lost: %s" % (self.net, self.num, error)) ctime = str(datetime.now().isoformat()) sendRelayNotification( { "type": "conn", "net": self.net, "num": self.num, "status": "lost", "message": error, "ts": ctime, } ) self.retry(connector) # ReconnectingClientFactory.clientConnectionLost(self, connector, reason) def clientConnectionFailed(self, connector, reason): if not self.client == None: self.client.isconnected = False self.client.authenticated = False self.client.channels = [] error = reason.getErrorMessage() log("%s - %i: connection failed: %s" % (self.net, self.num, error)) if not self.relay: sendAll("%s - %s: connection failed: %s" % (self.net, self.num, error)) ctime = str(datetime.now().isoformat()) sendRelayNotification( { "type": "conn", "net": self.net, "num": self.num, "status": "failed", "message": error, "ts": ctime, } ) self.retry(connector) # ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)