Search code examples
pythonclienttwistedirc

Python twisted: Functions are not called properly?


I've got a problem with setting up a client which connects to a "distributor" server to send certain data. The server's purpose is to get data from the client and then send that data to it's all connected clients. The server works without any issues. The main client is also supposed to work as an IRC bot. Here's a text example of how it should work like:

(IRC) John: Hello there!

1. The IRC client got the message, we need to send it to the distributor now.

2. Distributor should get this "John: Hello there!" string and send it back to it's all connected clients.

3. If other clients send data to the distributor, which this will broadcast to all clients, the IRC client should output at it's turn the received data to a specified channel

The following code is the IRC bot client (ircbot.py):

import sys
import socket
import time
import traceback
from twisted.words.protocols import irc
from twisted.internet import reactor
from twisted.internet import protocol

VERBOSE = True
f = None

class IRCBot(irc.IRCClient):
    def _get_nickname(self):
        return self.factory.nickname
    nickname = property(_get_nickname)

    def signedOn(self):
        self.msg("NickServ", "id <password_removed>") # Identify the bot
        time.sleep(0.1) # Wait a little...
        self.join(self.factory.channel) # Join channel #chantest
        print "Signed on as %s." % (self.nickname,)

    def joined(self, channel):
        print "Joined %s." % (channel,)

    def privmsg(self, user, channel, msg):
        name = user.split('!', 1)[0]
        prefix = "%s: %s" % (name, msg)
        print prefix
        if not user:
            return
        if self.nickname in msg:
            msg = re.compile(self.nickname + "[:,]* ?", re.I).sub('', msg)
            print msg
        else:
            prefix = ''
        if msg.startswith("!"):
            if name.lower() == "longdouble":
                self.msg(channel, "Owner command") # etc just testing stuff
            else:
                self.msg(channel, "Command")
        if channel == "#testchan" and name != "BotName":
            EchoClient().sendData('IRC:'+' '.join(map(str, [name, msg])))
            # This should make the bot send chat data to the distributor server (NOT IRC server)

    def irc_NICK(self, prefix, params):
        """Called when an IRC user changes their nickname."""
        old_nick = prefix.split('!')[0]
        new_nick = params[0]
        self.msg(, "%s is now known as %s" % (old_nick, new_nick))

    def alterCollidedNick(self, nickname):
        return nickname + '1'

class BotFactory(protocol.ClientFactory):
    protocol = IRCBot    
    def __init__(self, channel, nickname='BotName'):
        self.channel = channel
        self.nickname = nickname

    def clientConnectionLost(self, connector, reason):
        print "Lost connection (%s), reconnecting." % (reason,)
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        print "Could not connect: %s" % (reason,)


class EchoClient(protocol.Protocol):
    def connectionMade(self):
        pass

    def sendData(self, data):
            self.transport.write(data)

    def dataReceived(self, data):
        if VERBOSE:
            print "RECV:", data
        IRC.msg("#chantest", data)
        #This one should send the received data from the distributor to the IRC channel

        def connectionLost(self, reason):
            print "Connection was lost."

class EchoFactory(protocol.ClientFactory):
    def startedConnecting(self, connector):
        print 'Started to connect.'

    def buildProtocol(self, addr):
        print 'Connected to the Distributor'
        return EchoClient()
    def clientConnectionFailed(self, connector, reason):
        print "Cannot connect to distributor! Check all settings!"
        reactor.stop()

    def clientConnectionLost(self, connector, reason):
        print "Distributor Lost connection!!"
        reactor.stop()


if __name__ == "__main__":
    IRC = BotFactory('#chantest')
    reactor.connectTCP('irc.rizon.net', 6667, IRC) # Our IRC connection
    f = EchoFactory()
    reactor.connectTCP("localhost", 8000, f) # Connection to the Distributor server
    reactor.run()

The following code is the distributor server (distributor.py):

(This one works fine, but maybe it could be useful for further reference)

from twisted.internet.protocol import Protocol, Factory
from twisted.internet import reactor


class MultiEcho(Protocol):
    def __init__(self, factory):
        self.factory = factory

    def connectionMade(self):
        print "Client connected:",self
        self.factory.echoers.append(self)
        self.factory.clients = self.factory.clients+1
        #self.transport.write("Welcome to the server! There are currently "+`self.factory.clients`+" clients connected.")

    def dataReceived(self, data):
        print "RECV:",data
        for echoer in self.factory.echoers:
            echoer.transport.write(data)

    def connectionLost(self, reason):
        print "Client disconnected:",self
        self.factory.echoers.remove(self)
        self.factory.clients = self.factory.clients-1

class MultiEchoFactory(Factory):
    def __init__(self):
        self.clients = 0
        self.names = []
        self.echoers = []

    def buildProtocol(self, addr):
        return MultiEcho(self)

if __name__ == '__main__':
    print "Running..."
    reactor.listenTCP(8000, MultiEchoFactory())
    reactor.run()

I want the client to output all incoming chat data from the IRC server to the "distributor" server and also output incoming data from the "distributor". However, I get errors like this:

For the following line in ircbot.py,

EchoClient().sendData('IRC'+' '.join(map(str, [name, msg])))

I get the following error:

Joined #chantest.
Longdouble: test
Traceback (most recent call last):
  File "C:\Python\lib\site-packages\twisted\internet\tcp.py", line 460, in doRea
d
    return self.protocol.dataReceived(data)
  File "C:\Python\lib\site-packages\twisted\words\protocols\irc.py", line 2277,
in dataReceived
    basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
  File "C:\Python\lib\site-packages\twisted\protocols\basic.py", line 564, in da
taReceived
    why = self.lineReceived(line)
  File "C:\Python\lib\site-packages\twisted\words\protocols\irc.py", line 2285,
in lineReceived
    self.handleCommand(command, prefix, params)
--- <exception caught here> ---
  File "C:\Python\lib\site-packages\twisted\words\protocols\irc.py", line 2329,
in handleCommand
    method(prefix, params)
  File "C:\Python\lib\site-packages\twisted\words\protocols\irc.py", line 1813,
in irc_PRIVMSG
    self.privmsg(user, channel, message)
  File "C:\Python\Traance\kwlbot\ircbot.py", line 51, in privmsg
    EchoClient().sendData('IRC'+' '.join(map(str, [name, msg])))
  File "C:\Python\Traance\kwlbot\ircbot.py", line 90, in sendData
    self.transport.write(data)
exceptions.AttributeError: 'NoneType' object has no attribute 'write'

And same goes to this line in the same ircbot.py

IRC.msg("#chantest", data)

->

RECV: Hello from Distributor Server
Traceback (most recent call last):
  File "C:\Python\Traance\kwlbot\ircbot.py", line 96, in dataReceived
    IRC.msg("#chantest", data)
AttributeError: BotFactory instance has no attribute 'msg'

What am I doing wrong? How can I call the right function from the IRCbot class to make it send the data to the distributor server and data received from the distributor server to output in the specified channel via IRC?

Any suggestions and possible solutions are welcome.

If I missed any other details, please let me know.

Thank you for your time!


Solution

  • You should avoid writing blocking code like this:

    def signedOn(self):
        self.msg("NickServ", "id <password_removed>") # Identify the bot
        time.sleep(0.1) # Wait a little...
        self.join(self.factory.channel) # Join channel #chantest
        print "Signed on as %s." % (self.nickname,)
    

    For details, see Tail -f log on server, process data, then serve to client via twisted.

    Apart from that, the main problem here is that you are trying to send data without having a connection. When you write something like:

    EchoClient().sendData('IRC'+' '.join(map(str, [name, msg])))
    

    you're creating a protocol instance which is responsible for handling a connection and then trying to use it, but you're not creating a connection. The attempt to send data fails because the protocol hasn't been attached to any transport.

    Your snippet already demonstrates the correct way to create a connection, twice in fact:

    IRC = BotFactory('#chantest')
    reactor.connectTCP('irc.rizon.net', 6667, IRC) # Our IRC connection
    f = EchoFactory()
    reactor.connectTCP("localhost", 8000, f) # Connection to the Distributor server
    

    The mistake is creating a new EchoClient instance, one with no connection. The reactor.connectTCP call creates a new connection and a new EchoClient instance and associates them with each other.

    Instead of EchoClient().sendData(...), you want to use the EchoClient instance created by your factory:

    def buildProtocol(self, addr):
        print 'Connected to the Distributor'
        return EchoClient()
    

    Your buildProtocol implementation creates the instance, all that's missing is for it to save the instance so it can be used by your IRC bot.

    Consider something like this:

    def buildProtocol(self, addr):
        print 'Connected to the Distributor'
        self.connection = EchoClient()
        return self.connection
    

    Your IRC client can then use the saved EchoClient instance:

        if channel == "#testchan" and name != "BotName":
            f.connection.sendData('IRC:'+' '.join(map(str, [name, msg])))
            # This should make the bot send chat data to the distributor server (NOT IRC server)
    

    Note that the specific code I give here is a very crude approach. It uses the global variable f to find the EchoFactory instance. As with most global variable usage this makes the code a little hard to follow. Further, I haven't added any code to handle connectionLost events to clear the connection attribute out. This means you might think you're sending data to the distributed server when the connection has already been lost. And similarly, there's no guarantee that the connection to the distributed server will have been created by the time the IRC client first tries to use it, so you may have an AttributeError when it tries to use f.connection.sendData.

    However, fixing these doesn't require much of a leap. Fix the global variable usage as you would any other - by passing arguments to functions, saving objects as references on other objects, etc. Fix the possible AttributeError by handling it, or by not creating the IRC connection until after you've created the distributed connection, etc. And handle lost connections by resetting the attribute value to None or some other sentinel, and paying attention to such a case in the IRC code before trying to use the distributed client connection to send any data.