Package buildbot :: Package status :: Module words
[frames] | no frames]

Source Code for Module buildbot.status.words

  1   
  2  # code to deliver build status through twisted.words (instant messaging 
  3  # protocols: irc, etc) 
  4   
  5  import re, shlex 
  6  from string import join, capitalize, lower 
  7   
  8  from zope.interface import Interface, implements 
  9  from twisted.internet import protocol, reactor 
 10  from twisted.words.protocols import irc 
 11  from twisted.python import log, failure 
 12  from twisted.application import internet 
 13   
 14  from buildbot import interfaces, util 
 15  from buildbot import version 
 16  from buildbot.interfaces import IStatusReceiver 
 17  from buildbot.sourcestamp import SourceStamp 
 18  from buildbot.status import base 
 19  from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION 
 20  from buildbot.scripts.runner import ForceOptions 
 21   
 22  # twisted.internet.ssl requires PyOpenSSL, so be resilient if it's missing 
 23  try: 
 24      from twisted.internet import ssl 
 25      have_ssl = True 
 26  except ImportError: 
 27      have_ssl = False 
 28   
29 -class UsageError(ValueError):
30 - def __init__(self, string = "Invalid usage", *more):
31 ValueError.__init__(self, string, *more)
32
33 -class IrcBuildRequest:
34 hasStarted = False 35 timer = None 36
37 - def __init__(self, parent):
38 self.parent = parent 39 self.timer = reactor.callLater(5, self.soon)
40
41 - def soon(self):
42 del self.timer 43 if not self.hasStarted: 44 self.parent.send("The build has been queued, I'll give a shout" 45 " when it starts")
46
47 - def started(self, s):
48 self.hasStarted = True 49 if self.timer: 50 self.timer.cancel() 51 del self.timer 52 eta = s.getETA() 53 response = "build #%d forced" % s.getNumber() 54 if eta is not None: 55 response = "build forced [ETA %s]" % self.parent.convertTime(eta) 56 self.parent.send(response) 57 self.parent.send("I'll give a shout when the build finishes") 58 d = s.waitUntilFinished() 59 d.addCallback(self.parent.watchedBuildFinished)
60
61 -class Contact(base.StatusReceiver):
62 implements(IStatusReceiver) 63 """I hold the state for a single user's interaction with the buildbot. 64 65 This base class provides all the basic behavior (the queries and 66 responses). Subclasses for each channel type (IRC, different IM 67 protocols) are expected to provide the lower-level send/receive methods. 68 69 There will be one instance of me for each user who interacts personally 70 with the buildbot. There will be an additional instance for each 71 'broadcast contact' (chat rooms, IRC channels as a whole). 72 """ 73
74 - def __init__(self, channel):
75 #StatusReceiver.__init__(self) doesn't exist 76 self.channel = channel 77 self.notify_events = {} 78 self.subscribed = 0 79 self.add_notification_events(channel.notify_events)
80 81 silly = { 82 "What happen ?": "Somebody set up us the bomb.", 83 "It's You !!": ["How are you gentlemen !!", 84 "All your base are belong to us.", 85 "You are on the way to destruction."], 86 "What you say !!": ["You have no chance to survive make your time.", 87 "HA HA HA HA ...."], 88 } 89
90 - def getCommandMethod(self, command):
91 meth = getattr(self, 'command_' + command.upper(), None) 92 return meth
93
94 - def getBuilder(self, which):
95 try: 96 b = self.channel.status.getBuilder(which) 97 except KeyError: 98 raise UsageError, "no such builder '%s'" % which 99 return b
100
101 - def getControl(self, which):
102 if not self.channel.control: 103 raise UsageError("builder control is not enabled") 104 try: 105 bc = self.channel.control.getBuilder(which) 106 except KeyError: 107 raise UsageError("no such builder '%s'" % which) 108 return bc
109
110 - def getAllBuilders(self):
111 """ 112 @rtype: list of L{buildbot.process.builder.Builder} 113 """ 114 names = self.channel.status.getBuilderNames(categories=self.channel.categories) 115 names.sort() 116 builders = [self.channel.status.getBuilder(n) for n in names] 117 return builders
118
119 - def convertTime(self, seconds):
120 if seconds < 60: 121 return "%d seconds" % seconds 122 minutes = int(seconds / 60) 123 seconds = seconds - 60*minutes 124 if minutes < 60: 125 return "%dm%02ds" % (minutes, seconds) 126 hours = int(minutes / 60) 127 minutes = minutes - 60*hours 128 return "%dh%02dm%02ds" % (hours, minutes, seconds)
129
130 - def doSilly(self, message):
131 response = self.silly[message] 132 if type(response) != type([]): 133 response = [response] 134 when = 0.5 135 for r in response: 136 reactor.callLater(when, self.send, r) 137 when += 2.5
138
139 - def command_HELLO(self, args, who):
140 self.send("yes?")
141
142 - def command_VERSION(self, args, who):
143 self.send("buildbot-%s at your service" % version)
144
145 - def command_LIST(self, args, who):
146 args = shlex.split(args) 147 if len(args) == 0: 148 raise UsageError, "try 'list builders'" 149 if args[0] == 'builders': 150 builders = self.getAllBuilders() 151 str = "Configured builders: " 152 for b in builders: 153 str += b.name 154 state = b.getState()[0] 155 if state == 'offline': 156 str += "[offline]" 157 str += " " 158 str.rstrip() 159 self.send(str) 160 return
161 command_LIST.usage = "list builders - List configured builders" 162
163 - def command_STATUS(self, args, who):
164 args = shlex.split(args) 165 if len(args) == 0: 166 which = "all" 167 elif len(args) == 1: 168 which = args[0] 169 else: 170 raise UsageError, "try 'status <builder>'" 171 if which == "all": 172 builders = self.getAllBuilders() 173 for b in builders: 174 self.emit_status(b.name) 175 return 176 self.emit_status(which)
177 command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)" 178
179 - def validate_notification_event(self, event):
180 if not re.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event): 181 raise UsageError("try 'notify on|off <EVENT>'")
182
183 - def list_notified_events(self):
184 self.send( "The following events are being notified: %r" % self.notify_events.keys() )
185
186 - def notify_for(self, *events):
187 for event in events: 188 if self.notify_events.has_key(event): 189 return 1 190 return 0
191
193 self.channel.status.subscribe(self) 194 self.subscribed = 1
195
197 self.channel.status.unsubscribe(self) 198 self.subscribed = 0
199
200 - def add_notification_events(self, events):
201 for event in events: 202 self.validate_notification_event(event) 203 self.notify_events[event] = 1 204 205 if not self.subscribed: 206 self.subscribe_to_build_events()
207
208 - def remove_notification_events(self, events):
209 for event in events: 210 self.validate_notification_event(event) 211 del self.notify_events[event] 212 213 if len(self.notify_events) == 0 and self.subscribed: 214 self.unsubscribe_from_build_events()
215
217 self.notify_events = {} 218 219 if self.subscribed: 220 self.unsubscribe_from_build_events()
221
222 - def command_NOTIFY(self, args, who):
223 args = shlex.split(args) 224 225 if not args: 226 raise UsageError("try 'notify on|off|list <EVENT>'") 227 action = args.pop(0) 228 events = args 229 230 if action == "on": 231 if not events: events = ('started','finished') 232 self.add_notification_events(events) 233 234 self.list_notified_events() 235 236 elif action == "off": 237 if events: 238 self.remove_notification_events(events) 239 else: 240 self.remove_all_notification_events() 241 242 self.list_notified_events() 243 244 elif action == "list": 245 self.list_notified_events() 246 return 247 248 else: 249 raise UsageError("try 'notify on|off <EVENT>'")
250 251 command_NOTIFY.usage = "notify on|off|list [<EVENT>] ... - Notify me about build events. event should be one or more of: 'started', 'finished', 'failure', 'success', 'exception' or 'xToY' (where x and Y are one of success, warnings, failure, exception, but Y is capitalized)" 252
253 - def command_WATCH(self, args, who):
254 args = shlex.split(args) 255 if len(args) != 1: 256 raise UsageError("try 'watch <builder>'") 257 which = args[0] 258 b = self.getBuilder(which) 259 builds = b.getCurrentBuilds() 260 if not builds: 261 self.send("there are no builds currently running") 262 return 263 for build in builds: 264 assert not build.isFinished() 265 d = build.waitUntilFinished() 266 d.addCallback(self.watchedBuildFinished) 267 r = "watching build %s #%d until it finishes" \ 268 % (which, build.getNumber()) 269 eta = build.getETA() 270 if eta is not None: 271 r += " [%s]" % self.convertTime(eta) 272 r += ".." 273 self.send(r)
274 command_WATCH.usage = "watch <which> - announce the completion of an active build" 275
276 - def buildsetSubmitted(self, buildset):
277 log.msg('[Contact] Buildset %s added' % (buildset))
278
279 - def builderAdded(self, builderName, builder):
280 log.msg('[Contact] Builder %s added' % (builder)) 281 builder.subscribe(self)
282
283 - def builderChangedState(self, builderName, state):
284 log.msg('[Contact] Builder %s changed state to %s' % (builderName, state))
285
286 - def requestSubmitted(self, brstatus):
287 log.msg('[Contact] BuildRequest for %s submitted to Builder %s' % 288 (brstatus.getSourceStamp(), brstatus.builderName))
289
290 - def builderRemoved(self, builderName):
291 log.msg('[Contact] Builder %s removed' % (builderName))
292
293 - def buildStarted(self, builderName, build):
294 builder = build.getBuilder() 295 log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category)) 296 297 # only notify about builders we are interested in 298 299 if (self.channel.categories != None and 300 builder.category not in self.channel.categories): 301 log.msg('Not notifying for a build in the wrong category') 302 return 303 304 if not self.notify_for('started'): 305 log.msg('Not notifying for a build when started-notification disabled') 306 return 307 308 r = "build #%d of %s started, including [%s]" % \ 309 (build.getNumber(), 310 builder.getName(), 311 ", ".join([str(c.revision) for c in build.getChanges()]) 312 ) 313 314 self.send(r)
315 316 results_descriptions = { 317 SUCCESS: "Success", 318 WARNINGS: "Warnings", 319 FAILURE: "Failure", 320 EXCEPTION: "Exception", 321 } 322
323 - def buildFinished(self, builderName, build, results):
324 builder = build.getBuilder() 325 326 # only notify about builders we are interested in 327 log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category)) 328 329 if (self.channel.categories != None and 330 builder.category not in self.channel.categories): 331 return 332 333 if not self.notify_for_finished(build): 334 return 335 336 r = "build #%d of %s is complete: %s" % \ 337 (build.getNumber(), 338 builder.getName(), 339 self.results_descriptions.get(build.getResults(), "??")) 340 r += " [%s]" % " ".join(build.getText()) 341 buildurl = self.channel.status.getURLForThing(build) 342 if buildurl: 343 r += " Build details are at %s" % buildurl 344 345 if self.channel.showBlameList and build.getResults() != SUCCESS and len(build.changes) != 0: 346 r += ' blamelist: ' + ', '.join([c.who for c in build.changes]) 347 348 self.send(r)
349
350 - def notify_for_finished(self, build):
351 results = build.getResults() 352 353 if self.notify_for('finished'): 354 return True 355 356 if self.notify_for(lower(self.results_descriptions.get(results))): 357 return True 358 359 prevBuild = build.getPreviousBuild() 360 if prevBuild: 361 prevResult = prevBuild.getResults() 362 363 required_notification_control_string = join((lower(self.results_descriptions.get(prevResult)), \ 364 'To', \ 365 capitalize(self.results_descriptions.get(results))), \ 366 '') 367 368 if (self.notify_for(required_notification_control_string)): 369 return True 370 371 return False
372
373 - def watchedBuildFinished(self, b):
374 375 # only notify about builders we are interested in 376 builder = b.getBuilder() 377 log.msg('builder %r in category %s finished' % (builder, 378 builder.category)) 379 if (self.channel.categories != None and 380 builder.category not in self.channel.categories): 381 return 382 383 r = "Hey! build %s #%d is complete: %s" % \ 384 (b.getBuilder().getName(), 385 b.getNumber(), 386 self.results_descriptions.get(b.getResults(), "??")) 387 r += " [%s]" % " ".join(b.getText()) 388 self.send(r) 389 buildurl = self.channel.status.getURLForThing(b) 390 if buildurl: 391 self.send("Build details are at %s" % buildurl)
392
393 - def command_FORCE(self, args, who):
394 args = shlex.split(args) 395 if not args: 396 raise UsageError("try 'force build WHICH <REASON>'") 397 what = args.pop(0) 398 if what != "build": 399 raise UsageError("try 'force build WHICH <REASON>'") 400 opts = ForceOptions() 401 opts.parseOptions(args) 402 403 which = opts['builder'] 404 branch = opts['branch'] 405 revision = opts['revision'] 406 reason = opts['reason'] 407 408 if which is None: 409 raise UsageError("you must provide a Builder, " 410 "try 'force build WHICH <REASON>'") 411 412 # keep weird stuff out of the branch and revision strings. 413 # TODO: centralize this somewhere. 414 if branch and not re.match(r'^[\w\.\-\/]*$', branch): 415 log.msg("bad branch '%s'" % branch) 416 self.send("sorry, bad branch '%s'" % branch) 417 return 418 if revision and not re.match(r'^[\w\.\-\/]*$', revision): 419 log.msg("bad revision '%s'" % revision) 420 self.send("sorry, bad revision '%s'" % revision) 421 return 422 423 bc = self.getControl(which) 424 425 # TODO: maybe give certain users the ability to request builds of 426 # certain branches 427 reason = "forced: by %s: %s" % (self.describeUser(who), reason) 428 ss = SourceStamp(branch=branch, revision=revision) 429 try: 430 brs = bc.submitBuildRequest(ss, reason, now=True) 431 except interfaces.NoSlaveError: 432 self.send("sorry, I can't force a build: all slaves are offline") 433 return 434 ireq = IrcBuildRequest(self) 435 brs.subscribe(ireq.started)
436 437 438 command_FORCE.usage = "force build <which> <reason> - Force a build" 439
440 - def command_STOP(self, args, who):
441 args = shlex.split(args) 442 if len(args) < 3 or args[0] != 'build': 443 raise UsageError, "try 'stop build WHICH <REASON>'" 444 which = args[1] 445 reason = args[2] 446 447 buildercontrol = self.getControl(which) 448 449 r = "stopped: by %s: %s" % (self.describeUser(who), reason) 450 451 # find an in-progress build 452 builderstatus = self.getBuilder(which) 453 builds = builderstatus.getCurrentBuilds() 454 if not builds: 455 self.send("sorry, no build is currently running") 456 return 457 for build in builds: 458 num = build.getNumber() 459 460 # obtain the BuildControl object 461 buildcontrol = buildercontrol.getBuild(num) 462 463 # make it stop 464 buildcontrol.stopBuild(r) 465 466 self.send("build %d interrupted" % num)
467 468 command_STOP.usage = "stop build <which> <reason> - Stop a running build" 469
470 - def emit_status(self, which):
471 b = self.getBuilder(which) 472 str = "%s: " % which 473 state, builds = b.getState() 474 str += state 475 if state == "idle": 476 last = b.getLastFinishedBuild() 477 if last: 478 start,finished = last.getTimes() 479 str += ", last build %s ago: %s" % \ 480 (self.convertTime(int(util.now() - finished)), " ".join(last.getText())) 481 if state == "building": 482 t = [] 483 for build in builds: 484 step = build.getCurrentStep() 485 if step: 486 s = "(%s)" % " ".join(step.getText()) 487 else: 488 s = "(no current step)" 489 ETA = build.getETA() 490 if ETA is not None: 491 s += " [ETA %s]" % self.convertTime(ETA) 492 t.append(s) 493 str += ", ".join(t) 494 self.send(str)
495
496 - def emit_last(self, which):
497 last = self.getBuilder(which).getLastFinishedBuild() 498 if not last: 499 str = "(no builds run since last restart)" 500 else: 501 start,finish = last.getTimes() 502 str = "%s ago: " % (self.convertTime(int(util.now() - finish))) 503 str += " ".join(last.getText()) 504 self.send("last build [%s]: %s" % (which, str))
505
506 - def command_LAST(self, args, who):
507 args = shlex.split(args) 508 if len(args) == 0: 509 which = "all" 510 elif len(args) == 1: 511 which = args[0] 512 else: 513 raise UsageError, "try 'last <builder>'" 514 if which == "all": 515 builders = self.getAllBuilders() 516 for b in builders: 517 self.emit_last(b.name) 518 return 519 self.emit_last(which)
520 command_LAST.usage = "last <which> - list last build status for builder <which>" 521
522 - def build_commands(self):
523 commands = [] 524 for k in dir(self): 525 if k.startswith('command_'): 526 commands.append(k[8:].lower()) 527 commands.sort() 528 return commands
529
530 - def command_HELP(self, args, who):
531 args = shlex.split(args) 532 if len(args) == 0: 533 self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)") 534 return 535 command = args[0] 536 meth = self.getCommandMethod(command) 537 if not meth: 538 raise UsageError, "no such command '%s'" % command 539 usage = getattr(meth, 'usage', None) 540 if usage: 541 self.send("Usage: %s" % usage) 542 else: 543 self.send("No usage info for '%s'" % command)
544 command_HELP.usage = "help <command> - Give help for <command>" 545
546 - def command_SOURCE(self, args, who):
547 banner = "My source can be found at http://buildbot.net/" 548 self.send(banner)
549
550 - def command_COMMANDS(self, args, who):
551 commands = self.build_commands() 552 str = "buildbot commands: " + ", ".join(commands) 553 self.send(str)
554 command_COMMANDS.usage = "commands - List available commands" 555
556 - def command_DESTROY(self, args, who):
557 self.act("readies phasers")
558
559 - def command_DANCE(self, args, who):
560 reactor.callLater(1.0, self.send, "0-<") 561 reactor.callLater(3.0, self.send, "0-/") 562 reactor.callLater(3.5, self.send, "0-\\")
563
564 - def command_EXCITED(self, args, who):
565 # like 'buildbot: destroy the sun!' 566 self.send("What you say!")
567
568 - def handleAction(self, data, user):
569 # this is sent when somebody performs an action that mentions the 570 # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of 571 # the person who performed the action, so if their action provokes a 572 # response, they can be named. 573 if not data.endswith("s buildbot"): 574 return 575 words = data.split() 576 verb = words[-2] 577 timeout = 4 578 if verb == "kicks": 579 response = "%s back" % verb 580 timeout = 1 581 else: 582 response = "%s %s too" % (verb, user) 583 reactor.callLater(timeout, self.act, response)
584
585 -class IRCContact(Contact):
586 implements(IStatusReceiver) 587 588 # this is the IRC-specific subclass of Contact 589
590 - def __init__(self, channel, dest):
591 Contact.__init__(self, channel) 592 # when people send us public messages ("buildbot: command"), 593 # self.dest is the name of the channel ("#twisted"). When they send 594 # us private messages (/msg buildbot command), self.dest is their 595 # username. 596 self.dest = dest
597
598 - def describeUser(self, user):
599 if self.dest[0] == '#': 600 return "IRC user <%s> on channel %s" % (user, self.dest) 601 return "IRC user <%s> (privmsg)" % user
602 603 # userJoined(self, user, channel) 604
605 - def send(self, message):
606 self.channel.msgOrNotice(self.dest, message.encode("ascii", "replace"))
607
608 - def act(self, action):
609 self.channel.me(self.dest, action.encode("ascii", "replace"))
610
611 - def command_JOIN(self, args, who):
612 args = shlex.split(args) 613 to_join = args[0] 614 self.channel.join(to_join) 615 self.send("Joined %s" % to_join)
616 command_JOIN.usage = "join channel - Join another channel" 617
618 - def command_LEAVE(self, args, who):
619 args = shlex.split(args) 620 to_leave = args[0] 621 self.send("Buildbot has been told to leave %s" % to_leave) 622 self.channel.part(to_leave)
623 command_LEAVE.usage = "leave channel - Leave a channel" 624 625
626 - def handleMessage(self, message, who):
627 # a message has arrived from 'who'. For broadcast contacts (i.e. when 628 # people do an irc 'buildbot: command'), this will be a string 629 # describing the sender of the message in some useful-to-log way, and 630 # a single Contact may see messages from a variety of users. For 631 # unicast contacts (i.e. when people do an irc '/msg buildbot 632 # command'), a single Contact will only ever see messages from a 633 # single user. 634 message = message.lstrip() 635 if self.silly.has_key(message): 636 return self.doSilly(message) 637 638 parts = message.split(' ', 1) 639 if len(parts) == 1: 640 parts = parts + [''] 641 cmd, args = parts 642 log.msg("irc command", cmd) 643 644 meth = self.getCommandMethod(cmd) 645 if not meth and message[-1] == '!': 646 meth = self.command_EXCITED 647 648 error = None 649 try: 650 if meth: 651 meth(args.strip(), who) 652 except UsageError, e: 653 self.send(str(e)) 654 except: 655 f = failure.Failure() 656 log.err(f) 657 error = "Something bad happened (see logs): %s" % f.type 658 659 if error: 660 try: 661 self.send(error) 662 except: 663 log.err() 664 665 #self.say(channel, "count %d" % self.counter) 666 self.channel.counter += 1
667
668 -class IChannel(Interface):
669 """I represent the buildbot's presence in a particular IM scheme. 670 671 This provides the connection to the IRC server, or represents the 672 buildbot's account with an IM service. Each Channel will have zero or 673 more Contacts associated with it. 674 """
675
676 -class IrcStatusBot(irc.IRCClient):
677 """I represent the buildbot to an IRC server. 678 """ 679 implements(IChannel) 680 contactClass = IRCContact 681
682 - def __init__(self, nickname, password, channels, status, categories, notify_events, noticeOnChannel = False, showBlameList = False):
683 """ 684 @type nickname: string 685 @param nickname: the nickname by which this bot should be known 686 @type password: string 687 @param password: the password to use for identifying with Nickserv 688 @type channels: list of strings 689 @param channels: the bot will maintain a presence in these channels 690 @type status: L{buildbot.status.builder.Status} 691 @param status: the build master's Status object, through which the 692 bot retrieves all status information 693 """ 694 self.nickname = nickname 695 self.channels = channels 696 self.password = password 697 self.status = status 698 self.categories = categories 699 self.notify_events = notify_events 700 self.counter = 0 701 self.hasQuit = 0 702 self.contacts = {} 703 self.noticeOnChannel = noticeOnChannel 704 self.showBlameList = showBlameList
705
706 - def msgOrNotice(self, dest, message):
707 if self.noticeOnChannel and dest[0] == '#': 708 self.notice(dest, message) 709 else: 710 self.msg(dest, message)
711
712 - def addContact(self, name, contact):
713 self.contacts[name] = contact
714
715 - def getContact(self, name):
716 if name in self.contacts: 717 return self.contacts[name] 718 new_contact = self.contactClass(self, name) 719 self.contacts[name] = new_contact 720 return new_contact
721
722 - def deleteContact(self, contact):
723 name = contact.getName() 724 if name in self.contacts: 725 assert self.contacts[name] == contact 726 del self.contacts[name]
727
728 - def log(self, msg):
729 log.msg("%s: %s" % (self, msg))
730 731 732 # the following irc.IRCClient methods are called when we have input 733
734 - def privmsg(self, user, channel, message):
735 user = user.split('!', 1)[0] # rest is ~user@hostname 736 # channel is '#twisted' or 'buildbot' (for private messages) 737 channel = channel.lower() 738 #print "privmsg:", user, channel, message 739 if channel == self.nickname: 740 # private message 741 contact = self.getContact(user) 742 contact.handleMessage(message, user) 743 return 744 # else it's a broadcast message, maybe for us, maybe not. 'channel' 745 # is '#twisted' or the like. 746 contact = self.getContact(channel) 747 if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname): 748 message = message[len("%s:" % self.nickname):] 749 contact.handleMessage(message, user)
750 # to track users comings and goings, add code here 751
752 - def action(self, user, channel, data):
753 #log.msg("action: %s,%s,%s" % (user, channel, data)) 754 user = user.split('!', 1)[0] # rest is ~user@hostname 755 # somebody did an action (/me actions) in the broadcast channel 756 contact = self.getContact(channel) 757 if "buildbot" in data: 758 contact.handleAction(data, user)
759 760 761
762 - def signedOn(self):
763 if self.password: 764 self.msg("Nickserv", "IDENTIFY " + self.password) 765 for c in self.channels: 766 self.join(c)
767
768 - def joined(self, channel):
769 self.log("I have joined %s" % (channel,)) 770 # trigger contact contructor, which in turn subscribes to notify events 771 self.getContact(channel)
772
773 - def left(self, channel):
774 self.log("I have left %s" % (channel,))
775 - def kickedFrom(self, channel, kicker, message):
776 self.log("I have been kicked from %s by %s: %s" % (channel, 777 kicker, 778 message))
779 780 # we can using the following irc.IRCClient methods to send output. Most 781 # of these are used by the IRCContact class. 782 # 783 # self.say(channel, message) # broadcast 784 # self.msg(user, message) # unicast 785 # self.me(channel, action) # send action 786 # self.away(message='') 787 # self.quit(message='') 788
789 -class ThrottledClientFactory(protocol.ClientFactory):
790 lostDelay = 2 791 failedDelay = 60
792 - def clientConnectionLost(self, connector, reason):
793 reactor.callLater(self.lostDelay, connector.connect)
794 - def clientConnectionFailed(self, connector, reason):
795 reactor.callLater(self.failedDelay, connector.connect)
796
797 -class IrcStatusFactory(ThrottledClientFactory):
798 protocol = IrcStatusBot 799 800 status = None 801 control = None 802 shuttingDown = False 803 p = None 804
805 - def __init__(self, nickname, password, channels, categories, notify_events, noticeOnChannel = False, showBlameList = False):
806 #ThrottledClientFactory.__init__(self) # doesn't exist 807 self.status = None 808 self.nickname = nickname 809 self.password = password 810 self.channels = channels 811 self.categories = categories 812 self.notify_events = notify_events 813 self.noticeOnChannel = noticeOnChannel 814 self.showBlameList = showBlameList
815
816 - def __getstate__(self):
817 d = self.__dict__.copy() 818 del d['p'] 819 return d
820
821 - def shutdown(self):
822 self.shuttingDown = True 823 if self.p: 824 self.p.quit("buildmaster reconfigured: bot disconnecting")
825
826 - def buildProtocol(self, address):
827 p = self.protocol(self.nickname, self.password, 828 self.channels, self.status, 829 self.categories, self.notify_events, 830 noticeOnChannel = self.noticeOnChannel, 831 showBlameList = self.showBlameList) 832 p.factory = self 833 p.status = self.status 834 p.control = self.control 835 self.p = p 836 return p
837 838 # TODO: I think a shutdown that occurs while the connection is being 839 # established will make this explode 840
841 - def clientConnectionLost(self, connector, reason):
842 if self.shuttingDown: 843 log.msg("not scheduling reconnection attempt") 844 return 845 ThrottledClientFactory.clientConnectionLost(self, connector, reason)
846
847 - def clientConnectionFailed(self, connector, reason):
848 if self.shuttingDown: 849 log.msg("not scheduling reconnection attempt") 850 return 851 ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
852 853
854 -class IRC(base.StatusReceiverMultiService):
855 implements(IStatusReceiver) 856 """I am an IRC bot which can be queried for status information. I 857 connect to a single IRC server and am known by a single nickname on that 858 server, however I can join multiple channels.""" 859 860 in_test_harness = False 861 862 compare_attrs = ["host", "port", "nick", "password", 863 "channels", "allowForce", "useSSL", 864 "categories"] 865
866 - def __init__(self, host, nick, channels, port=6667, allowForce=True, 867 categories=None, password=None, notify_events={}, 868 noticeOnChannel = False, showBlameList = True, 869 useSSL=False):
870 base.StatusReceiverMultiService.__init__(self) 871 872 assert allowForce in (True, False) # TODO: implement others 873 874 # need to stash these so we can detect changes later 875 self.host = host 876 self.port = port 877 self.nick = nick 878 self.channels = channels 879 self.password = password 880 self.allowForce = allowForce 881 self.categories = categories 882 self.notify_events = notify_events 883 log.msg('Notify events %s' % notify_events) 884 self.f = IrcStatusFactory(self.nick, self.password, 885 self.channels, self.categories, self.notify_events, 886 noticeOnChannel = noticeOnChannel, 887 showBlameList = showBlameList) 888 889 # don't set up an actual ClientContextFactory if we're running tests. 890 if self.in_test_harness: 891 return 892 893 if useSSL: 894 # SSL client needs a ClientContextFactory for some SSL mumbo-jumbo 895 if not have_ssl: 896 raise RuntimeError("useSSL requires PyOpenSSL") 897 cf = ssl.ClientContextFactory() 898 c = internet.SSLClient(self.host, self.port, self.f, cf) 899 else: 900 c = internet.TCPClient(self.host, self.port, self.f) 901 902 c.setServiceParent(self)
903
904 - def setServiceParent(self, parent):
905 base.StatusReceiverMultiService.setServiceParent(self, parent) 906 self.f.status = parent.getStatus() 907 if self.allowForce: 908 self.f.control = interfaces.IControl(parent)
909
910 - def stopService(self):
911 # make sure the factory will stop reconnecting 912 self.f.shutdown() 913 return base.StatusReceiverMultiService.stopService(self)
914 915 916 ## buildbot: list builders 917 # buildbot: watch quick 918 # print notification when current build in 'quick' finishes 919 ## buildbot: status 920 ## buildbot: status full-2.3 921 ## building, not, % complete, ETA 922 ## buildbot: force build full-2.3 "reason" 923