Fix various bugs uncovered by the LIST system

* Work around Twisted's broken handling of spaces
* Work around Twisted's broken line decoding
* Don't run signedOn twice for relays
* Improved detection of whether the endpoint is connected to ZNC
* Delay a LIST until all configured relays are online
* Discard a LIST if there are no callbacks for it
* Get rid of some double-negative ternary blocks
This commit is contained in:
Mark Veidemanis 2019-10-31 15:44:59 +00:00
parent b4fa747853
commit 7ffb6125aa
2 changed files with 123 additions and 47 deletions

View File

@ -3,7 +3,9 @@ from twisted.words.protocols.irc import IRCClient
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet import reactor, task 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 string import digits
from random import randint from random import randint
from copy import deepcopy from copy import deepcopy
@ -36,9 +38,33 @@ def deliverRelayCommands(num, relayCommands, user=None, stage2=None):
port, port,
bot, contextFactory) bot, contextFactory)
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(' ')
args.append(trailing)
else:
args = s.split(' ')
command = args.pop(0)
return prefix, command, args
class IRCRelay(IRCClient): class IRCRelay(IRCClient):
def __init__(self, num, relayCommands, user, stage2): def __init__(self, num, relayCommands, user, stage2):
self.connected = False self.isconnected = False
self.buffer = "" self.buffer = ""
if user == None: if user == None:
self.user = main.config["Relay"]["User"] self.user = main.config["Relay"]["User"]
@ -78,23 +104,24 @@ class IRCRelay(IRCClient):
deliverRelayCommands(self.num, commands, user, self.stage2) deliverRelayCommands(self.num, commands, user, self.stage2)
def signedOn(self): def signedOn(self):
self.connected = True if not self.isconnected:
log("signed on as a relay: %s" % self.num) self.isconnected = True
#sendRelayNotification("Relay", {"type": "conn", "status": "connected"}) nobody actually cares log("signed on as a relay: %s" % self.num)
sleeptime = 0 #sendRelayNotification("Relay", {"type": "conn", "status": "connected"}) nobody actually cares
increment = 0.8 sleeptime = 0
for i in self.relayCommands.keys(): increment = 0.8
for x in self.relayCommands[i]: for i in self.relayCommands.keys():
reactor.callLater(sleeptime, self.msg, main.config["Tweaks"]["ZNC"]["Prefix"]+i, x) for x in self.relayCommands[i]:
sleeptime += increment reactor.callLater(sleeptime, self.msg, main.config["Tweaks"]["ZNC"]["Prefix"]+i, x)
increment += 0.8 sleeptime += increment
reactor.callLater(sleeptime, self.sendStage2) increment += 0.8
reactor.callLater(sleeptime+5, self.transport.loseConnection) reactor.callLater(sleeptime, self.sendStage2)
return reactor.callLater(sleeptime+5, self.transport.loseConnection)
return
class IRCBot(IRCClient): class IRCBot(IRCClient):
def __init__(self, net, num): def __init__(self, net, num):
self.connected = False self.isconnected = False
self.channels = [] self.channels = []
self.net = net self.net = net
self.num = num self.num = num
@ -121,12 +148,26 @@ class IRCBot(IRCClient):
self.listOngoing = False # we are currently receiving a LIST self.listOngoing = False # we are currently receiving a LIST
self.listRetried = False # we asked and got nothing so asked again self.listRetried = False # we asked and got nothing so asked again
self.listAttempted = False # we asked for a list self.listAttempted = False # we asked for a list
self.listSimple = False # after asking again we got the list, so self.listSimple = False # after asking again we got the list, so use the simple
# use the simple syntax from now on # syntax from now on
self.wantList = False # we want to send a LIST, but not all relays are active yet
self.chanlimit = 0 self.chanlimit = 0
self.servername = None self.servername = 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]
self.handleCommand(command, prefix, params)
except IRCBadMessage:
self.badMessage(line, *sys.exc_info())
def joinChannels(self, channels): def joinChannels(self, channels):
sleeptime = 0.0 sleeptime = 0.0
increment = 0.8 increment = 0.8
@ -172,8 +213,11 @@ class IRCBot(IRCClient):
del cast["host"] del cast["host"]
del cast["channel"] del cast["channel"]
if "Disconnected from IRC" in cast["msg"]: if "Disconnected from IRC" in cast["msg"]:
log("ZNC disconnected on %s - %i" (self.net, self.num)) log("ZNC disconnected on %s - %i" % (self.net, self.num))
self.connected = False self.isconnected = False
if "Connected!" in cast["msg"]:
log("ZNC connected on %s - %i" % (self.net, self.num))
self.isconnected = True
if not cast["type"] in ["query", "self", "highlight", "znc", "who"]: 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 "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 if cast["channel"].lower() == self.nickname.lower(): # as they are channel == nickname
@ -339,28 +383,38 @@ class IRCBot(IRCClient):
self._tempList[0].append(d) self._tempList[0].append(d)
if self.listSimple: if self.listSimple:
self.sendLine("LIST") self.sendLine("LIST")
return d return d # return early if we know what to do
if noargs: if noargs:
self.sendLine("LIST") self.sendLine("LIST")
else: else:
self.sendLine("LIST >0") self.sendLine("LIST >0")
return d return d
def list(self, noargs=False): def list(self, noargs=False, nocheck=False):
if not self.listAttempted: if self.listAttempted:
self.listAttempted = True
else:
debug("List request dropped, already asked for LIST - %s - %i" % (self.net, self.num)) debug("List request dropped, already asked for LIST - %s - %i" % (self.net, self.num))
return return
if not self.listOngoing:
self._list(noargs).addCallback(self.got_list)
else: else:
debug("List request dropped, already ongoing - %s - %i" % (self.net, self.num)) self.listAttempted = True
if self.listOngoing:
debug("LIST request dropped, already ongoing - %s - %i" % (self.net, self.num))
return 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): def irc_RPL_LISTSTART(self, prefix, params):
self.listAttempted = False self.listAttempted = False
self.listOngoing = True self.listOngoing = True
self.wantList = False
def irc_RPL_LIST(self, prefix, params): def irc_RPL_LIST(self, prefix, params):
channel = params[1] channel = params[1]
@ -369,6 +423,11 @@ class IRCBot(IRCClient):
self._tempList[1].append([channel, users, topic]) self._tempList[1].append([channel, users, topic])
def irc_RPL_LISTEND(self, prefix, params): 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 callbacks, info = self._tempList
self.listOngoing = False self.listOngoing = False
for cb in callbacks: for cb in callbacks:
@ -379,13 +438,13 @@ class IRCBot(IRCClient):
self._tempList[0].clear() self._tempList[0].clear()
self._tempList[1].clear() self._tempList[1].clear()
if noResults: if noResults:
if not self.listRetried: if self.listRetried:
self.list(True) warn("LIST still empty after retry: %s - %i" % (net, num))
self.listRetried = True
else:
warn("List still empty after retry: %s - %i" % (net, num))
self.listRetried = False self.listRetried = False
return return
else:
self.list(True)
self.listRetried = True
else: else:
if self.listRetried: if self.listRetried:
self.listRetried = False self.listRetried = False
@ -402,7 +461,10 @@ class IRCBot(IRCClient):
if not any((x for x in options if any(y in x for y in interested))): if not any((x for x in options if any(y in x for y in interested))):
return # check if any of interested is in any of options, some networks return # check if any of interested is in any of options, some networks
chanlimit = None # call isupport() more than once, so discard swiftly anything chanlimit = None # call isupport() more than once, so discard swiftly anything
for i in options: # we don't care about if not self.isconnected: # we don't care about
log("endpoint connected: %s - %i" % (self.net, self.num))
self.isconnected = True
for i in options:
if i.startswith("CHANLIMIT"): if i.startswith("CHANLIMIT"):
if ":" in i: if ":" in i:
split = i.split(":") split = i.split(":")
@ -415,14 +477,28 @@ class IRCBot(IRCClient):
if len(split) == 2: if len(split) == 2:
chanlimit = split[1] chanlimit = split[1]
break break
if chanlimit: print("chanlimit", chanlimit)
try: try:
self.chanlimit = int(chanlimit) self.chanlimit = int(chanlimit)
except TypeError: except TypeError:
warn("Invalid chanlimit: %s" % i) warn("Invalid chanlimit: %s" % i)
if self.num == 1: # Only one instance should do a list, so if self.chanlimit == 0:
self.chanlimit = 200 # don't take the piss if it's unlimited
allRelays = chankeep.allRelaysActive(self.net)
print(self.net, self.num, allRelays)
if allRelays:
for i in main.network.keys():
for x in main.network[i].relays.keys():
name = i+str(x)
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 self.chanlimit:
self.list() # why not this one? :P if allRelays:
self.list()
else:
self.wantList = True
else: else:
debug("Aborting LIST due to bad chanlimit") debug("Aborting LIST due to bad chanlimit")
self.checkChannels() self.checkChannels()
@ -496,7 +572,6 @@ class IRCBot(IRCClient):
#END hacks #END hacks
def signedOn(self): def signedOn(self):
self.connected = True
log("signed on: %s - %i" % (self.net, self.num)) log("signed on: %s - %i" % (self.net, self.num))
#self.event(type="conn", status="connected") #self.event(type="conn", status="connected")
sendRelayNotification({"type": "conn", "net": self.net, "num": self.num, "status": "signedon"}) sendRelayNotification({"type": "conn", "net": self.net, "num": self.num, "status": "signedon"})
@ -607,7 +682,7 @@ class IRCBotFactory(ReconnectingClientFactory):
if not self.relay: if not self.relay:
userinfo.delChannels(self.net, self.client.channels) userinfo.delChannels(self.net, self.client.channels)
if not self.client == None: if not self.client == None:
self.client.connected = False self.client.isconnected = False
self.client.channels = [] self.client.channels = []
error = reason.getErrorMessage() error = reason.getErrorMessage()
log("%s - %i: connection lost: %s" % (self.net, self.num, error)) log("%s - %i: connection lost: %s" % (self.net, self.num, error))
@ -619,7 +694,7 @@ class IRCBotFactory(ReconnectingClientFactory):
def clientConnectionFailed(self, connector, reason): def clientConnectionFailed(self, connector, reason):
if not self.client == None: if not self.client == None:
self.client.connected = False self.client.isconnected = False
self.client.channels = [] self.client.channels = []
error = reason.getErrorMessage() error = reason.getErrorMessage()
log("%s - %i: connection failed: %s" % (self.net, self.num, error)) log("%s - %i: connection failed: %s" % (self.net, self.num, error))

View File

@ -12,7 +12,7 @@ def allRelaysActive(net):
for i in main.network[net].relays.keys(): for i in main.network[net].relays.keys():
name = net+str(i) name = net+str(i)
if name in main.IRCPool.keys(): if name in main.IRCPool.keys():
if main.IRCPool[name].connected: if main.IRCPool[name].isconnected:
existNum += 1 existNum += 1
if existNum == relayNum: if existNum == relayNum:
return True return True
@ -28,7 +28,8 @@ def getChanFree(net, new):
chanfree[i] = main.IRCPool[name].chanlimit-len(main.IRCPool[name].channels) chanfree[i] = main.IRCPool[name].chanlimit-len(main.IRCPool[name].channels)
chanlimits.add(main.IRCPool[name].chanlimit) chanlimits.add(main.IRCPool[name].chanlimit)
if not len(chanlimits) == 1: if not len(chanlimits) == 1:
error("Network %s has servers with different CHANMAX values" % net) error("Network %s has servers with different CHANLIMIT values" % net)
print(chanlimits)
return False return False
return (chanfree, chanlimits.pop()) return (chanfree, chanlimits.pop())
@ -72,7 +73,7 @@ def notifyJoin(net):
def minifyChans(net, listinfo): def minifyChans(net, listinfo):
if not allRelaysActive(net): if not allRelaysActive(net):
error("All relays for %s are not active, cannot minify list") error("All relays for %s are not active, cannot minify list" % net)
return False return False
for i in main.network[net].relays.keys(): for i in main.network[net].relays.keys():
name = net+str(i) name = net+str(i)