1 # This file is part of Buildbot. Buildbot is free software: you can
2 # redistribute it and/or modify it under the terms of the GNU General Public
3 # License as published by the Free Software Foundation, version 2.
5 # This program is distributed in the hope that it will be useful, but WITHOUT
6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
10 # You should have received a copy of the GNU General Public License along with
11 # this program; if not, write to the Free Software Foundation, Inc., 51
12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
14 # Copyright Buildbot Team Members
16 import re
, shlex
, random
17 from string
import join
, capitalize
, lower
19 from zope
.interface
import implements
20 from twisted
.internet
import protocol
, reactor
21 from twisted
.words
.protocols
import irc
22 from twisted
.python
import usage
, log
23 from twisted
.application
import internet
24 from twisted
.internet
import defer
, task
26 from buildbot
import interfaces
, util
27 from buildbot
import version
28 from buildbot
.interfaces
import IStatusReceiver
29 from buildbot
.sourcestamp
import SourceStamp
30 from buildbot
.status
import base
31 from buildbot
.status
.results
import SUCCESS
, WARNINGS
, FAILURE
, EXCEPTION
, RETRY
32 from buildbot
.process
.properties
import Properties
34 # twisted.internet.ssl requires PyOpenSSL, so be resilient if it's missing
36 from twisted
.internet
import ssl
41 def maybeColorize(text
, color
, useColors
):
62 return "%c%d%s%c" % (3, irc_colors
.index(color
), text
, 3)
66 class UsageError(ValueError):
67 def __init__(self
, string
= "Invalid usage", *more
):
68 ValueError.__init
__(self
, string
, *more
)
70 class ForceOptions(usage
.Options
):
72 ["builder", None, None, "which Builder to start"],
73 ["branch", None, None, "which branch to build"],
74 ["revision", None, None, "which revision to build"],
75 ["reason", None, None, "the reason for starting the build"],
77 "A set of properties made available in the build environment, "
78 "format is --properties=prop1=value1,prop2=value2,.. "
79 "option can be specified multiple times."],
82 def parseArgs(self
, *args
):
85 if self
['builder'] is not None:
86 raise UsageError("--builder provided in two ways")
87 self
['builder'] = args
.pop(0)
89 if self
['reason'] is not None:
90 raise UsageError("--reason provided in two ways")
91 self
['reason'] = " ".join(args
)
94 class IrcBuildRequest
:
98 def __init__(self
, parent
, useRevisions
=False, useColors
=True):
100 self
.useRevisions
= useRevisions
101 self
.useColors
= useColors
102 self
.timer
= reactor
.callLater(5, self
.soon
)
106 if not self
.hasStarted
:
107 self
.parent
.send("The build has been queued, I'll give a shout"
110 def started(self
, s
):
111 self
.hasStarted
= True
116 if self
.useRevisions
:
117 response
= "build containing revision(s) [%s] forced" % s
.getRevisions()
119 response
= "build #%d forced" % s
.getNumber()
121 response
= "build forced [ETA %s]" % self
.parent
.convertTime(eta
)
122 self
.parent
.send(response
)
123 self
.parent
.send("I will politely inform you when the build finishes")
124 d
= s
.waitUntilFinished()
125 d
.addCallback(self
.parent
.watchedBuildFinished
)
127 class IRCContact(base
.StatusReceiver
):
128 implements(IStatusReceiver
)
129 """I hold the state for a single user's interaction with the buildbot.
131 There will be one instance of me for each user who interacts personally
132 with the buildbot. There will be an additional instance for each
133 'broadcast contact' (chat rooms, IRC channels as a whole).
136 def __init__(self
, bot
, dest
):
138 self
.master
= bot
.master
139 self
.notify_events
= {}
142 self
.useRevisions
= bot
.useRevisions
143 self
.useColors
= bot
.useColors
144 self
.reported_builds
= [] # tuples (when, buildername, buildnum)
145 self
.add_notification_events(bot
.notify_events
)
147 # when people send us public messages ("buildbot: command"),
148 # self.dest is the name of the channel ("#twisted"). When they send
149 # us private messages (/msg buildbot command), self.dest is their
156 "What happen ?": [ "Somebody set up us the bomb." ],
157 "It's You !!": ["How are you gentlemen !!",
158 "All your base are belong to us.",
159 "You are on the way to destruction."],
160 "What you say !!": ["You have no chance to survive make your time.",
164 def doSilly(self
, message
):
165 response
= self
.silly
[message
]
168 reactor
.callLater(when
, self
.send
, r
)
171 def getBuilder(self
, which
):
173 b
= self
.bot
.status
.getBuilder(which
)
175 raise UsageError
, "no such builder '%s'" % which
178 def getControl(self
, which
):
179 if not self
.bot
.control
:
180 raise UsageError("builder control is not enabled")
182 bc
= self
.bot
.control
.getBuilder(which
)
184 raise UsageError("no such builder '%s'" % which
)
187 def getAllBuilders(self
):
189 @rtype: list of L{buildbot.process.builder.Builder}
191 names
= self
.bot
.status
.getBuilderNames(categories
=self
.bot
.categories
)
193 builders
= [self
.bot
.status
.getBuilder(n
) for n
in names
]
196 def convertTime(self
, seconds
):
198 return "%d seconds" % seconds
199 minutes
= int(seconds
/ 60)
200 seconds
= seconds
- 60*minutes
202 return "%dm%02ds" % (minutes
, seconds
)
203 hours
= int(minutes
/ 60)
204 minutes
= minutes
- 60*hours
205 return "%dh%02dm%02ds" % (hours
, minutes
, seconds
)
207 def reportBuild(self
, builder
, buildnum
):
208 """Returns True if this build should be reported for this contact
209 (eliminating duplicates), and also records the report for later"""
210 for w
, b
, n
in self
.reported_builds
:
211 if b
== builder
and n
== buildnum
:
213 self
.reported_builds
.append([util
.now(), builder
, buildnum
])
215 # clean the reported builds
216 horizon
= util
.now() - 60
217 while self
.reported_builds
and self
.reported_builds
[0][0] < horizon
:
218 self
.reported_builds
.pop(0)
220 # and return True, since this is a new one
223 def command_HELLO(self
, args
, who
):
226 def command_VERSION(self
, args
, who
):
227 self
.send("buildbot-%s at your service" % version
)
229 def command_LIST(self
, args
, who
):
230 args
= shlex
.split(args
)
232 raise UsageError
, "try 'list builders'"
233 if args
[0] == 'builders':
234 builders
= self
.getAllBuilders()
235 str = "Configured builders: "
238 state
= b
.getState()[0]
239 if state
== 'offline':
245 command_LIST
.usage
= "list builders - List configured builders"
247 def command_STATUS(self
, args
, who
):
248 args
= shlex
.split(args
)
254 raise UsageError
, "try 'status <builder>'"
256 builders
= self
.getAllBuilders()
258 self
.emit_status(b
.name
)
260 self
.emit_status(which
)
261 command_STATUS
.usage
= "status [<which>] - List status of a builder (or all builders)"
263 def validate_notification_event(self
, event
):
264 if not re
.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event
):
265 raise UsageError("try 'notify on|off <EVENT>'")
267 def list_notified_events(self
):
268 self
.send( "The following events are being notified: %r" % self
.notify_events
.keys() )
270 def notify_for(self
, *events
):
272 if self
.notify_events
.has_key(event
):
276 def subscribe_to_build_events(self
):
277 self
.bot
.status
.subscribe(self
)
280 def unsubscribe_from_build_events(self
):
281 self
.bot
.status
.unsubscribe(self
)
284 def add_notification_events(self
, events
):
286 self
.validate_notification_event(event
)
287 self
.notify_events
[event
] = 1
289 if not self
.subscribed
:
290 self
.subscribe_to_build_events()
292 def remove_notification_events(self
, events
):
294 self
.validate_notification_event(event
)
295 del self
.notify_events
[event
]
297 if len(self
.notify_events
) == 0 and self
.subscribed
:
298 self
.unsubscribe_from_build_events()
300 def remove_all_notification_events(self
):
301 self
.notify_events
= {}
304 self
.unsubscribe_from_build_events()
306 def command_NOTIFY(self
, args
, who
):
307 args
= shlex
.split(args
)
310 raise UsageError("try 'notify on|off|list <EVENT>'")
315 if not events
: events
= ('started','finished')
316 self
.add_notification_events(events
)
318 self
.list_notified_events()
320 elif action
== "off":
322 self
.remove_notification_events(events
)
324 self
.remove_all_notification_events()
326 self
.list_notified_events()
328 elif action
== "list":
329 self
.list_notified_events()
333 raise UsageError("try 'notify on|off <EVENT>'")
335 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)"
337 def command_WATCH(self
, args
, who
):
338 args
= shlex
.split(args
)
340 raise UsageError("try 'watch <builder>'")
342 b
= self
.getBuilder(which
)
343 builds
= b
.getCurrentBuilds()
345 self
.send("there are no builds currently running")
348 assert not build
.isFinished()
349 d
= build
.waitUntilFinished()
350 d
.addCallback(self
.watchedBuildFinished
)
351 if self
.useRevisions
:
352 r
= "watching build %s containing revision(s) [%s] until it finishes" \
353 % (which
, build
.getRevisions())
355 r
= "watching build %s #%d until it finishes" \
356 % (which
, build
.getNumber())
359 r
+= " [%s]" % self
.convertTime(eta
)
362 command_WATCH
.usage
= "watch <which> - announce the completion of an active build"
364 def builderAdded(self
, builderName
, builder
):
365 if (self
.bot
.categories
!= None and
366 builder
.category
not in self
.bot
.categories
):
369 log
.msg('[Contact] Builder %s added' % (builder
))
370 builder
.subscribe(self
)
372 def builderRemoved(self
, builderName
):
373 log
.msg('[Contact] Builder %s removed' % (builderName
))
375 def buildStarted(self
, builderName
, build
):
376 builder
= build
.getBuilder()
377 log
.msg('[Contact] Builder %r in category %s started' % (builder
, builder
.category
))
379 # only notify about builders we are interested in
381 if (self
.bot
.categories
!= None and
382 builder
.category
not in self
.bot
.categories
):
383 log
.msg('Not notifying for a build in the wrong category')
386 if not self
.notify_for('started'):
389 if self
.useRevisions
:
390 r
= "build containing revision(s) [%s] on %s started" % \
391 (build
.getRevisions(), builder
.getName())
393 r
= "build #%d of %s started, including [%s]" % \
396 ", ".join([str(c
.revision
) for c
in build
.getChanges()])
401 results_descriptions
= {
402 SUCCESS
: ("Success", 'GREEN'),
403 WARNINGS
: ("Warnings", 'YELLOW'),
404 FAILURE
: ("Failure", 'RED'),
405 EXCEPTION
: ("Exception", 'PURPLE'),
406 RETRY
: ("Retry", 'AQUA_LIGHT'),
409 def getResultsDescriptionAndColor(self
, results
):
410 return self
.results_descriptions
.get(results
, ("??",'RED'))
412 def buildFinished(self
, builderName
, build
, results
):
413 builder
= build
.getBuilder()
415 if (self
.bot
.categories
!= None and
416 builder
.category
not in self
.bot
.categories
):
419 if not self
.notify_for_finished(build
):
422 builder_name
= builder
.getName()
423 buildnum
= build
.getNumber()
424 buildrevs
= build
.getRevisions()
426 results
= self
.getResultsDescriptionAndColor(build
.getResults())
427 if self
.reportBuild(builder_name
, buildnum
):
428 if self
.useRevisions
:
429 r
= "build containing revision(s) [%s] on %s is complete: %s" % \
430 (buildrevs
, builder_name
, results
[0])
432 r
= "build #%d of %s is complete: %s" % \
433 (buildnum
, builder_name
, results
[0])
435 r
+= ' [%s]' % maybeColorize(" ".join(build
.getText()), results
[1], self
.useColors
)
436 buildurl
= self
.bot
.status
.getURLForThing(build
)
438 r
+= " Build details are at %s" % buildurl
440 if self
.bot
.showBlameList
and build
.getResults() != SUCCESS
and len(build
.changes
) != 0:
441 r
+= ' blamelist: ' + ', '.join(list(set([c
.who
for c
in build
.changes
])))
445 def notify_for_finished(self
, build
):
446 results
= build
.getResults()
448 if self
.notify_for('finished'):
451 if self
.notify_for(lower(self
.results_descriptions
.get(results
)[0])):
454 prevBuild
= build
.getPreviousBuild()
456 prevResult
= prevBuild
.getResults()
458 required_notification_control_string
= join((lower(self
.results_descriptions
.get(prevResult
)[0]), \
460 capitalize(self
.results_descriptions
.get(results
)[0])), \
463 if (self
.notify_for(required_notification_control_string
)):
468 def watchedBuildFinished(self
, b
):
470 # only notify about builders we are interested in
471 builder
= b
.getBuilder()
472 if (self
.bot
.categories
!= None and
473 builder
.category
not in self
.bot
.categories
):
476 builder_name
= builder
.getName()
477 buildnum
= b
.getNumber()
478 buildrevs
= b
.getRevisions()
480 results
= self
.getResultsDescriptionAndColor(b
.getResults())
481 if self
.reportBuild(builder_name
, buildnum
):
482 if self
.useRevisions
:
483 r
= "Hey! build %s containing revision(s) [%s] is complete: %s" % \
484 (builder_name
, buildrevs
, results
[0])
486 r
= "Hey! build %s #%d is complete: %s" % \
487 (builder_name
, buildnum
, results
[0])
489 r
+= ' [%s]' % maybeColorize(" ".join(b
.getText()), results
[1], self
.useColors
)
491 buildurl
= self
.bot
.status
.getURLForThing(b
)
493 self
.send("Build details are at %s" % buildurl
)
495 def command_FORCE(self
, args
, who
):
496 errReply
= "try 'force build [--branch=BRANCH] [--revision=REVISION] [--props=PROP1=VAL1,PROP2=VAL2...] <WHICH> <REASON>'"
497 args
= shlex
.split(args
)
499 raise UsageError(errReply
)
502 raise UsageError(errReply
)
503 opts
= ForceOptions()
504 opts
.parseOptions(args
)
506 which
= opts
['builder']
507 branch
= opts
['branch']
508 revision
= opts
['revision']
509 reason
= opts
['reason']
510 props
= opts
['props']
513 raise UsageError("you must provide a Builder, " + errReply
)
515 # keep weird stuff out of the branch, revision, and properties args.
516 branch_validate
= self
.master
.config
.validation
['branch']
517 revision_validate
= self
.master
.config
.validation
['revision']
518 pname_validate
= self
.master
.config
.validation
['property_name']
519 pval_validate
= self
.master
.config
.validation
['property_value']
520 if branch
and not branch_validate
.match(branch
):
521 log
.msg("bad branch '%s'" % branch
)
522 self
.send("sorry, bad branch '%s'" % branch
)
524 if revision
and not revision_validate
.match(revision
):
525 log
.msg("bad revision '%s'" % revision
)
526 self
.send("sorry, bad revision '%s'" % revision
)
529 properties
= Properties()
531 # split props into name:value dict
533 propertylist
= props
.split(",")
534 for i
in range(0,len(propertylist
)):
535 splitproperty
= propertylist
[i
].split("=", 1)
536 pdict
[splitproperty
[0]] = splitproperty
[1]
542 if not pname_validate
.match(pname
) \
543 or not pval_validate
.match(pvalue
):
544 log
.msg("bad property name='%s', value='%s'" % (pname
, pvalue
))
545 self
.send("sorry, bad property name='%s', value='%s'" %
548 properties
.setProperty(pname
, pvalue
, "Force Build IRC")
550 bc
= self
.getControl(which
)
552 reason
= "forced: by %s: %s" % (self
.describeUser(who
), reason
)
553 ss
= SourceStamp(branch
=branch
, revision
=revision
)
554 d
= bc
.submitBuildRequest(ss
, reason
, props
=properties
.asDict())
555 def subscribe(buildreq
):
556 ireq
= IrcBuildRequest(self
, self
.useRevisions
)
557 buildreq
.subscribe(ireq
.started
)
558 d
.addCallback(subscribe
)
559 d
.addErrback(log
.err
, "while forcing a build")
562 command_FORCE
.usage
= "force build [--branch=branch] [--revision=revision] [--props=prop1=val1,prop2=val2...] <which> <reason> - Force a build"
564 def command_STOP(self
, args
, who
):
565 args
= shlex
.split(args
)
566 if len(args
) < 3 or args
[0] != 'build':
567 raise UsageError
, "try 'stop build WHICH <REASON>'"
571 buildercontrol
= self
.getControl(which
)
573 r
= "stopped: by %s: %s" % (self
.describeUser(who
), reason
)
575 # find an in-progress build
576 builderstatus
= self
.getBuilder(which
)
577 builds
= builderstatus
.getCurrentBuilds()
579 self
.send("sorry, no build is currently running")
582 num
= build
.getNumber()
583 revs
= build
.getRevisions()
585 # obtain the BuildControl object
586 buildcontrol
= buildercontrol
.getBuild(num
)
589 buildcontrol
.stopBuild(r
)
591 if self
.useRevisions
:
592 response
= "build containing revision(s) [%s] interrupted" % revs
594 response
= "build %d interrupted" % num
597 command_STOP
.usage
= "stop build <which> <reason> - Stop a running build"
599 def emit_status(self
, which
):
600 b
= self
.getBuilder(which
)
602 state
, builds
= b
.getState()
605 last
= b
.getLastFinishedBuild()
607 start
,finished
= last
.getTimes()
608 str += ", last build %s ago: %s" % \
609 (self
.convertTime(int(util
.now() - finished
)), " ".join(last
.getText()))
610 if state
== "building":
613 step
= build
.getCurrentStep()
615 s
= "(%s)" % " ".join(step
.getText())
617 s
= "(no current step)"
620 s
+= " [ETA %s]" % self
.convertTime(ETA
)
625 def command_LAST(self
, args
, who
):
626 args
= shlex
.split(args
)
633 raise UsageError
, "try 'last <builder>'"
635 def emit_last(which
):
636 last
= self
.getBuilder(which
).getLastFinishedBuild()
638 str = "(no builds run since last restart)"
640 start
,finish
= last
.getTimes()
641 str = "%s ago: " % (self
.convertTime(int(util
.now() - finish
)))
642 str += " ".join(last
.getText())
643 self
.send("last build [%s]: %s" % (which
, str))
646 builders
= self
.getAllBuilders()
651 command_LAST
.usage
= "last <which> - list last build status for builder <which>"
653 def build_commands(self
):
656 if k
.startswith('command_'):
657 commands
.append(k
[8:].lower())
661 def describeUser(self
, user
):
662 if self
.dest
[0] == '#':
663 return "IRC user <%s> on channel %s" % (user
, self
.dest
)
664 return "IRC user <%s> (privmsg)" % user
668 def command_MUTE(self
, args
, who
):
669 # The order of these is important! ;)
670 self
.send("Shutting up for now.")
672 command_MUTE
.usage
= "mute - suppress all messages until a corresponding 'unmute' is issued"
674 def command_UNMUTE(self
, args
, who
):
676 # The order of these is important! ;)
678 self
.send("I'm baaaaaaaaaaack!")
680 self
.send("You hadn't told me to be quiet, but it's the thought that counts, right?")
681 command_UNMUTE
.usage
= "unmute - disable a previous 'mute'"
683 def command_HELP(self
, args
, who
):
684 args
= shlex
.split(args
)
686 self
.send("Get help on what? (try 'help <foo>', "
687 "or 'commands' for a command list)")
690 meth
= self
.getCommandMethod(command
)
692 raise UsageError
, "no such command '%s'" % command
693 usage
= getattr(meth
, 'usage', None)
695 self
.send("Usage: %s" % usage
)
697 self
.send("No usage info for '%s'" % command
)
698 command_HELP
.usage
= "help <command> - Give help for <command>"
700 def command_SOURCE(self
, args
, who
):
701 self
.send("My source can be found at "
702 "https://github.com/buildbot/buildbot")
703 command_SOURCE
.usage
= "source - the source code for Buildbot"
705 def command_COMMANDS(self
, args
, who
):
706 commands
= self
.build_commands()
707 str = "buildbot commands: " + ", ".join(commands
)
709 command_COMMANDS
.usage
= "commands - List available commands"
711 def command_DESTROY(self
, args
, who
):
712 self
.act("readies phasers")
714 def command_DANCE(self
, args
, who
):
716 reactor
.callLater(1.0, self
.send
, "<(^.^<)")
717 reactor
.callLater(2.0, self
.send
, "<(^.^)>")
718 reactor
.callLater(3.0, self
.send
, "(>^.^)>")
719 reactor
.callLater(3.5, self
.send
, "(7^.^)7")
720 reactor
.callLater(5.0, self
.send
, "(>^.^<)")
722 # communication with the user
724 def send(self
, message
):
726 self
.bot
.msgOrNotice(self
.dest
, message
.encode("ascii", "replace"))
728 def act(self
, action
):
730 self
.bot
.describe(self
.dest
, action
.encode("ascii", "replace"))
732 # main dispatchers for incoming messages
734 def getCommandMethod(self
, command
):
735 return getattr(self
, 'command_' + command
.upper(), None)
737 def handleMessage(self
, message
, who
):
738 # a message has arrived from 'who'. For broadcast contacts (i.e. when
739 # people do an irc 'buildbot: command'), this will be a string
740 # describing the sender of the message in some useful-to-log way, and
741 # a single Contact may see messages from a variety of users. For
742 # unicast contacts (i.e. when people do an irc '/msg buildbot
743 # command'), a single Contact will only ever see messages from a
745 message
= message
.lstrip()
746 #if self.silly.has_key(message):
747 # self.doSilly(message)
748 # return defer.succeed(None)
750 parts
= message
.split(' ', 1)
754 log
.msg("irc command", cmd
)
756 meth
= self
.getCommandMethod(cmd
)
757 if not meth
and message
[-1] == '!':
758 self
.send("What you say!")
759 return defer
.succeed(None)
762 d
= defer
.maybeDeferred(meth
, args
.strip(), who
)
766 self
.send(str(f
.value
))
770 self
.send("Something bad happened (see logs)")
771 d
.addErrback(log
.err
)
773 return defer
.succeed(None)
775 def handleAction(self
, data
, user
):
777 # this is sent when somebody performs an action that mentions the
778 # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of
779 # the person who performed the action, so if their action provokes a
780 # response, they can be named. This is 100% silly.
781 if not data
.endswith("s "+ self
.bot
.nickname
):
786 response
= "%s back" % verb
788 response
= "%s %s too" % (verb
, user
)
792 class IrcStatusBot(irc
.IRCClient
):
793 """I represent the buildbot to an IRC server.
795 contactClass
= IRCContact
797 def __init__(self
, nickname
, password
, channels
, pm_to_nicks
, status
,
798 categories
, notify_events
, noticeOnChannel
=False,
799 useRevisions
=False, showBlameList
=False, useColors
=True):
800 self
.nickname
= nickname
801 self
.channels
= channels
802 self
.pm_to_nicks
= pm_to_nicks
803 self
.password
= password
805 self
.master
= status
.master
806 self
.categories
= categories
807 self
.notify_events
= notify_events
810 self
.noticeOnChannel
= noticeOnChannel
811 self
.useColors
= useColors
812 self
.useRevisions
= useRevisions
813 self
.showBlameList
= showBlameList
814 self
._keepAliveCall
= task
.LoopingCall(lambda: self
.ping(self
.nickname
))
816 def connectionMade(self
):
817 irc
.IRCClient
.connectionMade(self
)
818 self
._keepAliveCall
.start(60)
820 def connectionLost(self
, reason
):
821 if self
._keepAliveCall
.running
:
822 self
._keepAliveCall
.stop()
823 irc
.IRCClient
.connectionLost(self
, reason
)
825 def msgOrNotice(self
, dest
, message
):
826 if self
.noticeOnChannel
and dest
[0] == '#':
827 self
.notice(dest
, message
)
829 self
.msg(dest
, message
)
831 def getContact(self
, name
):
832 name
= name
.lower() # nicknames and channel names are case insensitive
833 if name
in self
.contacts
:
834 return self
.contacts
[name
]
835 new_contact
= self
.contactClass(self
, name
)
836 self
.contacts
[name
] = new_contact
840 log
.msg("%s: %s" % (self
, msg
))
843 # the following irc.IRCClient methods are called when we have input
845 def privmsg(self
, user
, channel
, message
):
846 user
= user
.split('!', 1)[0] # rest is ~user@hostname
847 # channel is '#twisted' or 'buildbot' (for private messages)
848 if channel
== self
.nickname
:
850 contact
= self
.getContact(user
)
851 contact
.handleMessage(message
, user
)
853 # else it's a broadcast message, maybe for us, maybe not. 'channel'
854 # is '#twisted' or the like.
855 contact
= self
.getContact(channel
)
856 if message
.startswith("%s:" % self
.nickname
) or message
.startswith("%s," % self
.nickname
):
857 message
= message
[len("%s:" % self
.nickname
):]
858 contact
.handleMessage(message
, user
)
860 def action(self
, user
, channel
, data
):
861 user
= user
.split('!', 1)[0] # rest is ~user@hostname
862 # somebody did an action (/me actions) in the broadcast channel
863 contact
= self
.getContact(channel
)
864 if self
.nickname
in data
:
865 contact
.handleAction(data
, user
)
869 self
.msg("Nickserv", "IDENTIFY " + self
.password
)
870 for c
in self
.channels
:
871 if isinstance(c
, dict):
872 channel
= c
.get('channel', None)
873 password
= c
.get('password', None)
877 self
.join(channel
=channel
, key
=password
)
878 for c
in self
.pm_to_nicks
:
881 def joined(self
, channel
):
882 self
.log("I have joined %s" % (channel
,))
883 # trigger contact constructor, which in turn subscribes to notify events
884 self
.getContact(channel
)
886 def left(self
, channel
):
887 self
.log("I have left %s" % (channel
,))
889 def kickedFrom(self
, channel
, kicker
, message
):
890 self
.log("I have been kicked from %s by %s: %s" % (channel
,
895 class ThrottledClientFactory(protocol
.ClientFactory
):
896 lostDelay
= random
.randint(1, 5)
897 failedDelay
= random
.randint(45, 60)
899 def __init__(self
, lostDelay
=None, failedDelay
=None):
900 if lostDelay
is not None:
901 self
.lostDelay
= lostDelay
902 if failedDelay
is not None:
903 self
.failedDelay
= failedDelay
905 def clientConnectionLost(self
, connector
, reason
):
906 reactor
.callLater(self
.lostDelay
, connector
.connect
)
908 def clientConnectionFailed(self
, connector
, reason
):
909 reactor
.callLater(self
.failedDelay
, connector
.connect
)
912 class IrcStatusFactory(ThrottledClientFactory
):
913 protocol
= IrcStatusBot
920 def __init__(self
, nickname
, password
, channels
, pm_to_nicks
, categories
, notify_events
,
921 noticeOnChannel
=False, useRevisions
=False, showBlameList
=False,
922 lostDelay
=None, failedDelay
=None, useColors
=True):
923 ThrottledClientFactory
.__init
__(self
, lostDelay
=lostDelay
,
924 failedDelay
=failedDelay
)
926 self
.nickname
= nickname
927 self
.password
= password
928 self
.channels
= channels
929 self
.pm_to_nicks
= pm_to_nicks
930 self
.categories
= categories
931 self
.notify_events
= notify_events
932 self
.noticeOnChannel
= noticeOnChannel
933 self
.useRevisions
= useRevisions
934 self
.showBlameList
= showBlameList
935 self
.useColors
= useColors
937 def __getstate__(self
):
938 d
= self
.__dict
__.copy()
943 self
.shuttingDown
= True
945 self
.p
.quit("buildmaster reconfigured: bot disconnecting")
947 def buildProtocol(self
, address
):
948 p
= self
.protocol(self
.nickname
, self
.password
,
949 self
.channels
, self
.pm_to_nicks
, self
.status
,
950 self
.categories
, self
.notify_events
,
951 noticeOnChannel
= self
.noticeOnChannel
,
952 useColors
= self
.useColors
,
953 useRevisions
= self
.useRevisions
,
954 showBlameList
= self
.showBlameList
)
956 p
.status
= self
.status
957 p
.control
= self
.control
961 # TODO: I think a shutdown that occurs while the connection is being
962 # established will make this explode
964 def clientConnectionLost(self
, connector
, reason
):
965 if self
.shuttingDown
:
966 log
.msg("not scheduling reconnection attempt")
968 ThrottledClientFactory
.clientConnectionLost(self
, connector
, reason
)
970 def clientConnectionFailed(self
, connector
, reason
):
971 if self
.shuttingDown
:
972 log
.msg("not scheduling reconnection attempt")
974 ThrottledClientFactory
.clientConnectionFailed(self
, connector
, reason
)
977 class IRC(base
.StatusReceiverMultiService
):
978 implements(IStatusReceiver
)
980 in_test_harness
= False
982 compare_attrs
= ["host", "port", "nick", "password",
983 "channels", "pm_to_nicks", "allowForce", "useSSL",
984 "useRevisions", "categories", "useColors",
985 "lostDelay", "failedDelay"]
987 def __init__(self
, host
, nick
, channels
, pm_to_nicks
=[], port
=6667,
988 allowForce
=False, categories
=None, password
=None, notify_events
={},
989 noticeOnChannel
= False, showBlameList
= True, useRevisions
=False,
990 useSSL
=False, lostDelay
=None, failedDelay
=None, useColors
=True):
991 base
.StatusReceiverMultiService
.__init
__(self
)
993 assert allowForce
in (True, False) # TODO: implement others
995 # need to stash these so we can detect changes later
999 self
.channels
= channels
1000 self
.pm_to_nicks
= pm_to_nicks
1001 self
.password
= password
1002 self
.allowForce
= allowForce
1003 self
.useRevisions
= useRevisions
1004 self
.categories
= categories
1005 self
.notify_events
= notify_events
1007 self
.f
= IrcStatusFactory(self
.nick
, self
.password
,
1008 self
.channels
, self
.pm_to_nicks
,
1009 self
.categories
, self
.notify_events
,
1010 noticeOnChannel
= noticeOnChannel
,
1011 useRevisions
= useRevisions
,
1012 showBlameList
= showBlameList
,
1013 lostDelay
= lostDelay
,
1014 failedDelay
= failedDelay
,
1015 useColors
= useColors
)
1018 # SSL client needs a ClientContextFactory for some SSL mumbo-jumbo
1020 raise RuntimeError("useSSL requires PyOpenSSL")
1021 cf
= ssl
.ClientContextFactory()
1022 c
= internet
.SSLClient(self
.host
, self
.port
, self
.f
, cf
)
1024 c
= internet
.TCPClient(self
.host
, self
.port
, self
.f
)
1026 c
.setServiceParent(self
)
1028 def setServiceParent(self
, parent
):
1029 base
.StatusReceiverMultiService
.setServiceParent(self
, parent
)
1030 self
.f
.status
= parent
1032 self
.f
.control
= interfaces
.IControl(self
.master
)
1034 def stopService(self
):
1035 # make sure the factory will stop reconnecting
1037 return base
.StatusReceiverMultiService
.stopService(self
)