diff --git a/plugins/Seen/README.txt b/plugins/Seen/README.txt new file mode 100644 index 000000000..d60b47a97 --- /dev/null +++ b/plugins/Seen/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/plugins/Seen/__init__.py b/plugins/Seen/__init__.py new file mode 100644 index 000000000..57c814d11 --- /dev/null +++ b/plugins/Seen/__init__.py @@ -0,0 +1,60 @@ +### +# Copyright (c) 2005, 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. +### + +""" +Keeps track of the last time a user was seen on a channel. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "%%VERSION%%" + +__author__ = supybot.authors.jemfinch + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +import config +import plugin +reload(plugin) # In case we're being reloaded. +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Seen/config.py b/plugins/Seen/config.py new file mode 100644 index 000000000..f02065b06 --- /dev/null +++ b/plugins/Seen/config.py @@ -0,0 +1,48 @@ +### +# Copyright (c) 2005, 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. +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified himself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Seen', True) + + +Seen = conf.registerPlugin('Seen') +# This is where your configuration variables (if any) should go. For example: +# conf.registerGlobalValue(Seen, 'someConfigVariableName', +# registry.Boolean(False, """Help for someConfigVariableName.""")) + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78 diff --git a/plugins/Seen/plugin.py b/plugins/Seen/plugin.py new file mode 100644 index 000000000..e12fa9891 --- /dev/null +++ b/plugins/Seen/plugin.py @@ -0,0 +1,189 @@ +### +# Copyright (c) 2002-2004, 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. +### + +import re +import time +import string + +import supybot.log as log +import supybot.conf as conf +import supybot.utils as utils +import supybot.world as world +import supybot.ircdb as ircdb +from supybot.commands import * +import supybot.ircmsgs as ircmsgs +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks + +class IrcStringAndIntDict(utils.InsensitivePreservingDict): + def key(self, x): + if isinstance(x, int): + return x + else: + return ircutils.toLower(x) + +class SeenDB(plugins.ChannelUserDB): + IdDict = IrcStringAndIntDict + def serialize(self, v): + return list(v) + + def deserialize(self, channel, id, L): + (seen, saying) = L + return (float(seen), saying) + + def update(self, channel, nickOrId, saying): + seen = time.time() + self[channel, nickOrId] = (seen, saying) + self[channel, ''] = (seen, saying) + + def seenWildcard(self, channel, nick): + nicks = [] + nickRe = re.compile('.*'.join(nick.split('*')), re.I) + for (searchChan, searchNick) in self.keys(): + #print 'chan: %s ... nick: %s' % (searchChan, searchNick) + if isinstance(searchNick, int): + # We need to skip the reponses that are keyed by id as they + # apparently duplicate the responses for the same person that + # are keyed by nick-string + continue + if ircutils.strEqual(searchChan, channel): + try: + s = nickRe.match(searchNick).group() + except AttributeError: + continue + nicks.append(s) + L = [[nick, self.seen(channel, nick)] for nick in nicks] + def negativeTime(x): + return -x[1][0] + utils.sortBy(negativeTime, L) + return L + + def seen(self, channel, nickOrId): + return self[channel, nickOrId] + +filename = conf.supybot.directories.data.dirize('Seen.db') + +class Seen(callbacks.Privmsg): + noIgnore = True + def __init__(self, irc): + self.__parent = super(Seen, self) + self.__parent.__init__(irc) + self.db = SeenDB(filename) + world.flushers.append(self.db.flush) + + def die(self): + if self.db.flush in world.flushers: + world.flushers.remove(self.db.flush) + else: + self.log.debug('Odd, no flush in flushers: %r', world.flushers) + self.db.close() + self.__parent.die() + + def doPrivmsg(self, irc, msg): + if irc.isChannel(msg.args[0]): + said = ircmsgs.prettyPrint(msg) + channel = plugins.getChannel(msg.args[0]) + self.db.update(channel, msg.nick, said) + try: + id = ircdb.users.getUserId(msg.prefix) + self.db.update(channel, id, said) + except KeyError: + pass # Not in the database. + + def seen(self, irc, msg, args, channel, name): + """[] + + Returns the last time was seen and what was last seen + saying. is only necessary if the message isn't sent on the + channel itself. + """ + try: + results = [] + if '*' in name: + results = self.db.seenWildcard(channel, name) + else: + results = [[name, self.db.seen(channel, name)]] + if len(results) == 1: + (nick, info) = results[0] + (when, said) = info + irc.reply(format('%s was last seen in %s %s ago saying: %s', + nick, channel, + utils.timeElapsed(time.time()-when), said)) + elif len(results) > 1: + L = [] + for (nick, info) in results: + (when, said) = info + L.append(format('%s (%s ago)', nick, + utils.timeElapsed(time.time()-when))) + irc.reply(format('%s could be %L', name, (L, 'or'))) + else: + irc.reply(format('I haven\'t seen anyone matching %s.', name)) + except KeyError: + irc.reply(format('I have not seen %s.', name)) + seen = wrap(seen, ['channeldb', 'nick']) + + def last(self, irc, msg, args, channel): + """[] + + Returns the last thing said in . is only necessary + if the message isn't sent in the channel itself. + """ + try: + (when, said) = self.db.seen(channel, '') + irc.reply(format('Someone was last seen in %s %s ago saying: %s', + channel, utils.timeElapsed(time.time()-when), + said)) + except KeyError: + irc.reply('I have never seen anyone.') + last = wrap(last, ['channeldb']) + + + def user(self, irc, msg, args, channel, user): + """[] + + Returns the last time was seen and what was last seen + saying. This looks up in the user seen database, which means + that it could be any nick recognized as user that was seen. + is only necessary if the message isn't sent in the channel + itself. + """ + try: + (when, said) = self.db.seen(channel, user.id) + irc.reply(format('%s was last seen in %s %s ago saying: %s', + user.name, channel, + utils.timeElapsed(time.time()-when), said)) + except KeyError: + irc.reply(formen('I have not seen %s.',user.name)) + user = wrap(user, ['channeldb', 'otherUser']) + + +Class = Seen + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/plugins/Seen/test.py b/plugins/Seen/test.py new file mode 100644 index 000000000..c6811d63a --- /dev/null +++ b/plugins/Seen/test.py @@ -0,0 +1,70 @@ +### +# Copyright (c) 2002-2004, 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 supybot.test import * + +import supybot.ircdb as ircdb + +class ChannelDBTestCase(ChannelPluginTestCase): + plugins = ('Seen', 'User') + def setUp(self): + ChannelPluginTestCase.setUp(self) + self.prefix = 'foo!bar@baz' + self.nick = 'foo' + self.wildcardTest = ['f*', '*oo', '*foo*', 'f*o*o'] + self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, + 'register foo bar', + prefix=self.prefix)) + _ = self.irc.takeMsg() + chancap = ircdb.makeChannelCapability(self.channel, 'op') + ircdb.users.getUser(self.nick).addCapability(chancap) + + def testNoKeyErrorEscapeFromSeen(self): + self.assertRegexp('seen asldfkjasdlfkj', '^I have not seen') + self.assertNotRegexp('seen asldfkjasdlfkj', 'KeyError') + + def testSeen(self): + self.assertNotError('seen last') + self.assertNotError('list') + self.assertNotError('seen %s' % self.nick) + m = self.assertNotError('seen %s' % self.nick.upper()) + self.failUnless(self.nick.upper() in m.args[1]) + self.assertRegexp('seen user %s' % self.nick, + '^%s was last seen' % self.nick) + for wildcard in self.wildcardTest: + self.assertRegexp('seen %s' % wildcard, + '^%s was last seen' % self.nick) + self.assertRegexp('seen bar*', '^I haven\'t seen anyone matching') + + def testSeenNoUser(self): + self.assertNotRegexp('seen user alsdkfjalsdfkj', 'KeyError') + + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: + diff --git a/setup.py b/setup.py index aa146ce9f..313e1f557 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ plugins = [ 'Relay', 'RSS', 'Scheduler', + 'Seen', 'ShrinkUrl', 'Status', 'String',