[Buildbot-commits] buildbot/buildbot/status words.py,1.48,1.49

Brian Warner warner at users.sourceforge.net
Sun Jun 17 21:10:30 UTC 2007


Update of /cvsroot/buildbot/buildbot/buildbot/status
In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv21025/buildbot/status

Modified Files:
	words.py 
Log Message:
[project @ refactor the irc bot, in preparation for other IM status backends]

Original author: warner at lothar.com
Date: 2007-05-17 22:41:10+00:00

Index: words.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/words.py,v
retrieving revision 1.48
retrieving revision 1.49
diff -u -d -r1.48 -r1.49
--- words.py	7 Feb 2007 04:25:29 -0000	1.48
+++ words.py	17 Jun 2007 21:10:27 -0000	1.49
@@ -5,6 +5,7 @@
 
 import re, shlex
 
+from zope.interface import Interface, implements
 from twisted.internet import protocol, reactor
 from twisted.words.protocols import irc
 from twisted.python import log, failure
@@ -26,17 +27,15 @@
     hasStarted = False
     timer = None
 
-    def __init__(self, parent, reply):
+    def __init__(self, parent):
         self.parent = parent
-        self.reply = reply
         self.timer = reactor.callLater(5, self.soon)
 
     def soon(self):
         del self.timer
         if not self.hasStarted:
-            self.parent.reply(self.reply,
-                              "The build has been queued, I'll give a shout"
-                              " when it starts")
+            self.parent.send("The build has been queued, I'll give a shout"
+                             " when it starts")
 
     def started(self, c):
         self.hasStarted = True
@@ -48,14 +47,28 @@
         response = "build #%d forced" % s.getNumber()
         if eta is not None:
             response = "build forced [ETA %s]" % self.parent.convertTime(eta)
-        self.parent.reply(self.reply, response)
-        self.parent.reply(self.reply,
-                          "I'll give a shout when the build finishes")
+        self.parent.send(response)
+        self.parent.send("I'll give a shout when the build finishes")
         d = s.waitUntilFinished()
-        d.addCallback(self.parent.buildFinished, self.reply)
+        d.addCallback(self.parent.buildFinished)
 
 
-class IrcStatusBot(irc.IRCClient):
+class Contact:
+    """I hold the state for a single user's interaction with the buildbot.
+
+    This base class provides all the basic behavior (the queries and
+    responses). Subclasses for each channel type (IRC, different IM
+    protocols) are expected to provide the lower-level send/receive methods.
+
+    There will be one instance of me for each user who interacts personally
+    with the buildbot. There will be an additional instance for each
+    'broadcast contact' (chat rooms, IRC channels as a whole).
+    """
+
+    def __init__(self, channel, username):
+        self.channel = channel
+        self.username = username
+
     silly = {
         "What happen ?": "Somebody set up us the bomb.",
         "It's You !!": ["How are you gentlemen !!",
@@ -64,91 +77,6 @@
         "What you say !!": ["You have no chance to survive make your time.",
                             "HA HA HA HA ...."],
         }
-    def __init__(self, nickname, password, channels, status, categories):
-        """
-        @type  nickname: string
-        @param nickname: the nickname by which this bot should be known
-        @type  password: string
-        @param password: the password to use for identifying with Nickserv
-        @type  channels: list of strings
-        @param channels: the bot will maintain a presence in these channels
-        @type  status: L{buildbot.status.builder.Status}
-        @param status: the build master's Status object, through which the
-                       bot retrieves all status information
-        """
-        self.nickname = nickname
-        self.channels = channels
-        self.password = password
-        self.status = status
-        self.categories = categories
-        self.counter = 0
-        self.hasQuit = 0
-
-    def signedOn(self):
-        if self.password:
-            self.msg("Nickserv", "IDENTIFY " + self.password)
-        for c in self.channels:
-            self.join(c)
-    def joined(self, channel):
-        log.msg("I have joined", channel)
-    def left(self, channel):
-        log.msg("I have left", channel)
-    def kickedFrom(self, channel, kicker, message):
-        log.msg("I have been kicked from %s by %s: %s" % (channel,
-                                                          kicker,
-                                                          message))
-
-    # input
-    def privmsg(self, user, channel, message):
-        user = user.split('!', 1)[0] # rest is ~user at hostname
-        # channel is '#twisted' or 'buildbot' (for private messages)
-        channel = channel.lower()
-        #print "privmsg:", user, channel, message
-        if channel == self.nickname:
-            # private message
-            message = "%s: %s" % (self.nickname, message)
-            reply = user
-        else:
-            reply = channel
-        if message.startswith("%s:" % self.nickname):
-            message = message[len("%s:" % self.nickname):]
-
-            message = message.lstrip()
-            if self.silly.has_key(message):
-                return self.doSilly(user, reply, message)
-
-            parts = message.split(' ', 1)
-            if len(parts) == 1:
-                parts = parts + ['']
-            cmd, args = parts
-            log.msg("irc command", cmd)
-
-            meth = self.getCommandMethod(cmd)
-            if not meth and message[-1] == '!':
-                meth = self.command_EXCITED
-
-            error = None
-            try:
-                if meth:
-                    meth(user, reply, args.strip())
-            except UsageError, e:
-                self.reply(reply, str(e))
-            except:
-                f = failure.Failure()
-                log.err(f)
-                error = "Something bad happened (see logs): %s" % f.type
-
-            if error:
-                try:
-                    self.reply(reply, error)
-                except:
-                    log.err()
-
-            #self.say(channel, "count %d" % self.counter)
-            self.counter += 1
-    def reply(self, dest, message):
-        # maybe self.notice(dest, message) instead?
-        self.msg(dest, message)
 
     def getCommandMethod(self, command):
         meth = getattr(self, 'command_' + command.upper(), None)
@@ -156,16 +84,16 @@
 
     def getBuilder(self, which):
         try:
-            b = self.status.getBuilder(which)
+            b = self.channel.status.getBuilder(which)
         except KeyError:
             raise UsageError, "no such builder '%s'" % which
         return b
 
     def getControl(self, which):
-        if not self.control:
+        if not self.channel.control:
             raise UsageError("builder control is not enabled")
         try:
-            bc = self.control.getBuilder(which)
+            bc = self.channel.control.getBuilder(which)
         except KeyError:
             raise UsageError("no such builder '%s'" % which)
         return bc
@@ -174,9 +102,9 @@
         """
         @rtype: list of L{buildbot.process.builder.Builder}
         """
-        names = self.status.getBuilderNames(categories=self.categories)
+        names = self.channel.status.getBuilderNames(categories=self.categories)
         names.sort()
-        builders = [self.status.getBuilder(n) for n in names]
+        builders = [self.channel.status.getBuilder(n) for n in names]
         return builders
 
     def convertTime(self, seconds):
@@ -190,22 +118,22 @@
         minutes = minutes - 60*hours
         return "%dh%02dm%02ds" % (hours, minutes, seconds)
 
-    def doSilly(self, user, reply, message):
+    def doSilly(self, message):
         response = self.silly[message]
         if type(response) != type([]):
             response = [response]
         when = 0.5
         for r in response:
-            reactor.callLater(when, self.reply, reply, r)
+            reactor.callLater(when, self.send, r)
             when += 2.5
 
-    def command_HELLO(self, user, reply, args):
-        self.reply(reply, "yes?")
+    def command_HELLO(self, args):
+        self.send("yes?")
 
-    def command_VERSION(self, user, reply, args):
-        self.reply(reply, "buildbot-%s at your service" % version)
+    def command_VERSION(self, args):
+        self.send("buildbot-%s at your service" % version)
 
-    def command_LIST(self, user, reply, args):
+    def command_LIST(self, args):
         args = args.split()
         if len(args) == 0:
             raise UsageError, "try 'list builders'"
@@ -219,11 +147,11 @@
                     str += "[offline]"
                 str += " "
             str.rstrip()
-            self.reply(reply, str)
+            self.send(str)
             return
     command_LIST.usage = "list builders - List configured builders"
 
-    def command_STATUS(self, user, reply, args):
+    def command_STATUS(self, args):
         args = args.split()
         if len(args) == 0:
             which = "all"
@@ -234,12 +162,12 @@
         if which == "all":
             builders = self.getAllBuilders()
             for b in builders:
-                self.emit_status(reply, b.name)
+                self.emit_status(b.name)
             return
-        self.emit_status(reply, which)
+        self.emit_status(which)
     command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)"
 
-    def command_WATCH(self, user, reply, args):
+    def command_WATCH(self, args):
         args = args.split()
         if len(args) != 1:
             raise UsageError("try 'watch <builder>'")
@@ -247,22 +175,22 @@
         b = self.getBuilder(which)
         builds = b.getCurrentBuilds()
         if not builds:
-            self.reply(reply, "there are no builds currently running")
+            self.send("there are no builds currently running")
             return
         for build in builds:
             assert not build.isFinished()
             d = build.waitUntilFinished()
-            d.addCallback(self.buildFinished, reply)
+            d.addCallback(self.buildFinished)
             r = "watching build %s #%d until it finishes" \
                 % (which, build.getNumber())
             eta = build.getETA()
             if eta is not None:
                 r += " [%s]" % self.convertTime(eta)
             r += ".."
-            self.reply(reply, r)
+            self.send(r)
     command_WATCH.usage = "watch <which> - announce the completion of an active build"
 
-    def buildFinished(self, b, reply):
+    def buildFinished(self, b):
         results = {SUCCESS: "Success",
                    WARNINGS: "Warnings",
                    FAILURE: "Failure",
@@ -282,12 +210,12 @@
              b.getNumber(),
              results.get(b.getResults(), "??"))
         r += " [%s]" % " ".join(b.getText())
-        self.reply(reply, r)
-        buildurl = self.status.getURLForThing(b)
+        self.send(r)
+        buildurl = self.channel.status.getURLForThing(b)
         if buildurl:
-            self.reply(reply, "Build details are at %s" % buildurl)
+            self.send("Build details are at %s" % buildurl)
 
-    def command_FORCE(self, user, reply, args):
+    def command_FORCE(self, args):
         args = shlex.split(args) # TODO: this requires python2.3 or newer
         if args.pop(0) != "build":
             raise UsageError("try 'force build WHICH <REASON>'")
@@ -303,11 +231,11 @@
         # centralize this somewhere.
         if branch and not re.match(r'^[\w\.\-\/]*$', branch):
             log.msg("bad branch '%s'" % branch)
-            self.reply(reply, "sorry, bad branch '%s'" % branch)
+            self.send("sorry, bad branch '%s'" % branch)
             return
         if revision and not re.match(r'^[\w\.\-\/]*$', revision):
             log.msg("bad revision '%s'" % revision)
-            self.reply(reply, "sorry, bad revision '%s'" % revision)
+            self.send("sorry, bad revision '%s'" % revision)
             return
 
         bc = self.getControl(which)
@@ -315,9 +243,8 @@
         who = None # TODO: if we can authenticate that a particular User
                    # asked for this, use User Name instead of None so they'll
                    # be informed of the results.
-        # TODO: or, monitor this build and announce the results through the
-        # 'reply' argument.
-        r = "forced: by IRC user <%s>: %s" % (user, reason)
+        # TODO: or, monitor this build and announce the results
+        r = "forced: by IRC user <%s>: %s" % (self.username, reason)
         # TODO: maybe give certain users the ability to request builds of
         # certain branches
         s = SourceStamp(branch=branch, revision=revision)
@@ -325,16 +252,15 @@
         try:
             bc.requestBuildSoon(req)
         except interfaces.NoSlaveError:
-            self.reply(reply,
-                       "sorry, I can't force a build: all slaves are offline")
+            self.send("sorry, I can't force a build: all slaves are offline")
             return
-        ireq = IrcBuildRequest(self, reply)
+        ireq = IrcBuildRequest(self)
         req.subscribe(ireq.started)
 
 
     command_FORCE.usage = "force build <which> <reason> - Force a build"
 
-    def command_STOP(self, user, reply, args):
+    def command_STOP(self, args):
         args = args.split(None, 2)
         if len(args) < 3 or args[0] != 'build':
             raise UsageError, "try 'stop build WHICH <REASON>'"
@@ -344,13 +270,13 @@
         buildercontrol = self.getControl(which)
 
         who = None
-        r = "stopped: by IRC user <%s>: %s" % (user, reason)
+        r = "stopped: by IRC user <%s>: %s" % (self.username, reason)
 
         # find an in-progress build
         builderstatus = self.getBuilder(which)
         builds = builderstatus.getCurrentBuilds()
         if not builds:
-            self.reply(reply, "sorry, no build is currently running")
+            self.send("sorry, no build is currently running")
             return
         for build in builds:
             num = build.getNumber()
@@ -361,11 +287,11 @@
             # make it stop
             buildcontrol.stopBuild(r)
 
-            self.reply(reply, "build %d interrupted" % num)
+            self.send("build %d interrupted" % num)
 
     command_STOP.usage = "stop build <which> <reason> - Stop a running build"
 
-    def emit_status(self, reply, which):
+    def emit_status(self, which):
         b = self.getBuilder(which)
         str = "%s: " % which
         state, builds = b.getState()
@@ -386,9 +312,9 @@
                     s += " [ETA %s]" % self.convertTime(ETA)
                 t.append(s)
             str += ", ".join(t)
-        self.reply(reply, str)
+        self.send(str)
 
-    def emit_last(self, reply, which):
+    def emit_last(self, which):
         last = self.getBuilder(which).getLastFinishedBuild()
         if not last:
             str = "(no builds run since last restart)"
@@ -396,9 +322,9 @@
             start,finish = last.getTimes()
             str = "%s secs ago: " % (int(util.now() - finish))
             str += " ".join(last.getText())
-        self.reply(reply, "last build [%s]: %s" % (which, str))
+        self.send("last build [%s]: %s" % (which, str))
 
-    def command_LAST(self, user, reply, args):
+    def command_LAST(self, args):
         args = args.split()
         if len(args) == 0:
             which = "all"
@@ -409,9 +335,9 @@
         if which == "all":
             builders = self.getAllBuilders()
             for b in builders:
-                self.emit_last(reply, b.name)
+                self.emit_last(b.name)
             return
-        self.emit_last(reply, which)
+        self.emit_last(which)
     command_LAST.usage = "last <which> - list last build status for builder <which>"
 
     def build_commands(self):
@@ -422,10 +348,10 @@
         commands.sort()
         return commands
 
-    def command_HELP(self, user, reply, args):
+    def command_HELP(self, args):
         args = args.split()
         if len(args) == 0:
-            self.reply(reply, "Get help on what? (try 'help <foo>', or 'commands' for a command list)")
+            self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)")
             return
         command = args[0]
         meth = self.getCommandMethod(command)
@@ -433,56 +359,208 @@
             raise UsageError, "no such command '%s'" % command
         usage = getattr(meth, 'usage', None)
         if usage:
-            self.reply(reply, "Usage: %s" % usage)
+            self.send("Usage: %s" % usage)
         else:
-            self.reply(reply, "No usage info for '%s'" % command)
+            self.send("No usage info for '%s'" % command)
     command_HELP.usage = "help <command> - Give help for <command>"
 
-    def command_SOURCE(self, user, reply, args):
+    def command_SOURCE(self, args):
         banner = "My source can be found at http://buildbot.sourceforge.net/"
-        self.reply(reply, banner)
+        self.send(banner)
 
-    def command_COMMANDS(self, user, reply, args):
+    def command_COMMANDS(self, args):
         commands = self.build_commands()
         str = "buildbot commands: " + ", ".join(commands)
-        self.reply(reply, str)
+        self.send(str)
     command_COMMANDS.usage = "commands - List available commands"
 
-    def command_DESTROY(self, user, reply, args):
-        self.me(reply, "readies phasers")
+    def command_DESTROY(self, args):
+        self.act("readies phasers")
 
-    def command_DANCE(self, user, reply, args):
-        reactor.callLater(1.0, self.reply, reply, "0-<")
-        reactor.callLater(3.0, self.reply, reply, "0-/")
-        reactor.callLater(3.5, self.reply, reply, "0-\\")
+    def command_DANCE(self, args):
+        reactor.callLater(1.0, self.send, "0-<")
+        reactor.callLater(3.0, self.send, "0-/")
+        reactor.callLater(3.5, self.send, "0-\\")
 
-    def command_EXCITED(self, user, reply, args):
+    def command_EXCITED(self, args):
         # like 'buildbot: destroy the sun!'
-        self.reply(reply, "What you say!")
+        self.send("What you say!")
+
+    def handleAction(self, data, user):
+        # this is sent when somebody performs an action that mentions the
+        # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of
+        # the person who performed the action, so if their action provokes a
+        # response, they can be named.
+        if not data.endswith("s buildbot"):
+            return
+        words = data.split()
+        verb = words[-2]
+        timeout = 4
+        if verb == "kicks":
+            response = "%s back" % verb
+            timeout = 1
+        else:
+            response = "%s %s too" % (verb, user)
+        reactor.callLater(timeout, self.act, response)
+
+class IRCContact(Contact):
+    # this is the IRC-specific subclass of Contact
+
+    def __init__(self, channel, dest):
+        Contact.__init__(self, channel)
+        self.dest = dest
+
+    # userJoined(self, user, channel)
+
+    def send(self, message):
+        self.channel.msg(self.dest, message)
+    def act(self, action):
+        self.channel.me(self.dest, action)
+
+
+    def handleMessage(self, message):
+        message = message.lstrip()
+        if self.silly.has_key(message):
+            return self.doSilly(message)
+
+        parts = message.split(' ', 1)
+        if len(parts) == 1:
+            parts = parts + ['']
+        cmd, args = parts
+        log.msg("irc command", cmd)
+
+        meth = self.getCommandMethod(cmd)
+        if not meth and message[-1] == '!':
+            meth = self.command_EXCITED
+
+        error = None
+        try:
+            if meth:
+                meth(args.strip())
+        except UsageError, e:
+            self.send(str(e))
+        except:
+            f = failure.Failure()
+            log.err(f)
+            error = "Something bad happened (see logs): %s" % f.type
+
+        if error:
+            try:
+                self.send(error)
+            except:
+                log.err()
+
+        #self.say(channel, "count %d" % self.counter)
+        self.channel.counter += 1
+
+class IChannel(Interface):
+    """I represent the buildbot's presence in a particular IM scheme.
+
+    This provides the connection to the IRC server, or represents the
+    buildbot's account with an IM service. Each Channel will have zero or
+    more Contacts associated with it.
+    """
+
+class IrcStatusBot(irc.IRCClient):
+    """I represent the buildbot to an IRC server.
+    """
+    implements(IChannel)
+
+    def __init__(self, nickname, password, channels, status, categories):
+        """
+        @type  nickname: string
+        @param nickname: the nickname by which this bot should be known
+        @type  password: string
+        @param password: the password to use for identifying with Nickserv
+        @type  channels: list of strings
+        @param channels: the bot will maintain a presence in these channels
+        @type  status: L{buildbot.status.builder.Status}
+        @param status: the build master's Status object, through which the
+                       bot retrieves all status information
+        """
+        self.nickname = nickname
+        self.channels = channels
+        self.password = password
+        self.status = status
+        self.categories = categories
+        self.counter = 0
+        self.hasQuit = 0
+        self.contacts = {}
+
+    def addContact(self, name, contact):
+        self.contacts[name] = contact
+
+    def getContact(self, name):
+        if name in self.contacts:
+            return self.contacts[name]
+        new_contact = IRCContact(self, name)
+        self.contacts[name] = new_contact
+        return new_contact
+
+    def deleteContact(self, contact):
+        name = contact.getName()
+        if name in self.contacts:
+            assert self.contacts[name] == contact
+            del self.contacts[name]
+
+    def log(self, msg):
+        log.msg("%s: %s" % (self, msg))
+
+
+    # the following irc.IRCClient methods are called when we have input
+
+    def privmsg(self, user, channel, message):
+        user = user.split('!', 1)[0] # rest is ~user at hostname
+        # channel is '#twisted' or 'buildbot' (for private messages)
+        channel = channel.lower()
+        #print "privmsg:", user, channel, message
+        if channel == self.nickname:
+            # private message
+            contact = self.getContact(user)
+            contact.handleMessage(message)
+            return
+        # else it's a broadcast message, maybe for us, maybe not. 'channel'
+        # is '#twisted' or the like.
+        contact = self.getContact(channel)
+        if message.startswith("%s:" % self.nickname):
+            message = message[len("%s:" % self.nickname):]
+            contact.handleMessage(message)
+        # to track users comings and goings, add code here
 
     def action(self, user, channel, data):
         #log.msg("action: %s,%s,%s" % (user, channel, data))
         user = user.split('!', 1)[0] # rest is ~user at hostname
-        # somebody did an action (/me actions)
-        if data.endswith("s buildbot"):
-            words = data.split()
-            verb = words[-2]
-            timeout = 4
-            if verb == "kicks":
-                response = "%s back" % verb
-                timeout = 1
-            else:
-                response = "%s %s too" % (verb, user)
-            reactor.callLater(timeout, self.me, channel, response)
-    # userJoined(self, user, channel)
-    
-    # output
+        # somebody did an action (/me actions) in the broadcast channel
+        contact = self.getContact(channel)
+        if "buildbot" in data:
+            contact.handleAction(data, user)
+
+
+
+    def signedOn(self):
+        if self.password:
+            self.msg("Nickserv", "IDENTIFY " + self.password)
+        for c in self.channels:
+            self.join(c)
+
+    def joined(self, channel):
+        self.log("I have joined %s" % (channel,))
+    def left(self, channel):
+        self.log("I have left %s" % (channel,))
+    def kickedFrom(self, channel, kicker, message):
+        self.log("I have been kicked from %s by %s: %s" % (channel,
+                                                          kicker,
+                                                          message))
+
+    # we can using the following irc.IRCClient methods to send output. Most
+    # of these are used by the IRCContact class.
+    #
     # self.say(channel, message) # broadcast
     # self.msg(user, message) # unicast
     # self.me(channel, action) # send action
     # self.away(message='')
     # self.quit(message='')
-    
+
 class ThrottledClientFactory(protocol.ClientFactory):
     lostDelay = 2
     failedDelay = 60
@@ -586,20 +664,6 @@
         return base.StatusReceiverMultiService.stopService(self)
 
 
-def main():
-    from twisted.internet import app
-    a = app.Application("irctest")
-    f = IrcStatusFactory()
-    host = "localhost"
-    port = 6667
-    f.addNetwork((host, port), ["private", "other"])
-    a.connectTCP(host, port, f)
-    a.run(save=0)
-    
-
-if __name__ == '__main__':
-    main()
-
 ## buildbot: list builders
 # buildbot: watch quick
 #  print notification when current build in 'quick' finishes





More information about the Commits mailing list