monolith/core/bot.py

847 lines
33 KiB
Python
Raw Normal View History

2022-07-21 12:40:09 +00:00
import sys
from copy import deepcopy
from datetime import datetime
from random import randint
from twisted.internet import reactor
from twisted.internet.defer import Deferred
2022-07-21 12:40:09 +00:00
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.internet.task import LoopingCall
from twisted.words.protocols.irc import (
IRCBadMessage,
2022-07-21 12:40:09 +00:00
IRCClient,
lowDequote,
numeric_to_symbolic,
)
2022-07-21 12:40:09 +00:00
import main
from core.relay import sendRelayNotification
2022-08-09 06:20:30 +00:00
from modules import chankeep, counters, helpers, monitor, regproc, userinfo
from utils.dedup import dedup
2022-08-11 20:47:44 +00:00
from utils.logging.debug import debug, trace
2022-07-21 12:40:09 +00:00
from utils.logging.log import error, log, warn
2022-07-21 12:40:05 +00:00
from utils.logging.send import sendAll
from utils.parsing import parsen
# 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}
"""
2022-07-21 12:39:41 +00:00
prefix = ""
trailing = []
if not s:
raise IRCBadMessage("Empty line.")
2022-07-21 12:39:41 +00:00
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:
2022-07-21 12:39:41 +00:00
args = s.split(" ") # And again
command = args.pop(0)
return prefix, command, args
2022-07-21 12:39:41 +00:00
class IRCBot(IRCClient):
def __init__(self, net, num):
self.isconnected = False
self.channels = []
self.net = net
self.authenticated = not regproc.needToAuth(self.net)
self.num = num
self.buffer = ""
self.name = net + str(num)
alias = main.alias[num]
relay = main.network[self.net].relays[num]
self.netinst = main.network[self.net]
self.nickname = alias["nick"]
self.realname = alias["realname"]
2022-07-21 12:39:41 +00:00
self.username = alias["nick"].lower() + "/" + relay["net"]
self.password = main.config["Relay"]["Password"]
2022-07-21 12:39:41 +00:00
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
2020-07-09 18:43:47 +00:00
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:
2022-07-21 12:40:05 +00:00
if i not in self.channels:
2020-11-01 19:54:24 +00:00
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))
2020-11-01 19:54:24 +00:00
continue
2022-08-11 20:47:44 +00:00
trace(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):
2022-08-11 19:18:49 +00:00
if chankeep.allRelaysActive(self.net):
debug(f"checkChannels() all relays active for {self.net}")
else:
debug("checkChannels() 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):
2022-07-21 12:40:05 +00:00
if "ts" not in cast.keys():
2022-07-21 12:39:54 +00:00
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
2022-07-21 12:40:05 +00:00
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:
2020-11-02 20:13:36 +00:00
cast["type"] = "conn"
if "channel" in cast.keys():
if cast["channel"] == "*":
2020-11-02 20:13:36 +00:00
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
2020-11-02 20:13:36 +00:00
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():
2022-07-21 12:39:41 +00:00
# castDup = deepcopy(cast)
cast["mtype"] = cast["type"]
cast["type"] = "self"
2022-07-21 12:39:41 +00:00
# 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:
2022-08-13 21:46:10 +00:00
if cast["channel"].lower() == self.nickname.lower() or not cast["channel"].startswith("#"):
cast["mtype"] = cast["type"]
cast["type"] = "query"
2022-07-21 12:39:41 +00:00
# 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
2022-08-13 21:15:50 +00:00
if cast["channel"] == "AUTH":
cast["type"] = "conn"
cast["mtype"] = cast["type"]
# TODO: better way to do this
# as we changed the types above, check again
2020-11-02 20:13:36 +00:00
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"
2022-08-14 19:44:04 +00:00
if self.net in main.blacklist.keys():
if cast["channel"] not in main.blacklist[self.net]:
main.blacklist[self.net].append(cast["channel"])
else:
main.blacklist[self.net] = [cast["channel"]]
main.saveConf("blacklist")
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
2022-07-21 12:39:41 +00:00
if "msg" in cast.keys(): # Don't highlight queries
2022-07-21 12:40:05 +00:00
if cast["msg"] is not None:
if self.nickname.lower() in cast["msg"].lower():
castDup = deepcopy(cast)
castDup["mtype"] = cast["type"]
castDup["type"] = "highlight"
self.event(**castDup)
2022-07-21 12:40:05 +00:00
if "net" not in cast.keys():
cast["net"] = self.net
2022-07-21 12:40:05 +00:00
if "num" not 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)
2022-08-09 06:20:30 +00:00
def sendmsg(self, channel, msg, in_query=False):
query = f"{self.nickname}!*@*"
2022-08-14 15:45:40 +00:00
res = userinfo.getWhoSingle(self.net, query)
if not res:
res = []
us = list(res)
if len(us) > 0:
hostmask = us[0]
else:
# Close enough...
hostmask = f"{self.nickname}!*@{self.servername}"
warn(f"Could not get a hostname, using {hostmask}")
nick, ident, host = parsen(hostmask)
2022-08-09 06:20:30 +00:00
# We sent someone a query reply
if in_query:
self.event(type="self", mtype="msg", channel=self.nickname, nick=channel, ident=ident, host=host, msg=msg)
else:
self.event(type="self", mtype="msg", channel=channel, nick=self.nickname, ident=ident, host=host, msg=msg)
self.event(type="msg", channel=channel, nick=self.nickname, ident=ident, host=host, msg=msg)
self.msg(channel, 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):
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])
2022-07-21 12:39:41 +00:00
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:
2020-07-09 18:43:47 +00:00
if data[0] in self.prefix.keys():
2022-07-21 12:39:41 +00:00
return (
self.prefix[data[0]],
data[1:],
) # would use a set but it's possible these are the same
2020-07-09 18:43:47 +00:00
return (None, data)
2018-08-27 19:42:49 +00:00
else:
2020-07-09 18:43:47 +00:00
return (None, False)
2018-08-27 19:42:49 +00:00
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]
2022-07-21 12:39:41 +00:00
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:
2020-07-09 18:43:47 +00:00
mode, nick = self.sanit(x)
if nick:
newNicklist.append((mode, nick))
2018-08-27 19:42:49 +00:00
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")
2022-07-21 12:39:41 +00:00
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:
2022-07-21 12:39:41 +00:00
allRelays = True # override the system - if this is
2022-07-21 12:40:05 +00:00
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):
2022-07-21 12:39:41 +00:00
if len(listinfo) == 0: # probably ngircd not supporting LIST >0
return
2022-07-21 12:40:11 +00:00
if main.config["ChanKeep"]["Enabled"]:
chankeep.initialList(self.net, self.num, listinfo)
def recheckList(self):
if not main.config["ChanKeep"]["Enabled"]:
return
allRelays = chankeep.allRelaysActive(self.net)
2022-08-11 19:18:49 +00:00
debug(f"recheckList() all relays for {self.net} {allRelays}")
if allRelays:
2022-08-11 19:18:49 +00:00
debug(f"recheckList() all relays active for {self.net}")
2022-08-11 18:22:09 +00:00
first_relay = helpers.get_first_relay(self.net)
2022-08-11 19:18:49 +00:00
debug(f"recheckList() first relay for {self.net}: {first_relay.num}")
2022-08-11 18:22:09 +00:00
if first_relay:
if first_relay.wantList is True:
first_relay.list(nocheck=True)
debug(f"recheckList() asking for a list for {self.net} after final relay {self.num} connected")
2022-08-11 19:21:39 +00:00
else:
debug(f"recheckList() first relay wantList is False for {self.net} ({first_relay.num})")
2022-08-11 18:22:09 +00:00
# name = self.net + "1"
# if self.num == 1: # Only one instance should do a list
if helpers.is_first_relay(self.net, self.num):
2022-08-11 19:18:49 +00:00
debug(f"recheckList() we are the first relay for {self.net} ({self.num})")
if self.chanlimit:
if allRelays:
self.list()
2022-08-11 19:18:49 +00:00
debug(f"recheckList() requested a list for {self.net} from {self.num}")
else:
2022-08-11 19:21:39 +00:00
debug(f"recheckList() not all relays active for {self.net}")
self.wantList = True
else:
2022-08-11 19:18:49 +00:00
debug("recheckList() aborting LIST due to bad chanlimit")
self.checkChannels()
def seed_chanlimit(self, chanlimit):
2022-07-21 12:39:41 +00:00
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:
2022-07-21 12:40:05 +00:00
warn("Invalid chanlimit: %s" % chanlimit)
if self.chanlimit == 0:
2022-07-21 12:39:41 +00:00
self.chanlimit = 200 # don't take the piss if it's not limited
net_inst_chanlimit = self.netinst.chanlimit
if net_inst_chanlimit:
if self.chanlimit > net_inst_chanlimit:
self.chanlimit = net_inst_chanlimit
# warn(f"Chanlimit on {self.net} too high, setting to {self.chanlimit}")
if not regproc.needToRegister(self.net): # if we need to register, only recheck on auth confirmation
if main.config["ChanKeep"]["Enabled"]:
self.recheckList()
def seed_prefix(self, prefix):
2020-07-09 18:43:47 +00:00
prefix = prefix.replace(")", "")
prefix = prefix.replace("(", "")
length = len(prefix)
2022-07-21 12:39:41 +00:00
half = int(length / 2)
2020-07-09 18:43:47 +00:00
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):
2022-07-21 12:39:41 +00:00
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):
2022-07-21 12:39:41 +00:00
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):
2022-07-21 12:40:05 +00:00
# nick = prefix.split("!")[0]
self.userQuit(prefix, params[0])
def irc_NICK(self, prefix, params):
2022-07-21 12:39:41 +00:00
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):
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)
2022-07-21 12:39:41 +00:00
# End of Twisted hackery
def regPing(self, negativepass=None):
if self.authenticated:
return
if not regproc.needToAuth(self.net):
self.authenticated = True
return
sinst = regproc.substitute(self.net, self.num)
2022-07-21 12:39:52 +00:00
if not sinst:
error(f"regPing() {self.net}: registration ping failed for {self.num}")
2022-07-21 12:39:52 +00:00
return
2022-07-21 12:40:05 +00:00
if self._negativePass is not True:
if negativepass is False:
self._negativePass = False
debug(f"regPing() {self.net}: negativepass is False for {self.num}")
return
2022-07-21 12:40:05 +00:00
if negativepass is True:
if self._negativePass is None:
self._negativePass = True
debug(f"regPing() {self.net}: negpass:True positive registration check - {self.num}")
if sinst["ping"]:
debug("Sending ping - %s - %i" % (self.net, self.num))
self.msg(sinst["entity"], sinst["pingmsg"])
debug(f"regPing() {self.net}: sent ping '{sinst['pingmsg']}' to {sinst['entity']} - {self.num}")
return
else:
debug("regPing() {self.net}: negative registration check - {self.num}")
return
if sinst["check"]:
if sinst["negative"]:
self._negativePass = None
self.msg(sinst["entity"], sinst["negativemsg"])
debug(
(
f"regPing() {self.net}: sent negativemsg "
f"'{sinst['negativemsg']}' to {sinst['entity']} - {self.num}"
)
)
return
else:
self._negativePass = True
if sinst["ping"]:
self.msg(sinst["entity"], sinst["pingmsg"])
debug(f"regPing() {self.net}: sent ping '{sinst['pingmsg']}' to {sinst['entity']} - {self.num}")
return
else:
self.authenticated = True
def signedOn(self):
log("signed on: %s - %i" % (self.net, self.num))
ctime = str(datetime.now().isoformat())
2022-07-21 12:39:41 +00:00
sendRelayNotification(
{
"type": "conn",
"net": self.net,
"num": self.num,
"status": "signedon",
2022-07-21 12:39:54 +00:00
"ts": ctime,
2022-07-21 12:39:41 +00:00
}
)
if not self.authenticated:
reactor.callLater(10, self.regPing)
def joined(self, channel):
2022-07-21 12:40:05 +00:00
if channel not 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"]:
lc = LoopingCall(self.who, channel)
self._getWho[channel] = lc
intrange = main.config["Tweaks"]["Delays"]["WhoRange"]
minint = main.config["Tweaks"]["Delays"]["WhoLoop"]
2022-07-21 12:39:41 +00:00
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.delChannels(self.net, [channel]) # < we do not need to deduplicate this
2022-07-21 12:39:41 +00:00
# log("Can no longer cover %s, removing records" % channel)# as it will only be matched once --
# other bots have different nicknames so
2022-07-21 12:40:05 +00:00
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)
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, 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):
2022-07-21 12:39:54 +00:00
cast["ts"] = str(datetime.now().isoformat())
cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"])
2022-07-21 12:39:41 +00:00
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"])
2022-07-21 12:40:05 +00:00
if chans is None:
error("No channels returned for chanless event: %s" % cast)
2022-07-21 12:39:41 +00:00
# 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):
2022-07-21 12:39:41 +00:00
self.event(
type="mode",
muser=user,
channel=channel,
mode=m,
status=toset,
modearg=a,
)
2022-07-21 12:40:05 +00:00
# TODO: strip out relay functionality
class IRCBotFactory(ReconnectingClientFactory):
def __init__(self, net, num=None, relayCommands=None, user=None, stage2=None):
2022-07-21 12:40:05 +00:00
if net is None:
self.num = num
self.net = None
self.name = "relay - %i" % num
self.relay = True
else:
2022-07-21 12:39:41 +00:00
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):
2022-07-21 12:40:07 +00:00
entry = IRCBot(self.net, self.num)
main.IRCPool[self.name] = entry
self.client = entry
return entry
def clientConnectionLost(self, connector, reason):
if not self.relay:
userinfo.delChannels(self.net, self.client.channels)
2022-07-21 12:40:05 +00:00
if self.client is not None:
self.client.isconnected = False
self.client.authenticated = False
self.client.channels = []
error = reason.getErrorMessage()
2022-07-21 12:40:07 +00:00
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)
2022-07-21 12:39:41 +00:00
# ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
def clientConnectionFailed(self, connector, reason):
2022-07-21 12:40:05 +00:00
if self.client is not 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())
2022-07-21 12:39:41 +00:00
sendRelayNotification(
{
"type": "conn",
"net": self.net,
"num": self.num,
"status": "failed",
"message": error,
2022-07-21 12:39:54 +00:00
"ts": ctime,
2022-07-21 12:39:41 +00:00
}
)
self.retry(connector)
2022-07-21 12:39:41 +00:00
# ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)