monolith/core/bot.py

562 lines
21 KiB
Python
Raw Normal View History

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
2018-08-27 19:42:49 +00:00
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
2018-08-27 19:42:49 +00:00
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
2018-08-27 19:42:49 +00:00
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):
2018-08-27 19:42:49 +00:00
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)
2018-08-27 19:42:49 +00:00
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:
2018-08-27 19:42:49 +00:00
return
n = self._tempNames[channel][1]
2018-08-27 19:42:49 +00:00
n.append(nicklist)
def irc_RPL_ENDOFNAMES(self, prefix, params):
channel = params[1]
if channel not in self._tempNames:
2018-08-27 19:42:49 +00:00
return
callbacks, namelist = self._tempNames[channel]
2018-08-27 19:42:49 +00:00
for cb in callbacks:
cb.callback((channel, namelist))
del self._tempNames[channel]
2018-08-27 19:42:49 +00:00
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...
2018-08-27 19:42:49 +00:00
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:
2018-08-27 19:42:49 +00:00
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)
2018-08-27 19:42:49 +00:00
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)
2018-08-27 19:42:49 +00:00
def botLeft(self, channel):
if channel in self.channels:
self.channels.remove(channel)
2018-08-27 19:42:49 +00:00
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)
2018-08-27 19:42:49 +00:00
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)