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.task import LoopingCall
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 random import randint
from copy import deepcopy
@ -36,9 +38,33 @@ def deliverRelayCommands(num, relayCommands, user=None, stage2=None):
port,
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):
def __init__(self, num, relayCommands, user, stage2):
self.connected = False
self.isconnected = False
self.buffer = ""
if user == None:
self.user = main.config["Relay"]["User"]
@ -78,7 +104,8 @@ class IRCRelay(IRCClient):
deliverRelayCommands(self.num, commands, user, self.stage2)
def signedOn(self):
self.connected = True
if not self.isconnected:
self.isconnected = True
log("signed on as a relay: %s" % self.num)
#sendRelayNotification("Relay", {"type": "conn", "status": "connected"}) nobody actually cares
sleeptime = 0
@ -94,7 +121,7 @@ class IRCRelay(IRCClient):
class IRCBot(IRCClient):
def __init__(self, net, num):
self.connected = False
self.isconnected = False
self.channels = []
self.net = net
self.num = num
@ -121,12 +148,26 @@ class IRCBot(IRCClient):
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.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.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):
sleeptime = 0.0
increment = 0.8
@ -172,8 +213,11 @@ class IRCBot(IRCClient):
del cast["host"]
del cast["channel"]
if "Disconnected from IRC" in cast["msg"]:
log("ZNC disconnected on %s - %i" (self.net, self.num))
self.connected = False
log("ZNC disconnected on %s - %i" % (self.net, self.num))
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 "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
@ -339,28 +383,38 @@ class IRCBot(IRCClient):
self._tempList[0].append(d)
if self.listSimple:
self.sendLine("LIST")
return d
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):
if not self.listAttempted:
self.listAttempted = True
else:
def list(self, noargs=False, nocheck=False):
if self.listAttempted:
debug("List request dropped, already asked for LIST - %s - %i" % (self.net, self.num))
return
if not self.listOngoing:
self._list(noargs).addCallback(self.got_list)
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
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]
@ -369,6 +423,11 @@ class IRCBot(IRCClient):
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:
@ -379,13 +438,13 @@ class IRCBot(IRCClient):
self._tempList[0].clear()
self._tempList[1].clear()
if noResults:
if not self.listRetried:
self.list(True)
self.listRetried = True
else:
warn("List still empty after retry: %s - %i" % (net, num))
if self.listRetried:
warn("LIST still empty after retry: %s - %i" % (net, num))
self.listRetried = False
return
else:
self.list(True)
self.listRetried = True
else:
if self.listRetried:
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))):
return # check if any of interested is in any of options, some networks
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 ":" in i:
split = i.split(":")
@ -415,14 +477,28 @@ class IRCBot(IRCClient):
if len(split) == 2:
chanlimit = split[1]
break
if chanlimit:
print("chanlimit", chanlimit)
try:
self.chanlimit = int(chanlimit)
except TypeError:
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:
self.list() # why not this one? :P
if allRelays:
self.list()
else:
self.wantList = True
else:
debug("Aborting LIST due to bad chanlimit")
self.checkChannels()
@ -496,7 +572,6 @@ class IRCBot(IRCClient):
#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"})
@ -607,7 +682,7 @@ class IRCBotFactory(ReconnectingClientFactory):
if not self.relay:
userinfo.delChannels(self.net, self.client.channels)
if not self.client == None:
self.client.connected = False
self.client.isconnected = False
self.client.channels = []
error = reason.getErrorMessage()
log("%s - %i: connection lost: %s" % (self.net, self.num, error))
@ -619,7 +694,7 @@ class IRCBotFactory(ReconnectingClientFactory):
def clientConnectionFailed(self, connector, reason):
if not self.client == None:
self.client.connected = False
self.client.isconnected = False
self.client.channels = []
error = reason.getErrorMessage()
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():
name = net+str(i)
if name in main.IRCPool.keys():
if main.IRCPool[name].connected:
if main.IRCPool[name].isconnected:
existNum += 1
if existNum == relayNum:
return True
@ -28,7 +28,8 @@ def getChanFree(net, new):
chanfree[i] = main.IRCPool[name].chanlimit-len(main.IRCPool[name].channels)
chanlimits.add(main.IRCPool[name].chanlimit)
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 (chanfree, chanlimits.pop())
@ -72,7 +73,7 @@ def notifyJoin(net):
def minifyChans(net, listinfo):
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
for i in main.network[net].relays.keys():
name = net+str(i)