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, deferLater from twisted.internet import reactor from string import digits from random import randint from copy import deepcopy from modules import userinfo from modules import counters from modules import monitor from modules import chankeep 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.send import * from twisted.internet.ssl import DefaultOpenSSLContextFactory def deliverRelayCommands(num, relayCommands, user=None, stage2=None): # where relay is a dictionary extracted from the Network object 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) class IRCRelay(IRCClient): def __init__(self, num, relayCommands, user, stage2): self.connected = False self.buffer = "" if user == None: self.user = main.config["Relay"]["User"] else: self.user = user 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 def parsen(self, user): step = user.split("!") nick = step[0] if len(step) == 2: step2 = step[1].split("@") ident, host = step2 else: ident = nick host = nick return [nick, ident, host] def privmsg(self, user, channel, msg): nick, ident, host = self.parsen(user) 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 signedOn(self): self.connected = True log("signed on as a relay: %s" % self.num) #sendRelayNotification("Relay", {"type": "conn", "status": "connected"}) nobody actually cares for i in self.relayCommands.keys(): for x in self.relayCommands[i]: self.msg(main.config["Tweaks"]["ZNC"]["Prefix"]+i, x) if not self.stage2 == None: # [["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) deferLater(reactor, 1, self.transport.loseConnection) return class IRCBot(IRCClient): def __init__(self, net, num): self.connected = False self.channels = [] self.net = 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"]+"/"+relay["net"] self.password = main.config["Relay"]["Password"] self.userinfo = None self.fingerReply = None self.versionName = None 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 self.chanLimit = 0 def parsen(self, user): step = user.split("!") nick = step[0] if len(step) == 2: step2 = step[1].split("@") if len(step2) == 2: ident, host = step2 else: ident = nick host = nick else: ident = nick host = nick return [nick, ident, host] def event(self, **cast): 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] if "muser" in cast.keys(): cast["nick"], cast["ident"], cast["host"] = self.parsen(cast["muser"]) #if not cast["type"] in ["nick", "kick", "quit", "part", "join"]: # del cast["muser"] if set(["nick", "ident", "host", "msg"]).issubset(set(cast)): if "msg" in cast.keys(): 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"] if not cast["type"] in ["query", "self", "highlight", "znc", "who"]: if "channel" in cast.keys() and not cast["type"] == "mode": # don't handle modes here if cast["channel"].lower() == self.nickname.lower(): # as they are channel == nickname #castDup = deepcopy(cast) # however modes are not queries! cast["mtype"] = cast["type"] cast["type"] = "query" cast["num"] = self.num #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 if "user" in cast.keys(): if cast["user"].lower() == self.nickname.lower(): castDup = deepcopy(cast) castDup["mtype"] = cast["type"] castDup["type"] = "self" cast["num"] = self.num self.event(**castDup) if "nick" in cast.keys(): if cast["nick"].lower() == self.nickname.lower(): castDup = deepcopy(cast) castDup["mtype"] = cast["type"] castDup["type"] = "self" cast["num"] = self.num if not cast["channel"].lower() == self.nickname.lower(): # modes has been set on us directly self.event(**castDup) # don't tell anyone else if "msg" in cast.keys() and not cast["type"] == "query": # 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" cast["num"] = self.num self.event(**castDup) if not "net" in cast.keys(): cast["net"] = self.net 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): print(locals()) print(globals()) log("%s - %i: password mismatch" % (self.net, self.num)) sendAll("%s - %i: password mismatch" % (self.net, self.num)) 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 ["!", "~", "&", "@", "%", "+"]: data = data[1:] return data return data else: return 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: f = self.sanit(x) # need to store this as well, or potentially just do not remove it... if f: newNicklist.append(f) userinfo.initialNames(self.net, nicklist[0], newNicklist) def _list(self): d = Deferred() self._tempList = ([], []) self._tempList[0].append(d) self.sendLine("LIST >0") return d def list(self): if not self.listOngoing: self._list().addCallback(self.got_list) def irc_RPL_LISTSTART(self, prefix, params): self.listOngoing = True 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): callbacks, info = self._tempList for cb in callbacks: cb.callback((info)) self.listOngoing = False self._tempList[0].clear() self._tempList[1].clear() def got_list(self, listinfo): chankeep.initialList(self.net, self.num, listinfo, self.chanLimit) def isupport(self, options): for i in options: if i.startswith("CHANLIMIT"): if "#" in i: split = i.split("#") if len(split) >= 2: chanLimit = split[-1].replace(":", "") try: self.chanLimit = int(chanLimit) return except TypeError: error("Invalid CHANLIMIT: %s" % i) #twisted sucks so i have to do this to actually get the user info def irc_JOIN(self, prefix, params): """ Called when a user joins a channel. """ 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): """ Called when a user leaves a channel. """ 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): """ Called when a user has quit. """ nick = prefix.split('!')[0] #if nick == self.nickname: #self.botQuit(prefix, params[0]) #else: self.userQuit(prefix, params[0]) def irc_NICK(self, prefix, params): """ Called when a user changes their nickname. """ 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): """ Called when a user is kicked from a channel. """ #kicker = prefix.split('!')[0] 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): """ Someone in the channel set the topic. """ #user = prefix.split('!')[0] channel = params[0] newtopic = params[1] self.topicUpdated(prefix, channel, newtopic) #END hacks def signedOn(self): self.connected = True log("signed on: %s - %i" % (self.net, self.num)) #self.event(type="conn", status="connected") sendRelayNotification({"type": "conn", "net": self.net, "num": self.num, "status": "signedon"}) 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"]: #self.who(channel).addCallback(self.got_who) #self.who(channel) 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.delChannel(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, message=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, message=message) def userQuit(self, user, quitMessage): self.chanlessEvent({"type": "quit", "muser": user, "message": 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, message=message, user=kickee) def chanlessEvent(self, cast): cast["nick"], cast["ident"], cast["host"] = self.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, message= 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, modes=m, status=toset, modeargs=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.delNetwork(self.net, self.client.channels) if not self.client == None: self.client.connected = False self.client.channels = [] error = reason.getErrorMessage() log("%s - %i: connection lost: %s" % (self.net, self.num, error)) if not self.relay: sendAll("%s - %i: connection lost: %s" % (self.net, self.num, error)) sendRelayNotification({"type": "conn", "net": self.net, "num": self.num, "status": "lost", "message": error}) self.retry(connector) #ReconnectingClientFactory.clientConnectionLost(self, connector, reason) def clientConnectionFailed(self, connector, reason): if not self.client == None: self.client.connected = 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)) sendRelayNotification({"type": "conn", "net": self.net, "num": self.num, "status": "failed", "message": error}) self.retry(connector) #ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)