monolith/core/bot.py

825 lines
32 KiB
Python

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
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.internet.task import LoopingCall
from twisted.words.protocols.irc import (
IRCBadMessage,
IRCClient,
lowDequote,
numeric_to_symbolic,
)
import main
from core.relay import sendRelayNotification
from modules import chankeep, counters, helpers, monitor, regproc, userinfo
from utils.dedup import dedup
from utils.logging.debug import debug, trace
from utils.logging.log import error, log, warn
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}
"""
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 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.netinst = main.network[self.net]
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 i not 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
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):
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):
if "ts" not 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() or not cast["channel"].startswith("#"):
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
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
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 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)
if "net" not in cast.keys():
cast["net"] = self.net
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)
def sendmsg(self, channel, msg, in_query=False):
query = f"{self.nickname}!*@*"
us = list(userinfo.getWhoSingle(self.net, query))
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)
# 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
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
if main.config["ChanKeep"]["Enabled"]:
chankeep.initialList(self.net, self.num, listinfo)
def recheckList(self):
allRelays = chankeep.allRelaysActive(self.net)
debug(f"recheckList() all relays for {self.net} {allRelays}")
if allRelays:
debug(f"recheckList() all relays active for {self.net}")
first_relay = helpers.get_first_relay(self.net)
debug(f"recheckList() first relay for {self.net}: {first_relay.num}")
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")
else:
debug(f"recheckList() first relay wantList is False for {self.net} ({first_relay.num})")
# name = self.net + "1"
# if self.num == 1: # Only one instance should do a list
if helpers.is_first_relay(self.net, self.num):
debug(f"recheckList() we are the first relay for {self.net} ({self.num})")
if self.chanlimit:
if allRelays:
self.list()
debug(f"recheckList() requested a list for {self.net} from {self.num}")
else:
debug(f"recheckList() not all relays active for {self.net}")
self.wantList = True
else:
debug("recheckList() 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" % chanlimit)
if self.chanlimit == 0:
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
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"regPing() {self.net}: registration ping failed for {self.num}")
return
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
if negativepass is True:
if self._negativePass is None:
self._negativePass = True
debug(f"regPing() {self.net}: 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"])
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 channel not 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 is 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,
)
# TODO: strip out relay functionality
class IRCBotFactory(ReconnectingClientFactory):
def __init__(self, net, num=None, relayCommands=None, user=None, stage2=None):
if net is 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):
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)
if self.client is not None:
self.client.isconnected = False
self.client.authenticated = False
self.client.channels = []
error = reason.getErrorMessage()
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 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())
sendRelayNotification(
{
"type": "conn",
"net": self.net,
"num": self.num,
"status": "failed",
"message": error,
"ts": ctime,
}
)
self.retry(connector)
# ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)