diff --git a/plugins/Herald.py b/plugins/Herald.py new file mode 100644 index 000000000..1c809bc46 --- /dev/null +++ b/plugins/Herald.py @@ -0,0 +1,189 @@ +#!/usr/bin/python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Greets users who join the channel with a recognized hostmask with a nice +little greeting. Otherwise, can greet +""" + +import plugins + +import os +import time + +import log +import conf +import utils +import ircdb +import ircmsgs +import ircutils +import privmsgs +import callbacks +import configurable + + +def configure(onStart, afterConnect, advanced): + # This will be called by setup.py to configure this module. onStart and + # afterConnect are both lists. Append to onStart the commands you would + # like to be run when the bot is started; append to afterConnect the + # commands you would like to be run when the bot has finished connecting. + from questions import expect, anything, something, yn + onStart.append('load Herald') + + +class HeraldDB(object): + def __init__(self): + self.heralds = {} + self.open() + + def open(self): + fd = file(os.path.join(conf.dataDir, 'Herald.db')) + for line in fd: + line = line.rstrip() + try: + (idChannel, msg) = line.split(':', 1) + (id, channel) = idChannel.split(',', 1) + id = int(id) + except ValueError: + log.warning('Invalid line in HeraldDB: %r', line) + continue + self.heralds[id] = msg + fd.close() + + def close(self): + fd = file(os.path.join(conf.dataDir, 'Herald.db'), 'w') + L = self.heralds.items() + L.sort() + for (id, msg) in L: + fd.write('%s:%s%s' % (id, msg, os.linesep)) + fd.close() + + def getHerald(id, channel): + return self.heralds[(id, channel)] + + def setHerald(id, channel, msg): + self.heralds[(id, channel)] = msg + + def delHerald(id, channel): + del self.heralds[(id, channel)] + + +class Herald(callbacks.Privmsg, configurable.Mixin): + configurables = configurable.Dictionary( + [('heralding', configurable.BoolType, True, + """Determines whether messages will be sent to the channel when + a recognized user joins; basically enables or disables the + plugin."""), + ('throttle-time', configurable.PositiveIntType, 600, + """Determines the minimum number of seconds between heralds."""), + ('throttle-after-part', configurable.IntType, 60, + """Determines the minimum number of seconds after parting that the + bot will not herald the person when he or she rejoins."""),] + ) + def __init__(self): + self.db = HeraldDB() + self.lastParts = {} + self.lastHerald = {} + + def die(self): + self.db.close() + callbacks.Privmsg.die() + configurable.Mixin.die() + + def doJoin(self, irc, msg): + channel = msg.args[0] + if self.configurables.get('heralding', channel): + try: + id = ircdb.users.getUserId(msg.prefix) + herald = self.db.getHerald(id, channel) + except KeyError: + return + now = time.time() + throttle = self.configurables.get('throttle-time', channel) + if now - self.lastHeralds[(id, channel)] > throttle: + if (id, channel) in self.lastParts: + i = self.configurables.get('throttle-after-part', channel) + if now - self.lastParts[(id, channel)] < i: + return + irc.queueMsg(ircmsgs.privmsg(channel, herald)) + + def doPart(self, irc, msg): + self.lastParts[(msg.prefix, msg.args[0])] = time.time() + + def _getId(self, userNickHostmask): + try: + id = ircdb.users.getUserId(userNickHostmask) + except KeyError: + if not ircutils.isUserHostmask(userNickHostmask): + hostmask = irc.state.nickToHostmask(userNickHostmask) + id = ircdb.users.getUserId(hostmask) + else: + raise KeyError + + def add(self, irc, msg, args): + """[] + + Sets the herald message for (or the user is + currently identified or recognized as) to . is only + necessary if the message isn't sent in the channel itself. + """ + channel = privmsgs.getChannel(msg, args) + (userNickHostmask, herald) = privmsgs.getArgs(args, required=2) + try: + id = self._getId(userNickHostmask) + except KeyError: + irc.error(msg, conf.replyNoUser) + return + self.db.setHerald(id, channel, herald) + irc.reply(msg, conf.replySuccess) + + def remove(self, irc, msg, args): + """[] + + Removes the herald message set for , or the user + is currently identified or recognized as. + is only necessary if the message isn't sent in the channel + itself. + """ + channel = privmsgs.getChannel(msg, args) + userNickHostmask = privmsgs.getArgs(args) + try: + id = self._getId(userNickHostmask) + except KeyError: + irc.error(msg, conf.replyNoUser) + return + self.db.delHerald(id, channel) + irc.reply(msg, conf.replySuccess) + + +Class = Herald + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Scheduler.py b/plugins/Scheduler.py new file mode 100644 index 000000000..6ca3d6543 --- /dev/null +++ b/plugins/Scheduler.py @@ -0,0 +1,156 @@ +#!/usr/bin/python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Gives the user the ability to schedule commands to run at a particular time, +or repeatedly run at a particular interval. +""" + +import plugins + +import time + +import conf +import utils +import privmsgs +import schedule +import callbacks + + +def configure(onStart, afterConnect, advanced): + # This will be called by setup.py to configure this module. onStart and + # afterConnect are both lists. Append to onStart the commands you would + # like to be run when the bot is started; append to afterConnect the + # commands you would like to be run when the bot has finished connecting. + from questions import expect, anything, something, yn + onStart.append('load Scheduler') + + +class Scheduler(callbacks.Privmsg): + pass + + def seconds(self, irc, msg, args): + """[d] [h] [m] [s] + + Returns the number of seconds in the number of , , + , and given. An example usage is + "seconds 2h 30m", which would return 9000, which is 3600*2 + 30*60. + Useful for scheduling events at a given number of seconds in the + future. + """ + if not args: + raise callbacks.ArgumentError + seconds = 0 + for arg in args: + if not arg or arg[-1] not in 'dhms': + raise callbacks.ArgumentError + (s, kind) = arg[:-1], arg[-1] + try: + i = int(s) + except ValueError: + irc.error(msg, 'Invalid argument: %s' % arg) + return + if kind == 'd': + seconds += i*86400 + elif kind == 'h': + seconds += i*3600 + elif kind == 'm': + seconds += i*60 + elif kind == 's': + seconds += i + irc.reply(msg, str(seconds)) + + def _makeCommandFunction(self, irc, msg, command): + """Makes a function suitable for scheduling from command.""" + tokens = callbacks.tokenize(command) + Owner = irc.getCallback('Owner') + ambiguous = Owner.disambiguate(irc, tokens) + if ambiguous: + raise callbacks.Error, callbacks.ambiguousReply(ambiguous) + def f(): + self.Proxy(irc.irc, msg, tokens) + return f + + def add(self, irc, msg, args): + """ + + Schedules the command string to run seconds in the + future. For example, 'schedule add [seconds 30m] "echo [cpu]"' will + schedule the command "cpu" to be sent to the channel the schedule add + command was given in (with no prefixed nick, a consequence of using + echo). + """ + (seconds, command) = privmsgs.getArgs(args, required=2) + try: + seconds = int(seconds) + except ValueError: + irc.error(msg, 'Invalid seconds value: %r' % seconds) + return + f = self._makeCommandFunction(irc, msg, command) + id = schedule.addEvent(f, time.time() + seconds) + irc.reply(msg, '%s Event #%s added.' % (conf.replySuccess, id)) + + def remove(self, irc, msg, args): + """ + + Removes the event scheduled with id from the schedule. + """ + id = privmsgs.getArgs(args) + try: + id = int(id) + except ValueError: + irc.error(msg, 'Invalid event id: %r' % id) + return + schedule.removeEvent(id) + irc.reply(msg, conf.replySuccess) + + def repeat(self, irc, msg, args): + """ + + Schedules the command to run every seconds, + starting now (i.e., the command runs now, and every seconds + thereafter). is a name by which the command can be + unscheduled. + """ + (name, seconds, command) = privmsgs.getArgs(args, required=3) + try: + seconds = int(seconds) + except ValueError: + irc.error(msg, 'Invalid seconds: %r' % seconds) + return + f = self._makeCommandFunction(irc, msg, command) + id = schedule.addPeriodicEvent(f, seconds, name) + irc.reply(msg, conf.replySuccess) + + +Class = Scheduler + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/test/test_Scheduler.py b/test/test_Scheduler.py new file mode 100644 index 000000000..1e209d54c --- /dev/null +++ b/test/test_Scheduler.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +from testsupport import * + +class MiscTestCase(ChannelPluginTestCase, PluginDocumentation): + plugins = ('Scheduler', 'Utilities') + def testSeconds(self): + self.assertResponse('seconds 1s', '1') + self.assertResponse('seconds 10s', '10') + self.assertResponse('seconds 1m', '60') + self.assertResponse('seconds 1m 1s', '61') + self.assertResponse('seconds 1h', '3600') + self.assertResponse('seconds 1h 1s', '3601') + self.assertResponse('seconds 1d', '86400') + self.assertResponse('seconds 1d 1s', '86401') + self.assertResponse('seconds 2s', '2') + self.assertResponse('seconds 2m', '120') + self.assertResponse('seconds 2d 2h 2m 2s', '180122') + self.assertResponse('seconds 1s', '1') + + def testAddRemove(self): + self.assertNotError('scheduler add [seconds 5s] echo foo bar baz') + self.assertNoResponse(' ', 4) + self.assertResponse(' ', 'foo bar baz') + m = self.assertNotError('scheduler add 5 echo xyzzy') + # Get id. + id = None + for s in m.args[1].split(): + s = s.lstrip('#') + if s.isdigit(): + id = s + break + self.failUnless(id, 'Couldn\'t find id in reply.') + self.assertNotError('scheduler remove %s' % id) + self.assertNoResponse(' ', 5) + + def testRepeat(self): + self.assertNotError('scheduler repeat repeater 5 echo foo bar baz') + self.assertNotError(' ') # replySuccess + self.assertNoResponse(' ', 4) + self.assertResponse(' ', 'foo bar baz') + self.assertNoResponse(' ', 4) + self.assertResponse(' ', 'foo bar baz') + self.assertNotError('scheduler remove repeater') + self.assertNoResponse(' ', 5) + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: +