Initial import.

This commit is contained in:
Jeremy Fincher 2005-01-19 13:14:38 +00:00
parent dbfec8afb9
commit 8ca625d339
76 changed files with 24013 additions and 0 deletions

7
ACKS Normal file
View File

@ -0,0 +1,7 @@
johhnyace, who gave me the modem that helped me tremendously in development.
bwp, who rewrote the Http.weather command, and also hosted the example
"supybot" in #supybot on OFTC and Freenode for quite some time.
sweede, for hosting the "main" supybot for awhile.
HostPC.com, for hosting the current example "supybot" and for graciously
providing DNS services and email.

10
BUGS Normal file
View File

@ -0,0 +1,10 @@
We're sure there are tons of them. When you find them, send them to us and
we'll fix them ASAP. We'd love to have a bugless bot someday...
Incidentally, the way to "send the bugs to us" is via SourceForge:
<http://sourceforge.net/tracker/?atid=489447&group_id=58965&func=browse>
Known bugs that probably won't get fixed:
BadWords' outFilter filters colors. It's not a high priority to get
that fixed.

1480
ChangeLog Normal file

File diff suppressed because it is too large Load Diff

67
DEVS Normal file
View File

@ -0,0 +1,67 @@
These are the developers of Supybot, in approximate order of ____.
Jeremy Fincher (jemfinch) is a Computer Science (and possibly
philosophy) student at The Ohio State University. He spends most of
his free time with his girlfriend Meg, but also...well, there's not
much also :) He hopes to graduate with good enough grades to go to
law school or seminary at some point in the future. He initially
wrote the majority of the Supybot framework and standard plugins,
though he's been trying to slowly phase himself out of plugin-writing
and more into framework-enhancement. Rather than list the specific
things he's done, you can just assume that if someone else isn't
claiming it, it was probably done by him.
Daniel DiPaolo (Strike/ddipaolo) is a lazy Texan punk with a job as an IT
monkey who spends his free time coding, playing ultimate frisbee, and arguing
pointless things on the internet. As far as the bot goes, he's mainly a
plugin developer but he has helped here and there with various under-the-hood
things and is one of the few people (other than jemfinch) who understands the
inner workings of Supybot. His biggest plugin contribution (in terms of sheer
lines of code) has been the MoobotFactoids plugin and all the workd involved
in getting that plugin to work, but he has also helped with a lot of testing,
debugging, and brainstorming. He also wrote the Dunno, News, and Todo plugins
and is responsible for a significant amount of code in the Poll, Debian,
QuoteGrabs, Karma, and ChannelDB plugins.
James Vega (jamessan) is an Electrical Engineering/Computer Science student at
Northeastern University. He wrote the Sourceforge and Ebay plugins as well as
the first incarnation of the Babelfish commands and most of Amazon. He has
also performed a significant amount of maintenance and refactoring of plugins
in general. Some of the plugins that were affected the most are Debian,
FunDB, Gameknot, Http, Note, and Quote. All of the link snarfers, save
Bugzilla's, were also written by jamessan. His meddlings have prompted the
implementation of Toggleables, which eventually evolved to Configurables and
then to the current registry system. As well as being the current webmaster,
he also overhauled the tool which is used to generate the site's HTML
documentation for Supybot and setup the weekly creation of CVS snapshots.
Brett Kelly (inkedmn) is a hobbyist (soon to be professional :)) coder
from southern California who enjoys collecting tattoos (on his body) and
drinking coffee with his wife. He initially wrote the Note plugin as well
as several commands in the Http plugin.
Vincent Foley-Bourgon is a recently-graduated student from Quebec who
enjoys anything pointless, unprofitable, and generally useless. Recently
returning to Supybot development (after writing the original freshmeat
command for the Http plugin) he wrote the entire Hangman infrastructure
for the Words plugin.
Daniel Berlin is a soon to be lawyer with a background in computer science
and compilers. He enjoys selling crack to young homeless orphans, and works
on Supybot when he's not lawyering or hacking on gcc.
Keith Jones (kmj) dislikes talking about himself in the third person. He
has an MS in Computer Science, and has decided to see how long he can go
without using that in any kind of professional capacity. To that end he
is currently taking some math classes and applying to math Ph.D programs
so some day he can be a professor at a college near a snowy mountain where
he will ski every morning. So far, he hasn't done much for the project
except squeeze Doug Bell's GPL'd unit conversion code into a supybot plugin.
Stéphan Kochen (G-LiTe) is a lazy (soon to be) computer science student.
He's usually just freelancing and submitting patches here and there when he
bumps into a bug that bothers him, but Supybot is one of the first projects
he semi-actively tries to work on. ;) His biggest contribution has been the
refactoring of the supybot-wizard script to use the registry, though he also
likes to track down those nasty obscure bugs which haunt many of our fine
applications these days.

110
INSTALL Normal file
View File

@ -0,0 +1,110 @@
So what do you do? That depends on which operating system you're
running. We've split this document up to address the different
methods, so find the section for your operating system and continue
from there. First let's start with the parts that are common to all
OSes.
###
# COMMON:
###
First things first: Supybot *requires* at least Python 2.3. There ain't
no getting around it. We do not require any version greater than 2.3,
but we will be compatible with any version of Python >= 2.3. If you're
a Python developer, you probably know how superior 2.3 is to previous
incarnations. If you're not, just think about the difference between a
bowl of plain vanilla ice cream and a banana split. Or something like
that. Either way, *We're* Python developers and we like banana splits.
So, be sure to install python2.3 or greater before continuing. You can
get it from http://www.python.org/
For more information and help on how to use Supybot, checkout
the documents under docs/ (especially GETTING_STARTED and CONFIGURATION).
Our forums (http://forums.supybot.org/) may also be of use, especially
the "Tips and Tricks" topic under "Supybot User Discussion".
###
# UNIX/Linux/*BSD:
###
If you're installing Python using your distributor's packages, you may
need a python-dev package installed, too. If you don't have a
/usr/lib/python2.3/distutils directory (assuming /usr/lib/python2.3 is
where your Python libs are installed), then you will need a python-dev
package.
After you extract Supybot and cd into the supybot directory just
created, you'll want to run (as root) "python setup.py install". This
will install Supybot globally. If you need to install locally for
whatever reason, see the addendum near the end of this document.
You'll then have several new programs installed where Python scripts
are normally installed on your system (/usr/bin or /usr/local/bin are
common on UNIX systems). The two that might be of particular interest
to you, the new user, are "supybot" and "supybot-wizard". The former
("supybot") is the script to run an actual bot; the latter
("supybot-wizard") is an in-depth wizard that provides a nice user
interface for creating a registry file for your bot.
So after running supybot-wizard, you've got a nice registry file
handy. If you're not satisfied with your answers to any of the
questions you were asked, feel free to run the program again until
you're satisfied with all your answers. Once you're satisfied,
though, run the "supybot" program with the registry file you created
as an argument. This will start the bot; unless you turned off
logging to stdout, you'll see some nice log messages describing what
the bot is doing at any particular moment; it may pause for a
significant amount of time after saying "Connecting to ..." while the
server tries to check its ident.
###
# Windows:
###
*** If you are using an IPV6 connection, you will not be able to run
Supybot under Windows (unless Python has fixed things). Current
versions of Python for Windows are *not* built with IPV6 support. This
isn't expected to be fixed until Python 2.4, at the earliest.
Now that you have Python installed, open up a command prompt. The
easiest way to do this is to open the run dialog (Programs -> run) and
type "cmd" (for Windows 2000/XP/2003) or "command" (for Windows 9x).
In order to reduce the amount of typing you need to do, I suggest
adding Python's directory to your path. If you installed Python using
the default settings, you would then do the following in the command
prompt (otherwise change the path to match your settings):
set PATH=%PATH%;C:\Python23\
You should now be able to type "python" to start the Python
interpreter (CTRL-Z and Return to exit). Now that that's setup,
you'll want to cd into the directory that was created when you
unzipped Supybot; I'll assume you unzipped it to C:\Supybot for these
instructions. From C:\Supybot, run "python setup.py install". This
will install Supybot under C:\Python23\. If you want to install
Supybot to a non-default location, see the addendum near the end of
this document. You will now have several new programs installed in
C:\Python23\Scripts\. The two that might be of particular interest to
you, the new user, are "supybot" and "supybot-wizard". The former
("supybot") is the script to run an actual bot; the latter
("supybot-wizard") is an in-depth wizard that provides a nice user
interface for creating a registry file for your bot.
Now you will want to run "python C:\Python23\Scripts\supybot-wizard"
to generate a registry file for your bot. So after running
supybot-wizard, you've got a nice registry file handy. If you're not
satisfied with your answers to any of the questions you were asked,
feel free to run the program again until you're satisfied with all
your answers. Once you're satisfied, though, run "python
C:\Python23\Scripts\supybot botname.conf". This will start the bot;
unless you turned off logging to stdout, you'll see some nice log
messages describing what the bot is doing at any particular moment; it
may pause for a significant amount of time after saying "Connecting
to ..." while the server tries to check its ident.
###
# Addenda
###
Local installs: See this forum post: http://tinyurl.com/2tb37

28
LICENSE Normal file
View File

@ -0,0 +1,28 @@
Copyright (c) 2002-2004 Jeremiah Fincher and others
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 permission.
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.
Portions of the included source code are copyright by its original author(s)
and remain subject to its associated license.

38
README Normal file
View File

@ -0,0 +1,38 @@
EVERYONE:
---------
Read LICENSE. It's a 2-clause BSD license, but you should read it anyway.
USERS:
------
If you're upgrading, read RELNOTES. If you're new to Supybot,
read docs/GETTING_STARTED for an introduction to the bot, and read
docs/CAPABILITIES to see how to use capabilities to your greater
benefit.
If you have any trouble, feel free to swing by #supybot on
irc.freenode.net or irc.oftc.net (we have a Supybot there relaying,
so either network works) and ask questions. We'll be happy to help
wherever we can. And by all means, if you find anything hard to
understand or think you know of a better way to do something,
*please* post it on Sourceforge.net so we can improve the bot!
WINDOWS USERS:
--------------
The wizards (supybot-wizard, supybot-newplugin, and
supybot-adduser) are all installed to your Python directory's
\Scripts. What that *probably* means is that you'll run them like
this: C:\Python23\python C:\Python23\Scripts\supybot-wizard
DEVELOPERS:
-----------
Read OVERVIEW to see what the modules are used for. Read PLUGIN-EXAMPLE
to see some examples of callbacks and commands written for the bot.
Read INTERFACES to see what kinds of objects you'll be dealing with.
Read STYLE if you wish to contribute; all contributed code must meet
the guidelines set forth there.
Be sure to run "test/test.py --help" to see what options are available
to you when testing. Windows users in particular should be sure to
exclude test_Debian.py and test_Unix.py.

257
RELNOTES Normal file
View File

@ -0,0 +1,257 @@
Version 0.80.0
We *finally* hit 0.80.0! This release is completely compatible with
the last release candidate.
An update to Babelfish may cause an error message to be displayed in
the console when the bot is first run. The error message should be
gone when the bot is restarted.
We also have a new community website at http://www.supybot.com/ where
our users can submit their own plugins, view/download other people's
plugins and discuss all things Supybot-related.
Version 0.80.0rc3
Another bugfix release. This one was pretty important as it actually
makes supybot.database.plugins.channelSpecific work properly.
Version 0.80.0rc2
supybot.databases.plugins.channelSpecific.channel was renamed to
supybot.databases.plugins.channelSpecific.link.
supybot.databases.plugins.channelSpecific.link.allow was added, which
determines whether a channel will allow other channels to link to its
database.
Infobot is no longer deprecated and the following changes were made to
its config variables:
supybot.plugins.Infobot.answerUnaddressedQuestions was renamed to
supybot.plugins.Infobot.unaddressed.answerQuestions.
supybot.plugins.Infobot.snarfUnaddressedDefinitions was renamed to
supybot.plugins.Infobot.unaddressed.snarfDefinitions.
supybot.plugins.Infobot.unaddressed.replyExistingFactoid was added to
determine whether the bot will reply when someone attempts to create a
duplicate factoid.
Version 0.80.0pre6
Another bugfix release. No incompatibilities known. The only
registry change is that supybot.databases.users.hash has been
removed.
Version 0.80.0pre5
Completely bugfix release. No incompatibilies known.
Version 0.80.0pre4
Mainly a bug fix release. This will likely be the last release before
0.80.0final, but we're gonna let it stew for a couple weeks to attempt
to catch any lingering bugs.
ansycoreDrivers is now deprecated in favor of socketDrivers or
twistedDrivers.
supybot.databases.plugins.channelSpecific.channel is now a channelValue
so that you can link specific channels together (instead of all channels
being linked together).
For those of you that use eval and/or exec, they have been removed from
the Owner plugin and are now in sandbox/Debug.py (which you'll have to
grab from CVS).
Version 0.80.0pre3
The database format for the Note plugin has changed to a flatfile
format; use tools/noteConvert.py to convert it to the new format.
Ditto that for the URL database.
FunDB is deprecated and will be removed at the next major release;
use tools/fundbConvert.py to convert your old FunDB databases to Lart
and Praise databases.
If you had turned off supybot.databases.plugins.channelSpecific, your
non-channel-specific database files had gone directly into your data/
directory. We had some problems with poor interactions between that
configuration variable and channel capabilities, though, so we
changed the implementation so that non-channel-specific databases are
considered databases of a single (configurable) channel (defaulting
to "#"). This will also help others who are converting from
channel-specific to non-channel-specific databases, but for you
who've already made the switch, you'll need to move your database
files again, from data/ to data/# (or whatever channel you might
change that variable to).
supybot.channels doesn't exist anymore; now the only list of channels
to join is per-network, in supybot.networks.<network>.channels.
We weren't serializing supybot.replies.* properly in older versions.
Now we are, but the old, improperly serialized versions won't work
properly. Remove from your configuration file all variables
beginning with "supybot.replies" before you start the bot.
The URL database has been changed again, but it will use a different
filename so you shouldn't run into conflicts, just a newly-empty
database.
We upgraded the SOAP stuff in others; you may do well to do a
setup.py install --clean this time around.
Version 0.80.0pre2
Many more bugs have been fixed. A few more plugins have been updated
to use our new-style database abstraction. If it seems like your
databases are suddenly empty, look for a new database file named
Plugin.dbtype.db. We've also added a few more configuration variables.
Version 0.80.0pre1
Tons of bugs fixed, many features and plugins added. Everything
should be entirely compatible; many more configuration variables have
been added.
Version 0.79.9999
Some more bugs fixed, added a few features and a couple configuration
variabless. This should hopefully be the last release before 0.80.0,
which will finally bring us to pure Beta status.
Version 0.79.999
Some bugs fixed, but the ones that were fixed were pretty big. This
is, of course, completely compatible with the last release.
Version 0.79.99
Many bugs fixed, thanks to the users who reported them. We're
getting asymptotically closer to 0.80.0 -- maybe this'll be the last
one, maybe we'll have to release an 0.79.999 -- either way, we're
getting close :) Check out the ChangeLog for the fixes and a few new
features.
Version 0.79.9
We've changed so much stuff in this release that we've given up on
users upgrading their configuration files for the new release. So
do a clean install (python2.3 setup.py install --clean), run the
wizard again, and kick some butt.
(It's rumored that you can save most of your old configuration by
appending your new configuration at the end of your old configuration
and running supybot with that new configuration file. This, of
course, comes with no warranty or guarantee of utility -- try it if
you want, but backup your original configuration file!)
Version 0.77.2
This is a drop-in replacement for 0.77.1, with two exceptions. The
configuration variable formerly known as
"supybot.plugins.Services.password" is now known as
"supybot.plugins.Services.NickServ.password", due to the fact that
there might be different passwords for NickServ and ChanServ (and
ChanServ passwords are per-channel, whereas NickServ passwords are
global). If you're using the Services plugin, you'll need to make
this change in order to continue identifying with services. The
configuration variable formerly known as
"supybot.plugins.Babelfish.disabledLanguages" is now known as
"supybot.plugins.Babelfish.languages". The configuration variable now
accepts the languages that *will* be translated as opposed to ones
that are *not* translated.
Tests and the developer sandbox are not longer delivered with our
release tarballs. If you're a developer and you want these, you
should either check out CVS or download one of our weekly CVS
snapshots, available at http://supybot.sourceforge.net/snapshots/ .
Version 0.77.1
This is a drop-in replacement for 0.77.0 -- no incompatibilities, to
out knowledge. Simply install over your old installation and restart
your bot :)
Version 0.77.0
Setup.py will automatically remove your old installations for you, no
need to worry about that yourself.
Configuration has been *entirely* redone. Read the new
GETTING_STARTED document to see how to work with configuration
variables now. Your old botscripts from earlier versions *will not*
work with the new configuration method. We'd appreciate it if you'd
rerun the wizard in order for us to find any bugs that remain in it
before we officially declare ourselves Beta. Note also that because
of the new configuration method, the interface for plugins' configure
function has changed: there are no longer any onStart or afterConnect
arguments, so all configuration should be performed via the registry.
Channel capabilities have been changed; rather than being
#channel.capability, they're now #channel,capability. It's a bit
uglier, we know, but dots can be valid in channel names, and we
needed the dot for handling plugin.command capabilities.
tools/ircdbConvert.py should update this for you.
The on-disk format of the user/channel databases has changed to be far
more readable. A conversion utility is included, as mentioned before:
tools/ircdbConvert.py. Run this with no arguments to see the
directions for using it.
Uh, we were just kidding about the upgrade script in 0.76.0 :) It'll
be a little while longer. We do have several little upgrade scripts,
though.
Version 0.76.1
Almost entirely bugfixes, just some minor (and some less minor) bugs
that need to get in before we really start hacking on the next
version. Should be *entirely* compatible with 0.76.0.
Version 0.76.0
Major bugfix release. A great number of bugs fixed. This is the last
release without an upgrade script.
The only hiccup in the upgrade from 0.75.0 should be that you'll need
to update your botscript to reflect the removal of the debug module.
We'd rather you use supybot-wizard to generate a new botscript, of
course, but if you insist on modifying your existing botscript, take a
look at
<http://cvs.sourceforge.net/viewcvs.py/supybot/supybot/src/template.py?r1=1.20&r2=1.21>
to see what you need to do.
Version 0.75.0
Don't forget to reinstall (i.e., run "python setup.py install" as
root). Sometimes it even does good to remove the old installation;
$PYTHON/site-packages/supybot can be removed with no problems
whatsoever.
You will need to re-run supybot-wizard and generate a new botscript.
The Infobot plugin has been removed from this release; it's not ready
for prime time. If you're interested in getting it running (i.e., you
want full Infobot compatibility and aren't satisfied with either
MoobotFactoids or Factoids) then swing over to #supybot and we can
discuss the tests. We simply don't know enough about Infobot to make
sure our Infobot plugin is an exact replica, and need someone's help
with making the changes necessary for that.

46
plugins/Admin/__init__.py Normal file
View File

@ -0,0 +1,46 @@
###
# Copyright (c) 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 supybot
__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.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure

48
plugins/Admin/config.py Normal file
View File

@ -0,0 +1,48 @@
###
# Copyright (c) 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 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('Admin', True)
Admin = conf.registerPlugin('Admin')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(Admin, 'someConfigVariableName',
# registry.Boolean(False, """Help for someConfigVariableName."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

349
plugins/Admin/plugin.py Normal file
View File

@ -0,0 +1,349 @@
###
# 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.
###
"""
These are commands useful for administrating the bot; they all require their
caller to have the 'admin' capability. This plugin is loaded by default.
"""
import supybot
__author__ = supybot.authors.jemfinch
import supybot.fix as fix
import sys
import time
import pprint
from itertools import imap
import supybot.log as log
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.utils as utils
from supybot.commands import *
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.schedule as schedule
import supybot.callbacks as callbacks
class Admin(callbacks.Privmsg):
def __init__(self):
self.__parent = super(Admin, self)
self.__parent.__init__()
self.joins = {}
self.pendingNickChanges = {}
def do437(self, irc, msg):
"""Nick/channel temporarily unavailable."""
target = msg.args[0]
if irc.isChannel(target): # We don't care about nicks.
t = time.time() + 30
# Let's schedule a rejoin.
networkGroup = conf.supybot.networks.get(irc.network)
def rejoin():
irc.queueMsg(networkGroup.channels.join(target))
# We don't need to schedule something because we'll get another
# 437 when we try to join later.
schedule.addEvent(rejoin, t)
self.log.info('Scheduling a rejoin to %s at %s; '
'Channel temporarily unavailable.', target, t)
def do471(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error('Cannot join %s, it\'s full.' % channel)
except KeyError:
self.log.debug('Got 471 without Admin.join being called.')
def do473(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error('Cannot join %s, I was not invited.' % channel)
except KeyError:
self.log.debug('Got 473 without Admin.join being called.')
def do474(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error('Cannot join %s, it\'s banned me.' % channel)
except KeyError:
self.log.debug('Got 474 without Admin.join being called.')
def do475(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error('Cannot join %s, my keyword was wrong.' % channel)
except KeyError:
self.log.debug('Got 475 without Admin.join being called.')
def do515(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error('Cannot join %s, I\'m not identified with the NickServ.'
% channel)
except KeyError:
self.log.debug('Got 515 without Admin.join being called.')
def doJoin(self, irc, msg):
if msg.prefix == irc.prefix:
try:
del self.joins[msg.args[0]]
except KeyError:
s = 'Joined a channel without Admin.join being called.'
self.log.debug(s)
def doInvite(self, irc, msg):
channel = msg.args[1]
if channel not in irc.state.channels:
if conf.supybot.alwaysJoinOnInvite() or \
ircdb.checkCapability(msg.prefix, 'admin'):
self.log.info('Invited to %s by %s.', channel, msg.prefix)
networkGroup = conf.supybot.networks.get(irc.network)
irc.queueMsg(networkGroup.channels.join(channel))
conf.supybot.networks.get(irc.network).channels().add(channel)
else:
self.log.warning('Invited to %s by %s, but '
'supybot.alwaysJoinOnInvite was False and '
'the user lacked the "admin" capability.',
channel, msg.prefix)
def join(self, irc, msg, args, channel, key):
"""<channel> [<key>]
Tell the bot to join the given channel. If <key> is given, it is used
when attempting to join the channel.
"""
if not irc.isChannel(channel):
irc.errorInvalid('channel', channel, Raise=True)
networkGroup = conf.supybot.networks.get(irc.network)
networkGroup.channels().add(channel)
if key:
networkGroup.channels.key.get(channel).setValue(key)
maxchannels = irc.state.supported.get('maxchannels', sys.maxint)
if len(irc.state.channels) + 1 > maxchannels:
irc.error('I\'m already too close to maximum number of '
'channels for this network.', Raise=True)
irc.queueMsg(networkGroup.channels.join(channel))
irc.noReply()
self.joins[channel] = (irc, msg)
join = wrap(join, ['validChannel', additional('something')])
def channels(self, irc, msg, args):
"""takes no arguments
Returns the channels the bot is on. Must be given in private, in order
to protect the secrecy of secret channels.
"""
L = irc.state.channels.keys()
if L:
utils.sortBy(ircutils.toLower, L)
irc.reply(utils.commaAndify(L))
else:
irc.reply('I\'m not currently in any channels.')
channels = wrap(channels, ['private'])
def do484(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error('My connection is restricted, I can\'t change nicks.')
else:
self.log.debug('Got 484 without Admin.nick being called.')
def do433(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error('Someone else is already using that nick.')
else:
self.log.debug('Got 433 without Admin.nick being called.')
def do435(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error('That nick is currently banned.')
else:
self.log.debug('Got 435 without Admin.nick being called.')
def do438(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error('I can\'t change nicks, the server said %s.' %
utils.quoted(msg.args[2]), private=True)
else:
self.log.debug('Got 438 without Admin.nick being called.')
def doNick(self, irc, msg):
if msg.nick == irc.nick or msg.args[0] == irc.nick:
try:
del self.pendingNickChanges[irc]
except KeyError:
self.log.debug('Got NICK without Admin.nick being called.')
def nick(self, irc, msg, args, nick):
"""[<nick>]
Changes the bot's nick to <nick>. If no nick is given, returns the
bot's current nick.
"""
if nick:
conf.supybot.nick.setValue(nick)
irc.queueMsg(ircmsgs.nick(nick))
self.pendingNickChanges[irc.getRealIrc()] = irc
else:
irc.reply(irc.nick)
nick = wrap(nick, [additional('nick')])
def part(self, irc, msg, args, channel, reason):
"""[<channel>] [<reason>]
Tells the bot to part the list of channels you give it. <channel> is
only necessary if you want the bot to part a channel other than the
current channel. If <reason> is specified, use it as the part
message.
"""
if channel is None:
if irc.isChannel(msg.args[0]):
channel = msg.args[0]
else:
irc.error(Raise=True)
try:
network = conf.supybot.networks.get(irc.network)
network.channels().remove(channel)
except KeyError:
pass
if channel not in irc.state.channels:
irc.error('I\'m not in %s.' % channel, Raise=True)
irc.queueMsg(ircmsgs.part(channel, reason or msg.nick))
if msg.nick in irc.state.channels[channel].users:
irc.noReply()
else:
irc.replySuccess()
part = wrap(part, [optional('validChannel'), additional('text')])
def addcapability(self, irc, msg, args, user, capability):
"""<name|hostmask> <capability>
Gives the user specified by <name> (or the user to whom <hostmask>
currently maps) the specified capability <capability>
"""
# Ok, the concepts that are important with capabilities:
#
### 1) No user should be able to elevate his privilege to owner.
### 2) Admin users are *not* superior to #channel.ops, and don't
### have God-like powers over channels.
### 3) We assume that Admin users are two things: non-malicious and
### and greedy for power. So they'll try to elevate their privilege
### to owner, but they won't try to crash the bot for no reason.
# Thus, the owner capability can't be given in the bot. Admin users
# can only give out capabilities they have themselves (which will
# depend on supybot.capabilities and its child default) but generally
# means they can't mess with channel capabilities.
if ircutils.strEqual(capability, 'owner'):
irc.error('The "owner" capability can\'t be added in the bot. '
'Use the supybot-adduser program (or edit the '
'users.conf file yourself) to add an owner capability.')
return
if ircdb.isAntiCapability(capability) or \
ircdb.checkCapability(msg.prefix, capability):
user.addCapability(capability)
ircdb.users.setUser(user)
irc.replySuccess()
else:
irc.error('You can\'t add capabilities you don\'t have.')
addcapability = wrap(addcapability, ['otherUser', 'lowered'])
def removecapability(self, irc, msg, args, user, capability):
"""<name|hostmask> <capability>
Takes from the user specified by <name> (or the user to whom
<hostmask> currently maps) the specified capability <capability>
"""
if ircdb.checkCapability(msg.prefix, capability) or \
ircdb.isAntiCapability(capability):
try:
user.removeCapability(capability)
ircdb.users.setUser(user)
irc.replySuccess()
except KeyError:
irc.error('That user doesn\'t have that capability.')
else:
s = 'You can\'t remove capabilities you don\'t have.'
irc.error(s)
removecapability = wrap(removecapability, ['otherUser','lowered'])
def ignore(self, irc, msg, args, hostmask, expires):
"""<hostmask|nick> [<expires>]
Ignores <hostmask> or, if a nick is given, ignores whatever hostmask
that nick is currently using. <expires> is a "seconds from now" value
that determines when the ignore will expire; if, for instance, you wish
for the ignore to expire in an hour, you could give an <expires> of
3600. If no <expires> is given, the ignore will never automatically
expire.
"""
ircdb.ignores.add(hostmask, expires)
irc.replySuccess()
ignore = wrap(ignore, ['hostmask', additional('expiry', 0)])
def unignore(self, irc, msg, args, hostmask):
"""<hostmask|nick>
Ignores <hostmask> or, if a nick is given, ignores whatever hostmask
that nick is currently using.
"""
try:
ircdb.ignores.remove(hostmask)
irc.replySuccess()
except KeyError:
irc.error('%s wasn\'t in the ignores database.' % hostmask)
unignore = wrap(unignore, ['hostmask'])
def ignores(self, irc, msg, args):
"""takes no arguments
Returns the hostmasks currently being globally ignored.
"""
# XXX Add the expirations.
if ircdb.ignores.hostmasks:
irc.reply(utils.commaAndify(imap(repr, ircdb.ignores.hostmasks)))
else:
irc.reply('I\'m not currently globally ignoring anyone.')
ignores = wrap(ignores)
Class = Admin
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

117
plugins/Admin/test.py Normal file
View File

@ -0,0 +1,117 @@
###
# 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 *
class AdminTestCase(PluginTestCase):
plugins = ('Admin',)
def testChannels(self):
def getAfterJoinMessages():
m = self.irc.takeMsg()
self.assertEqual(m.command, 'MODE')
m = self.irc.takeMsg()
self.assertEqual(m.command, 'WHO')
self.assertRegexp('channels', 'not.*in any')
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))
getAfterJoinMessages()
self.assertRegexp('channels', '#foo')
self.irc.feedMsg(ircmsgs.join('#bar', prefix=self.prefix))
getAfterJoinMessages()
self.assertRegexp('channels', '#bar and #foo')
self.irc.feedMsg(ircmsgs.join('#Baz', prefix=self.prefix))
getAfterJoinMessages()
self.assertRegexp('channels', '#bar, #Baz, and #foo')
def testIgnoreUnignore(self):
self.assertNotError('admin ignore foo!bar@baz')
self.assertError('admin ignore alsdkfjlasd')
self.assertNotError('admin unignore foo!bar@baz')
self.assertError('admin unignore foo!bar@baz')
def testIgnores(self):
self.assertNotError('admin ignores')
self.assertNotError('admin ignore foo!bar@baz')
self.assertNotError('admin ignores')
self.assertNotError('admin ignore foo!bar@baz')
self.assertNotError('admin ignores')
def testAddcapability(self):
self.assertError('addcapability sdlkfj foo')
u = ircdb.users.newUser()
u.name = 'foo'
ircdb.users.setUser(u)
self.assertError('removecapability foo bar')
self.assertNotRegexp('removecapability foo bar', 'find')
def testRemoveCapability(self):
self.assertError('removecapability alsdfkjasd foo')
def testJoin(self):
m = self.getMsg('join #foo')
self.assertEqual(m.command, 'JOIN')
self.assertEqual(m.args[0], '#foo')
m = self.getMsg('join #foo key')
self.assertEqual(m.command, 'JOIN')
self.assertEqual(m.args[0], '#foo')
self.assertEqual(m.args[1], 'key')
def testPart(self):
def getAfterJoinMessages():
m = self.irc.takeMsg()
self.assertEqual(m.command, 'MODE')
m = self.irc.takeMsg()
self.assertEqual(m.command, 'WHO')
self.assertError('part #foo')
self.assertRegexp('part #foo', 'not in')
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))
getAfterJoinMessages()
m = self.getMsg('part #foo')
self.assertEqual(m.command, 'PART')
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))
getAfterJoinMessages()
m = self.getMsg('part #foo reason')
self.assertEqual(m.command, 'PART')
self.assertEqual(m.args[0], '#foo')
self.assertEqual(m.args[1], 'reason')
def testNick(self):
original = conf.supybot.nick()
try:
m = self.getMsg('nick foobar')
self.assertEqual(m.command, 'NICK')
self.assertEqual(m.args[0], 'foobar')
finally:
conf.supybot.nick.setValue(original)
def testAddCapabilityOwner(self):
self.assertError('admin addcapability %s owner' % self.nick)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -0,0 +1,49 @@
###
# Copyright (c) 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 supybot
__author__ = supybot.authors.jemfinch
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {
supybot.authors.skorobeus: ['enable', 'disable'],
}
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Backwards compatibility.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure

82
plugins/Channel/config.py Normal file
View File

@ -0,0 +1,82 @@
###
# Copyright (c) 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 supybot.conf as conf
import supybot.utils as utils
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('Channel', True)
class BanmaskStyle(registry.SpaceSeparatedSetOfStrings):
validStrings = ('exact', 'nick', 'user', 'host')
def __init__(self, *args, **kwargs):
assert self.validStrings, 'There must be some valid strings. ' \
'This is a bug.'
registry.SpaceSeparatedSetOfStrings.__init__(self, *args, **kwargs)
self.__doc__ = 'Valid values include %s.' % \
utils.commaAndify(map(repr, self.validStrings))
def help(self):
strings = [s for s in self.validStrings if s]
return '%s Valid strings: %s.' % \
(self._help, utils.commaAndify(strings))
def normalize(self, s):
lowered = s.lower()
L = list(map(str.lower, self.validStrings))
try:
i = L.index(lowered)
except ValueError:
return s # This is handled in setValue.
return self.validStrings[i]
def setValue(self, v):
v = map(self.normalize, v)
for s in v:
if s not in self.validStrings:
self.error()
registry.SpaceSeparatedSetOfStrings.setValue(self, self.List(v))
Channel = conf.registerPlugin('Channel')
conf.registerChannelValue(Channel, 'alwaysRejoin',
registry.Boolean(True, """Determines whether the bot will always try to
rejoin a channel whenever it's kicked from the channel."""))
conf.registerChannelValue(Channel, 'banmask',
BanmaskStyle(['user', 'host'], """Determines what will be used as the
default banmask style."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

822
plugins/Channel/plugin.py Normal file
View File

@ -0,0 +1,822 @@
###
# 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.
###
"""
Basic channel management commands. Many of these commands require their caller
to have the <channel>.op capability. This plugin is loaded by default.
"""
import sys
import time
from itertools import imap
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.utils as utils
from supybot.commands import *
import supybot.ircmsgs as ircmsgs
import supybot.schedule as schedule
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
class Channel(callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
self.invites = {}
def doKick(self, irc, msg):
channel = msg.args[0]
if msg.args[1] == irc.nick:
if self.registryValue('alwaysRejoin', channel):
networkGroup = conf.supybot.networks.get(irc.network)
irc.sendMsg(networkGroup.channels.join(channel))
def _sendMsg(self, irc, msg):
irc.queueMsg(msg)
irc.noReply()
def mode(self, irc, msg, args, channel, modes):
"""[<channel>] <mode> [<arg> ...]
Sets the mode in <channel> to <mode>, sending the arguments given.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
self._sendMsg(irc, ircmsgs.mode(channel, modes))
mode = wrap(mode,
[('checkChannelCapability', 'op'),
('haveOp', 'change the mode'),
many('something')])
def limit(self, irc, msg, args, channel, limit):
"""[<channel>] [<limit>]
Sets the channel limit to <limit>. If <limit> is 0, or isn't given,
removes the channel limit. <channel> is only necessary if the message
isn't sent in the channel itself.
"""
if limit:
self._sendMsg(irc, ircmsgs.mode(channel, ['+l', limit]))
else:
self._sendMsg(irc, ircmsgs.mode(channel, ['-l']))
limit = wrap(limit, [('checkChannelCapability', 'op'),
('haveOp', 'change the limit'),
additional('nonNegativeInt', 0)])
def moderate(self, irc, msg, args, channel):
"""[<channel>]
Sets +m on <channel>, making it so only ops and voiced users can
send messages to the channel. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
self._sendMsg(irc, ircmsgs.mode(channel, ['+m']))
moderate = wrap(moderate, [('checkChannelCapability', 'op'),
('haveOp', 'moderate the channel')])
def unmoderate(self, irc, msg, args, channel):
"""[<channel>]
Sets -m on <channel>, making it so everyone can
send messages to the channel. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
self._sendMsg(irc, ircmsgs.mode(channel, ['-m']))
unmoderate = wrap(unmoderate, [('checkChannelCapability', 'op'),
('haveOp', 'unmoderate the channel')])
def key(self, irc, msg, args, channel, key):
"""[<channel>] [<key>]
Sets the keyword in <channel> to <key>. If <key> is not given, removes
the keyword requirement to join <channel>. <channel> is only necessary
if the message isn't sent in the channel itself.
"""
networkGroup = conf.supybot.networks.get(irc.network)
networkGroup.channels.key.get(channel).setValue(key)
if key:
self._sendMsg(irc, ircmsgs.mode(channel, ['+k', key]))
else:
self._sendMsg(irc, ircmsgs.mode(channel, ['-k']))
key = wrap(key, [('checkChannelCapability', 'op'),
('haveOp', 'change the keyword'),
additional('somethingWithoutSpaces', '')])
def op(self, irc, msg, args, channel, nicks):
"""[<channel>] [<nick> ...]
If you have the #channel,op capability, this will give all the <nick>s
you provide ops. If you don't provide any <nick>s, this will op you.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
if not nicks:
nicks = [msg.nick]
self._sendMsg(irc, ircmsgs.ops(channel, nicks))
op = wrap(op, [('checkChannelCapability', 'op'),
('haveOp', 'op someone'),
any('nickInChannel')])
def halfop(self, irc, msg, args, channel, nicks):
"""[<channel>] [<nick> ...]
If you have the #channel,halfop capability, this will give all the
<nick>s you provide halfops. If you don't provide any <nick>s, this
will give you halfops. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
if not nicks:
nicks = [msg.nick]
self._sendMsg(irc, ircmsgs.halfops(channel, nicks))
halfop = wrap(halfop, [('checkChannelCapability', 'halfop'),
('haveOp', 'halfop someone'),
any('nickInChannel')])
def voice(self, irc, msg, args, channel, nicks):
"""[<channel>] [<nick> ...]
If you have the #channel,voice capability, this will voice all the
<nick>s you provide. If you don't provide any <nick>s, this will
voice you. <channel> is only necessary if the message isn't sent in the
channel itself.
"""
if nicks:
if len(nicks) == 1 and msg.nick in nicks:
capability = 'voice'
else:
capability = 'op'
else:
nicks = [msg.nick]
capability = 'voice'
capability = ircdb.makeChannelCapability(channel, capability)
if ircdb.checkCapability(msg.prefix, capability):
self._sendMsg(irc, ircmsgs.voices(channel, nicks))
else:
irc.errorNoCapability(capability)
voice = wrap(voice, ['channel', ('haveOp', 'voice someone'),
any('nickInChannel')])
def deop(self, irc, msg, args, channel, nicks):
"""[<channel>] [<nick> ...]
If you have the #channel,op capability, this will remove operator
privileges from all the nicks given. If no nicks are given, removes
operator privileges from the person sending the message.
"""
if irc.nick in nicks:
irc.error('I cowardly refuse to deop myself. If you really want '
'me deopped, tell me to op you and then deop me '
'yourself.', Raise=True)
if not nicks:
nicks = [msg.nick]
self._sendMsg(irc, ircmsgs.deops(channel, nicks))
deop = wrap(deop, [('checkChannelCapability', 'op'),
('haveOp', 'deop someone'),
any('nickInChannel')])
def dehalfop(self, irc, msg, args, channel, nicks):
"""[<channel>] [<nick> ...]
If you have the #channel,op capability, this will remove half-operator
privileges from all the nicks given. If no nicks are given, removes
half-operator privileges from the person sending the message.
"""
if irc.nick in nicks:
irc.error('I cowardly refuse to dehalfop myself. If you really '
'want me dehalfopped, tell me to op you and then '
'dehalfop me yourself.', Raise=True)
if not nicks:
nicks = [msg.nick]
self._sendMsg(irc, ircmsgs.dehalfops(channel, nicks))
dehalfop = wrap(dehalfop, [('checkChannelCapability', 'halfop'),
('haveOp', 'dehalfop someone'),
any('nickInChannel')])
# XXX We should respect the MODES part of an 005 here. Helper function
# material.
def devoice(self, irc, msg, args, channel, nicks):
"""[<channel>] [<nick> ...]
If you have the #channel,op capability, this will remove voice from all
the nicks given. If no nicks are given, removes voice from the person
sending the message.
"""
if irc.nick in nicks:
irc.error('I cowardly refuse to devoice myself. If you really '
'want me devoiced, tell me to op you and then devoice '
'me yourself.', Raise=True)
if not nicks:
nicks = [msg.nick]
self._sendMsg(irc, ircmsgs.devoices(channel, nicks))
devoice = wrap(devoice, [('checkChannelCapability', 'voice'),
('haveOp', 'devoice someone'),
any('nickInChannel')])
def cycle(self, irc, msg, args, channel):
"""[<channel>]
If you have the #channel,op capability, this will cause the bot to
"cycle", or PART and then JOIN the channel. <channel> is only necessary
if the message isn't sent in the channel itself.
"""
self._sendMsg(irc, ircmsgs.part(channel, msg.nick))
networkGroup = conf.supybot.networks.get(irc.network)
self._sendMsg(irc, networkGroup.channels.join(channel))
cycle = wrap(cycle, [('checkChannelCapability','op')])
def kick(self, irc, msg, args, channel, nick, reason):
"""[<channel>] <nick> [<reason>]
Kicks <nick> from <channel> for <reason>. If <reason> isn't given,
uses the nick of the person making the command as the reason.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
if ircutils.strEqual(nick, irc.nick):
irc.error('I cowardly refuse to kick myself.', Raise=True)
if not reason:
reason = msg.nick
kicklen = irc.state.supported.get('kicklen', sys.maxint)
if len(reason) > kicklen:
irc.error('The reason you gave is longer than the allowed '
'length for a KICK reason on this server.')
return
self._sendMsg(irc, ircmsgs.kick(channel, nick, reason))
kick = wrap(kick, [('checkChannelCapability', 'op'),
('haveOp', 'kick someone'),
'nickInChannel',
additional('text')])
def kban(self, irc, msg, args,
channel, optlist, bannedNick, expiry, reason):
"""[<channel>] [--{exact,nick,user,host}] <nick> [<seconds>] [<reason>]
If you have the #channel,op capability, this will kickban <nick> for
as many seconds as you specify, or else (if you specify 0 seconds or
don't specify a number of seconds) it will ban the person indefinitely.
--exact bans only the exact hostmask; --nick bans just the nick;
--user bans just the user, and --host bans just the host. You can
combine these options as you choose. <reason> is a reason to give for
the kick.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
# Check that they're not trying to make us kickban ourself.
self.log.debug('In kban')
if not irc.isNick(bannedNick):
self.log.warning('%s tried to kban a non nick: %s',
utils.quoted(msg.prefix),
utils.quoted(bannedNick))
raise callbacks.ArgumentError
elif bannedNick == irc.nick:
self.log.warning('%s tried to make me kban myself.',
utils.quoted(msg.prefix))
irc.error('I cowardly refuse to kickban myself.')
return
if not reason:
reason = msg.nick
try:
bannedHostmask = irc.state.nickToHostmask(bannedNick)
except KeyError:
irc.error('I haven\'t seen %s.' % bannedNick, Raise=True)
capability = ircdb.makeChannelCapability(channel, 'op')
def makeBanmask(bannedHostmask, options):
(nick, user, host) = ircutils.splitHostmask(bannedHostmask)
self.log.debug('*** nick: %s' % nick)
self.log.debug('*** user: %s' % user)
self.log.debug('*** host: %s' % host)
bnick = '*'
buser = '*'
bhost = '*'
for option in options:
if option == 'nick':
bnick = nick
elif option == 'user':
buser = user
elif option == 'host':
bhost = host
elif option == 'exact':
(bnick, buser, bhost) = \
ircutils.splitHostmask(bannedHostmask)
return ircutils.joinHostmask(bnick, buser, bhost)
if optlist:
banmask = makeBanmask(bannedHostmask, [o[0] for o in optlist])
else:
banmask = makeBanmask(bannedHostmask,
self.registryValue('banmask', channel))
# Check (again) that they're not trying to make us kickban ourself.
if ircutils.hostmaskPatternEqual(banmask, irc.prefix):
if ircutils.hostmaskPatternEqual(banmask, irc.prefix):
self.log.warning('%s tried to make me kban myself.',
utils.quoted(msg.prefix))
irc.error('I cowardly refuse to ban myself.')
return
else:
banmask = bannedHostmask
# Now, let's actually get to it. Check to make sure they have
# #channel,op and the bannee doesn't have #channel,op; or that the
# bannee and the banner are both the same person.
def doBan():
if irc.state.channels[channel].isOp(bannedNick):
irc.queueMsg(ircmsgs.deop(channel, bannedNick))
irc.queueMsg(ircmsgs.ban(channel, banmask))
irc.queueMsg(ircmsgs.kick(channel, bannedNick, reason))
if expiry > 0:
def f():
if channel in irc.state.channels and \
banmask in irc.state.channels[channel].bans:
irc.queueMsg(ircmsgs.unban(channel, banmask))
schedule.addEvent(f, expiry)
if bannedNick == msg.nick:
doBan()
elif ircdb.checkCapability(msg.prefix, capability):
if ircdb.checkCapability(bannedHostmask, capability):
self.log.warning('%s tried to ban %s, but both have %s',
msg.prefix, utils.quoted(bannedHostmask),
capability)
irc.error('%s has %s too, you can\'t ban him/her/it.' %
(bannedNick, capability))
else:
doBan()
else:
self.log.warning('%s attempted kban without %s',
utils.quoted(msg.prefix), capability)
irc.errorNoCapability(capability)
exact,nick,user,host
kban = wrap(kban,
[('checkChannelCapability', 'op'),
getopts({'exact':'', 'nick':'', 'user':'', 'host':''}),
('haveOp', 'kick or ban someone'),
'nickInChannel',
optional('expiry', 0),
additional('text')])
def unban(self, irc, msg, args, channel, hostmask):
"""[<channel>] [<hostmask>]
Unbans <hostmask> on <channel>. If <hostmask> is not given, unbans
any hostmask currently banned on <channel> that matches your current
hostmask. Especially useful for unbanning yourself when you get
unexpectedly (or accidentally) banned from the channel. <channel> is
only necessary if the message isn't sent in the channel itself.
"""
if hostmask:
self._sendMsg(irc, ircmsgs.unban(channel, hostmask))
else:
bans = []
for banmask in irc.state.channels[channel].bans:
if ircutils.hostmaskPatternEqual(banmask, msg.prefix):
bans.append(banmask)
if bans:
irc.queueMsg(ircmsgs.unbans(channel, bans))
irc.replySuccess('All bans on %s matching %s '
'have been removed.' % (channel, msg.prefix))
else:
irc.error('No bans matching %s were found on %s.' %
(msg.prefix, channel))
unban = wrap(unban, [('checkChannelCapability', 'op'),
('haveOp', 'unban someone'),
additional('hostmask')])
def invite(self, irc, msg, args, channel, nick):
"""[<channel>] <nick>
If you have the #channel,op capability, this will invite <nick>
to join <channel>. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
self._sendMsg(irc, ircmsgs.invite(nick or msg.nick, channel))
self.invites[(irc.getRealIrc(), ircutils.toLower(nick))] = irc
invite = wrap(invite, [('checkChannelCapability', 'op'),
('haveOp', 'invite someone'),
additional('nick')])
def do341(self, irc, msg):
(_, nick, channel) = msg.args
nick = ircutils.toLower(nick)
replyIrc = self.invites.pop((irc, nick), None)
if replyIrc is not None:
self.log.info('Inviting %s to %s by command of %s.',
nick, channel, replyIrc.msg.prefix)
replyIrc.replySuccess()
else:
self.log.info('Inviting %s to %s.', nick, channel)
def do443(self, irc, msg):
(_, nick, channel, _) = msg.args
nick = ircutils.toLower(nick)
replyIrc = self.invites.pop((irc, nick), None)
if replyIrc is not None:
replyIrc.error('%s is already in %s.' % (nick, channel))
def do401(self, irc, msg):
nick = msg.args[1]
nick = ircutils.toLower(nick)
replyIrc = self.invites.pop((irc, nick), None)
if replyIrc is not None:
replyIrc.error('There is no %s on this network.' % nick)
def do504(self, irc, msg):
nick = msg.args[1]
nick = ircutils.toLower(nick)
replyIrc = self.invites.pop((irc, nick), None)
if replyirc is not None:
replyIrc.error('There is no %s on this server.' % nick)
def lobotomize(self, irc, msg, args, channel):
"""[<channel>]
If you have the #channel,op capability, this will "lobotomize" the
bot, making it silent and unanswering to all requests made in the
channel. <channel> is only necessary if the message isn't sent in the
channel itself.
"""
c = ircdb.channels.getChannel(channel)
c.lobotomized = True
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
lobotomize = wrap(lobotomize, [('checkChannelCapability', 'op')])
def unlobotomize(self, irc, msg, args, channel):
"""[<channel>]
If you have the #channel,op capability, this will unlobotomize the bot,
making it respond to requests made in the channel again.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
c = ircdb.channels.getChannel(channel)
c.lobotomized = False
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
unlobotomize = wrap(unlobotomize, [('checkChannelCapability', 'op')])
def permban(self, irc, msg, args, channel, banmask, expires):
"""[<channel>] <nick|hostmask> [<expires>]
If you have the #channel,op capability, this will effect a permanent
(persistent) ban from interacting with the bot on the given <hostmask>
(or the current hostmask associated with <nick>. Other plugins may
enforce this ban by actually banning users with matching hostmasks when
they join. <expires> is an optional argument specifying when (in
"seconds from now") the ban should expire; if none is given, the ban
will never automatically expire. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
c = ircdb.channels.getChannel(channel)
c.addBan(banmask, expires)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
permban = wrap(permban, [('checkChannelCapability', 'op'),
'hostmask',
additional('expiry', 0)])
def unpermban(self, irc, msg, args, channel, banmask):
"""[<channel>] <hostmask>
If you have the #channel,op capability, this will remove the permanent
ban on <hostmask>. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
c = ircdb.channels.getChannel(channel)
c.removeBan(banmask)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
unpermban = wrap(unpermban, [('checkChannelCapability', 'op'), 'hostmask'])
def permbans(self, irc, msg, args, channel):
"""[<channel>]
If you have the #channel,op capability, this will show you the
current bans on #channel.
"""
# XXX Add the expirations.
c = ircdb.channels.getChannel(channel)
if c.bans:
irc.reply(utils.commaAndify(map(utils.dqrepr, c.bans)))
else:
irc.reply('There are currently no permanent bans on %s' % channel)
permbans = wrap(permbans, [('checkChannelCapability', 'op')])
def ignore(self, irc, msg, args, channel, banmask, expires):
"""[<channel>] <nick|hostmask> [<expires>]
If you have the #channel,op capability, this will set a permanent
(persistent) ignore on <hostmask> or the hostmask currently associated
with <nick>. <expires> is an optional argument specifying when (in
"seconds from now") the ignore will expire; if it isn't given, the
ignore will never automatically expire. <channel> is only necessary
if the message isn't sent in the channel itself.
"""
c = ircdb.channels.getChannel(channel)
c.addIgnore(banmask, expires)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
ignore = wrap(ignore, [('checkChannelCapability', 'op'),
'hostmask', additional('expiry', 0)])
def unignore(self, irc, msg, args, channel, banmask):
"""[<channel>] <hostmask>
If you have the #channel,op capability, this will remove the permanent
ignore on <hostmask> in the channel. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
c = ircdb.channels.getChannel(channel)
c.removeIgnore(banmask)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
unignore = wrap(unignore, [('checkChannelCapability', 'op'), 'hostmask'])
def ignores(self, irc, msg, args, channel):
"""[<channel>]
Lists the hostmasks that the bot is ignoring on the given channel.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
# XXX Add the expirations.
c = ircdb.channels.getChannel(channel)
if len(c.ignores) == 0:
s = 'I\'m not currently ignoring any hostmasks in %s' % \
utils.quoted(channel)
irc.reply(s)
else:
L = sorted(c.ignores)
irc.reply(utils.commaAndify(imap(repr, L)))
ignores = wrap(ignores, [('checkChannelCapability', 'op')])
def addcapability(self, irc, msg, args, channel, user, capabilities):
"""[<channel>] <nick|username> <capability> [<capability> ...]
If you have the #channel,op capability, this will give the user
<name> (or the user to whom <nick> maps)
the capability <capability> in the channel. <channel> is only necessary
if the message isn't sent in the channel itself.
"""
for c in capabilities.split():
c = ircdb.makeChannelCapability(channel, c)
user.addCapability(c)
ircdb.users.setUser(user)
irc.replySuccess()
addcapability = wrap(addcapability, [('checkChannelCapability', 'op'),
'otherUser', 'capability'])
def removecapability(self, irc, msg, args, channel, user, capabilities):
"""[<channel>] <name|hostmask> <capability> [<capability> ...]
If you have the #channel,op capability, this will take from the user
currently identified as <name> (or the user to whom <hostmask> maps)
the capability <capability> in the channel. <channel> is only necessary
if the message isn't sent in the channel itself.
"""
fail = []
for c in capabilities.split():
cap = ircdb.makeChannelCapability(channel, c)
try:
user.removeCapability(cap)
except KeyError:
fail.append(c)
ircdb.users.setUser(user)
if fail:
irc.error('That user didn\'t have the %s %s.' %
(utils.commaAndify(fail),
utils.pluralize('capability', len(fail))), Raise=True)
irc.replySuccess()
removecapability = wrap(removecapability,
[('checkChannelCapability', 'op'),
'otherUser', 'capability'])
# XXX This needs to be fix0red to be like Owner.defaultcapability. Or
# something else. This is a horrible interface.
def setdefaultcapability(self, irc, msg, args, channel, v):
"""[<channel>] {True|False}
If you have the #channel,op capability, this will set the default
response to non-power-related (that is, not {op, halfop, voice}
capabilities to be the value you give. <channel> is only necessary if
the message isn't sent in the channel itself.
"""
c = ircdb.channels.getChannel(channel)
if v:
c.setDefaultCapability(True)
else:
c.setDefaultCapability(False)
ircdb.channels.setChannel(channel, c)
irc.replySuccess()
setdefaultcapability = wrap(setdefaultcapability,
[('checkChannelCapability', 'op'), 'boolean'])
def setcapability(self, irc, msg, args, channel, capabilities):
"""[<channel>] <capability> [<capability> ...]
If you have the #channel,op capability, this will add the channel
capability <capability> for all users in the channel. <channel> is
only necessary if the message isn't sent in the channel itself.
"""
chan = ircdb.channels.getChannel(channel)
for c in capabilities:
chan.addCapability(c)
ircdb.channels.setChannel(channel, chan)
irc.replySuccess()
setcapability = wrap(setcapability,
[('checkChannelCapability', 'op'), many('capability')])
def unsetcapability(self, irc, msg, args, channel, capabilities):
"""[<channel>] <capability> [<capability> ...]
If you have the #channel,op capability, this will unset the channel
capability <capability> so each user's specific capability or the
channel default capability will take precedence. <channel> is only
necessary if the message isn't sent in the channel itself.
"""
chan = ircdb.channels.getChannel(channel)
fail = []
for c in capabilities:
try:
chan.removeCapability(c)
except KeyError:
fail.append(c)
ircdb.channels.setChannel(channel, chan)
if fail:
irc.error('I do not know about the %s %s.' %
(utils.commaAndify(fail),
utils.pluralize('capability', len(fail))), Raise=True)
irc.replySuccess()
unsetcapability = wrap(unsetcapability,
[('checkChannelCapability', 'op'),
many('capability')])
def capabilities(self, irc, msg, args, channel):
"""[<channel>]
Returns the capabilities present on the <channel>. <channel> is only
necessary if the message isn't sent in the channel itself.
"""
c = ircdb.channels.getChannel(channel)
L = sorted(c.capabilities)
irc.reply(' '.join(L))
capabilities = wrap(capabilities, ['channel'])
def disable(self, irc, msg, args, channel, plugin, command):
"""[<channel>] [<plugin>] [<command>]
If you have the #channel,op capability, this will disable the <command>
in <channel>. If <plugin> is provided, <command> will be disabled only
for that plugin. If only <plugin> is provided, all commands in the
given plugin will be disabled. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
chan = ircdb.channels.getChannel(channel)
failMsg = ''
if plugin:
s = '-%s' % plugin.name()
if command:
if plugin.isCommand(command):
s = '-%s.%s' % (plugin.name(), command)
else:
failMsg = 'The %s plugin does not have a command called %s.'\
% (plugin.name(), command)
elif command:
# findCallbackForCommand
if irc.findCallbackForCommand(command):
s = '-%s' % command
else:
failMsg = 'No plugin or command named %s could be found.'\
% (command)
else:
raise callbacks.ArgumentError
if failMsg:
irc.error(failMsg)
else:
chan.addCapability(s)
ircdb.channels.setChannel(channel, chan)
irc.replySuccess()
disable = wrap(disable, [('checkChannelCapability', 'op'),
optional(('plugin', False)),
additional('commandName')])
def enable(self, irc, msg, args, channel, plugin, command):
"""[<channel>] [<plugin>] [<command>]
If you have the #channel,op capability, this will enable the <command>
in <channel> if it has been disabled. If <plugin> is provided,
<command> will be enabled only for that plugin. If only <plugin> is
provided, all commands in the given plugin will be enabled. <channel>
is only necessary if the message isn't sent in the channel itself.
"""
chan = ircdb.channels.getChannel(channel)
failMsg = ''
if plugin:
s = '-%s' % plugin.name()
if command:
if plugin.isCommand(command):
s = '-%s.%s' % (plugin.name(), command)
else:
failMsg = 'The %s plugin does not have a command called %s.'\
% (plugin.name(), command)
elif command:
# findCallbackForCommand
if irc.findCallbackForCommand(command):
s = '-%s' % command
else:
failMsg = 'No plugin or command named %s could be found.'\
% (command)
else:
raise callbacks.ArgumentError
if failMsg:
irc.error(failMsg)
else:
fail = []
try:
chan.removeCapability(s)
except KeyError:
fail.append(s)
ircdb.channels.setChannel(channel, chan)
if fail:
irc.error('%s was not disabled.' % s[1:])
else:
irc.replySuccess()
enable = wrap(enable, [('checkChannelCapability', 'op'),
optional(('plugin', False)),
additional('commandName')])
def lobotomies(self, irc, msg, args):
"""takes no arguments
Returns the channels in which this bot is lobotomized.
"""
L = []
for (channel, c) in ircdb.channels.iteritems():
if c.lobotomized:
L.append(channel)
if L:
L.sort()
s = 'I\'m currently lobotomized in %s.' % utils.commaAndify(L)
irc.reply(s)
else:
irc.reply('I\'m not currently lobotomized in any channels.')
def nicks(self, irc, msg, args, channel):
"""[<channel>]
Returns the nicks in <channel>. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
L = list(irc.state.channels[channel].users)
utils.sortBy(str.lower, L)
irc.reply(utils.commaAndify(L))
nicks = wrap(nicks, ['inChannel']) # XXX Check that the caller is in chan.
def alertOps(self, irc, channel, s, frm=None):
"""Internal message for notifying all the #channel,ops in a channel of
a given situation."""
capability = ircdb.makeChannelCapability(channel, 'op')
s = 'Alert to all %s ops: %s' % (channel, s)
if frm is not None:
s += ' (from %s)' % frm
for nick in irc.state.channels[channel].users:
hostmask = irc.state.nickToHostmask(nick)
if ircdb.checkCapability(hostmask, capability):
irc.reply(s, to=nick, private=True)
def alert(self, irc, msg, args, channel, text):
"""[<channel>] <text>
Sends <text> to all the users in <channel> who have the <channel>,op
capability.
"""
self.alertOps(irc, channel, text, frm=msg.nick)
alert = wrap(alert, [('checkChannelCapability', 'op'), 'text'])
Class = Channel
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

190
plugins/Channel/test.py Normal file
View File

@ -0,0 +1,190 @@
###
# 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.conf as conf
import supybot.ircdb as ircdb
import supybot.ircmsgs as ircmsgs
class ChannelTestCase(ChannelPluginTestCase):
plugins = ('Channel', 'User')
def setUp(self):
super(ChannelTestCase, self).setUp()
self.irc.state.channels[self.channel].addUser('foo')
self.irc.state.channels[self.channel].addUser('bar')
def testLobotomies(self):
self.assertRegexp('lobotomies', 'not.*any')
## def testCapabilities(self):
## self.prefix = 'foo!bar@baz'
## self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar',
## prefix=self.prefix))
## u = ircdb.users.getUser(0)
## u.addCapability('%s.op' % self.channel)
## ircdb.users.setUser(u)
## self.assertNotError(' ')
## self.assertResponse('user capabilities foo', '[]')
## self.assertNotError('channel addcapability foo op')
## self.assertRegexp('channel capabilities foo', 'op')
## self.assertNotError('channel removecapability foo op')
## self.assertResponse('user capabilities foo', '[]')
def testCapabilities(self):
self.assertNotError('channel capabilities')
self.assertNotError('channel setcapability -foo')
self.assertNotError('channel unsetcapability -foo')
self.assertError('channel unsetcapability -foo')
self.assertNotError('channel setcapability -foo bar baz')
self.assertRegexp('channel capabilities', 'baz')
self.assertNotError('channel unsetcapability -foo baz')
self.assertError('channel unsetcapability baz')
def testEnableDisable(self):
self.assertNotRegexp('channel capabilities', '-Channel')
self.assertError('channel enable channel')
self.assertNotError('channel disable channel')
self.assertRegexp('channel capabilities', '-Channel')
self.assertNotError('channel enable channel')
self.assertNotRegexp('channel capabilities', '-Channel')
self.assertNotError('channel disable channel nicks')
self.assertRegexp('channel capabilities', '-Channel.nicks')
self.assertNotError('channel enable channel nicks')
self.assertNotRegexp('channel capabilities', '-Channel.nicks')
self.assertNotRegexp('channel capabilities', 'nicks')
self.assertNotError('channel disable nicks')
self.assertRegexp('channel capabilities', 'nicks')
self.assertNotError('channel enable nicks')
self.assertError('channel disable invalidPlugin')
self.assertError('channel disable channel invalidCommand')
def testUnban(self):
self.assertError('unban foo!bar@baz')
self.irc.feedMsg(ircmsgs.op(self.channel, self.nick))
m = self.getMsg('unban foo!bar@baz')
self.assertEqual(m.command, 'MODE')
self.assertEqual(m.args, (self.channel, '-b', 'foo!bar@baz'))
self.assertNoResponse(' ', 2)
def testErrorsWithoutOps(self):
for s in 'op deop halfop dehalfop voice devoice kick invite'.split():
self.assertError('%s foo' % s)
self.irc.feedMsg(ircmsgs.op(self.channel, self.nick))
self.assertNotError('%s foo' % s)
self.irc.feedMsg(ircmsgs.deop(self.channel, self.nick))
def testWontDeItself(self):
for s in 'deop dehalfop devoice'.split():
self.irc.feedMsg(ircmsgs.op(self.channel, self.nick))
self.assertError('%s %s' % (s, self.nick))
def testOp(self):
self.assertError('op')
self.irc.feedMsg(ircmsgs.op(self.channel, self.nick))
self.assertNotError('op')
m = self.getMsg('op foo')
self.failUnless(m.command == 'MODE' and
m.args == (self.channel, '+o', 'foo'))
m = self.getMsg('op foo bar')
self.failUnless(m.command == 'MODE' and
m.args == (self.channel, '+oo', 'foo', 'bar'))
def testHalfOp(self):
self.assertError('halfop')
self.irc.feedMsg(ircmsgs.op(self.channel, self.nick))
self.assertNotError('halfop')
m = self.getMsg('halfop foo')
self.failUnless(m.command == 'MODE' and
m.args == (self.channel, '+h', 'foo'))
m = self.getMsg('halfop foo bar')
self.failUnless(m.command == 'MODE' and
m.args == (self.channel, '+hh', 'foo', 'bar'))
def testVoice(self):
self.assertError('voice')
self.irc.feedMsg(ircmsgs.op(self.channel, self.nick))
self.assertNotError('voice')
m = self.getMsg('voice foo')
self.failUnless(m.command == 'MODE' and
m.args == (self.channel, '+v', 'foo'))
m = self.getMsg('voice foo bar')
self.failUnless(m.command == 'MODE' and
m.args == (self.channel, '+vv', 'foo', 'bar'))
def assertBan(self, query, hostmask, **kwargs):
m = self.getMsg(query, **kwargs)
self.assertEqual(m, ircmsgs.ban(self.channel, hostmask))
m = self.getMsg(' ')
self.assertEqual(m.command, 'KICK')
## def testKban(self):
## self.irc.prefix = 'something!else@somehwere.else'
## self.irc.nick = 'something'
## self.irc.feedMsg(ircmsgs.join(self.channel,
## prefix='foobar!user@host.domain.tld'))
## self.assertError('kban foobar')
## self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
## self.assertError('kban foobar -1')
## self.assertBan('kban foobar', '*!*@*.domain.tld')
## self.assertBan('kban --exact foobar', 'foobar!user@host.domain.tld')
## self.assertBan('kban --host foobar', '*!*@host.domain.tld')
## self.assertBan('kban --user foobar', '*!user@*')
## self.assertBan('kban --nick foobar', 'foobar!*@*')
## self.assertBan('kban --nick --user foobar', 'foobar!user@*')
## self.assertBan('kban --nick --host foobar', 'foobar!*@host.domain.tld')
## self.assertBan('kban --user --host foobar', '*!user@host.domain.tld')
## self.assertBan('kban --nick --user --host foobar',
## 'foobar!user@host.domain.tld')
## self.assertNotRegexp('kban adlkfajsdlfkjsd', 'KeyError')
## self.assertNotRegexp('kban foobar time', 'ValueError')
## self.assertError('kban %s' % self.irc.nick)
def testPermban(self):
self.assertNotError('permban foo!bar@baz')
self.assertNotError('unpermban foo!bar@baz')
orig = conf.supybot.protocols.irc.strictRfc()
try:
conf.supybot.protocols.irc.strictRfc.setValue(True)
# something wonky is going on here. irc.error (src/Channel.py|449)
# is being called but the assert is failing
self.assertError('permban not!a.hostmask')
self.assertNotRegexp('permban not!a.hostmask', 'KeyError')
finally:
conf.supybot.protocols.irc.strictRfc.setValue(orig)
def testIgnore(self):
self.assertNotError('Channel ignore foo!bar@baz')
self.assertResponse('Channel ignores', "'foo!bar@baz'")
self.assertNotError('Channel unignore foo!bar@baz')
self.assertError('permban not!a.hostmask')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -0,0 +1,47 @@
###
# Copyright (c) 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 supybot
__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.
# Backwards compatibility.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure

48
plugins/Config/config.py Normal file
View File

@ -0,0 +1,48 @@
###
# Copyright (c) 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 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('Config', True)
Config = conf.registerPlugin('Config')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(Config, 'someConfigVariableName',
# registry.Boolean(False, """Help for someConfigVariableName."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

270
plugins/Config/plugin.py Normal file
View File

@ -0,0 +1,270 @@
###
# 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.
###
"""
Handles configuration of the bot while it is running.
"""
import os
import getopt
import signal
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.ircutils as ircutils
import supybot.registry as registry
import supybot.callbacks as callbacks
###
# Now, to setup the registry.
###
def getWrapper(name):
parts = registry.split(name)
if not parts or parts[0] not in ('supybot', 'users'):
raise InvalidRegistryName, name
group = getattr(conf, parts.pop(0))
while parts:
try:
group = group.get(parts.pop(0))
# We'll catch registry.InvalidRegistryName and re-raise it here so
# that we have a useful error message for the user.
except (registry.NonExistentRegistryEntry,
registry.InvalidRegistryName):
raise registry.InvalidRegistryName, name
return group
def getCapability(name):
capability = 'owner' # Default to requiring the owner capability.
parts = registry.split(name)
while parts:
part = parts.pop()
if ircutils.isChannel(part):
# If a registry value has a channel in it, it requires a channel.op
# capability, or so we assume. We'll see if we're proven wrong.
capability = ircdb.makeChannelCapability(part, 'op')
### Do more later, for specific capabilities/sections.
return capability
def _reload():
ircdb.users.reload()
ircdb.channels.reload()
registry.open(world.registryFilename)
def _hupHandler(sig, frame):
log.info('Received SIGHUP, reloading configuration.')
_reload()
if os.name == 'posix':
signal.signal(signal.SIGHUP, _hupHandler)
def getConfigVar(irc, msg, args, state):
name = args[0]
if name.startswith('conf.'):
name = name[5:]
if not name.startswith('supybot') and not name.startswith('users'):
name = 'supybot.' + name
try:
group = getWrapper(name)
state.args.append(group)
del args[0]
except registry.InvalidRegistryName, e:
irc.errorInvalid('configuration variable', str(e))
addConverter('configVar', getConfigVar)
class Config(callbacks.Privmsg):
def callCommand(self, name, irc, msg, *L, **kwargs):
try:
super(Config, self).callCommand(name, irc, msg, *L, **kwargs)
except registry.InvalidRegistryValue, e:
irc.error(str(e))
def _list(self, group):
L = []
for (vname, v) in group._children.iteritems():
if hasattr(group, 'channelValue') and group.channelValue and \
ircutils.isChannel(vname) and not v._children:
continue
if hasattr(v, 'channelValue') and v.channelValue:
vname = '#' + vname
if v._added and not all(ircutils.isChannel, v._added):
vname = '@' + vname
L.append(vname)
utils.sortBy(str.lower, L)
return L
def list(self, irc, msg, args, group):
"""<group>
Returns the configuration variables available under the given
configuration <group>. If a variable has values under it, it is
preceded by an '@' sign. If a variable is a 'ChannelValue', that is,
it can be separately configured for each channel using the 'channel'
command in this plugin, it is preceded by an '#' sign.
"""
L = self._list(group)
if L:
irc.reply(utils.commaAndify(L))
else:
irc.error('There don\'t seem to be any values in %s.' % group._name)
list = wrap(list, ['configVar'])
def search(self, irc, msg, args, word):
"""<word>
Searches for <word> in the current configuration variables.
"""
L = []
for (name, _) in conf.supybot.getValues(getChildren=True):
if word in name.lower():
possibleChannel = registry.split(name)[-1]
if not ircutils.isChannel(possibleChannel):
L.append(name)
if L:
irc.reply(utils.commaAndify(L))
else:
irc.reply('There were no matching configuration variables.')
search = wrap(search, ['lowered']) # XXX compose with withoutSpaces?
def _getValue(self, irc, msg, group):
value = str(group) or ' '
if hasattr(group, 'value'):
if not group._private:
irc.reply(value)
else:
capability = getCapability(group._name)
if ircdb.checkCapability(msg.prefix, capability):
irc.reply(value, private=True)
else:
irc.errorNoCapability(capability)
else:
irc.error('That registry variable has no value. Use the list '
'command in this plugin to see what variables are '
'available in this group.')
def _setValue(self, irc, msg, group, value):
capability = getCapability(group._name)
if ircdb.checkCapability(msg.prefix, capability):
# I think callCommand catches exceptions here. Should it?
group.set(value)
irc.replySuccess()
else:
irc.errorNoCapability(capability)
def channel(self, irc, msg, args, channel, group, value):
"""[<channel>] <name> [<value>]
If <value> is given, sets the channel configuration variable for <name>
to <value> for <channel>. Otherwise, returns the current channel
configuration value of <name>. <channel> is only necessary if the
message isn't sent in the channel itself."""
if not group.channelValue:
irc.error('That configuration variable is not a channel-specific '
'configuration variable.')
return
group = group.get(channel)
if value is not None:
self._setValue(irc, msg, group, value)
else:
self._getValue(irc, msg, group)
channel = wrap(channel, ['channel', 'configVar', additional('text')])
def config(self, irc, msg, args, group, value):
"""<name> [<value>]
If <value> is given, sets the value of <name> to <value>. Otherwise,
returns the current value of <name>. You may omit the leading
"supybot." in the name if you so choose.
"""
if value is not None:
self._setValue(irc, msg, group, value)
else:
self._getValue(irc, msg, group)
config = wrap(config, ['configVar', additional('text')])
def help(self, irc, msg, args, group):
"""<name>
Returns the description of the configuration variable <name>.
"""
if hasattr(group, '_help'):
s = group.help()
if s:
if hasattr(group, 'value') and not group._private:
s += ' (Current value: %s)' % group
irc.reply(s)
else:
irc.reply('That configuration group exists, but seems to have '
'no help. Try "config list %s" to see if it has '
'any children values.')
else:
irc.error('%s has no help.' % name)
help = wrap(help, ['configVar'])
def default(self, irc, msg, args, group):
"""<name>
Returns the default value of the configuration variable <name>.
"""
v = group.__class__(group._default, '')
irc.reply(str(v))
default = wrap(default, ['configVar'])
def reload(self, irc, msg, args):
"""takes no arguments
Reloads the various configuration files (user database, channel
database, registry, etc.).
"""
_reload() # This was factored out for SIGHUP handling.
irc.replySuccess()
reload = wrap(reload, [('checkCapability', 'owner')])
def export(self, irc, msg, args, filename):
"""<filename>
Exports the public variables of your configuration to <filename>.
If you want to show someone your configuration file, but you don't
want that person to be able to see things like passwords, etc., this
command will export a "sanitized" configuration file suitable for
showing publicly.
"""
registry.close(conf.supybot, filename, private=False)
irc.replySuccess()
export = wrap(export, [('checkCapability', 'owner'), 'filename'])
Class = Config
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

83
plugins/Config/test.py Normal file
View File

@ -0,0 +1,83 @@
###
# 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.conf as conf
class ConfigTestCase(ChannelPluginTestCase):
# We add utilities so there's something in supybot.plugins.
plugins = ('Config', 'Ebay')
def testGet(self):
self.assertNotRegexp('config get supybot.reply', r'registry\.Group')
self.assertResponse('config supybot.protocols.irc.throttleTime', '0.0')
def testList(self):
self.assertError('config list asldfkj')
self.assertError('config list supybot.asdfkjsldf')
self.assertNotError('config list supybot')
self.assertNotError('config list supybot.replies')
self.assertRegexp('config list supybot', r'@plugins.*@replies.*@reply')
def testHelp(self):
self.assertError('config help alsdkfj')
self.assertError('config help supybot.alsdkfj')
self.assertNotError('config help supybot') # We tell the user to list.
self.assertNotError('config help supybot.plugins')
self.assertNotError('config help supybot.replies.success')
self.assertNotError('config help replies.success')
def testHelpDoesNotAssertionError(self):
self.assertNotRegexp('config help ' # Cont'd.
'supybot.commands.defaultPlugins.help',
'AssertionError')
def testHelpExhaustively(self):
L = conf.supybot.getValues(getChildren=True)
for (name, v) in L:
self.assertNotError('config help %s' % name)
def testSearch(self):
self.assertNotError('config search chars')
self.assertNotError('config channel reply.whenAddressedBy.chars @')
self.assertNotRegexp('config search chars', self.channel)
def testDefault(self):
self.assertNotError('config default '
'supybot.replies.genericNoCapability')
def testConfigErrors(self):
self.assertRegexp('config supybot.replies.', 'not a valid')
self.assertRegexp('config supybot.repl', 'not a valid')
self.assertRegexp('config supybot.reply.withNickPrefix 123',
'True or False')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

49
plugins/Misc/__init__.py Normal file
View File

@ -0,0 +1,49 @@
###
# Copyright (c) 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 supybot
__author__ = supybot.authors.jemfinch
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {
supybot.authors.skorobeus: ['contributors'],
}
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Backwards compatibility.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure

63
plugins/Misc/config.py Normal file
View File

@ -0,0 +1,63 @@
###
# Copyright (c) 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 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('Misc', True)
Misc = conf.registerPlugin('Misc')
conf.registerGlobalValue(Misc, 'listPrivatePlugins',
registry.Boolean(True, """Determines whether the bot will list private
plugins with the list command if given the --private switch. If this is
disabled, non-owner users should be unable to see what private plugins
are loaded."""))
conf.registerGlobalValue(Misc, 'timestampFormat',
registry.String('[%H:%M:%S]', """Determines the format string for
timestamps in the Misc.last command. Refer to the Python documentation
for the time module to see what formats are accepted. If you set this
variable to the empty string, the timestamp will not be shown."""))
conf.registerGroup(Misc, 'last')
conf.registerGroup(Misc.last, 'nested')
conf.registerChannelValue(Misc.last.nested,
'includeTimestamp', registry.Boolean(False, """Determines whether or not
the timestamp will be included in the output of last when it is part of a
nested command"""))
conf.registerChannelValue(Misc.last.nested,
'includeNick', registry.Boolean(False, """Determines whether or not the
nick will be included in the output of last when it is part of a nested
command"""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

597
plugins/Misc/plugin.py Normal file
View File

@ -0,0 +1,597 @@
###
# 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.
###
"""
Miscellaneous commands.
"""
import supybot
import supybot.fix as fix
import os
import sys
import time
from itertools import imap, ifilter
import supybot.log as log
import supybot.conf as conf
import supybot.utils as utils
import supybot.world as world
from supybot.commands import *
import supybot.ircdb as ircdb
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.webutils as webutils
import supybot.callbacks as callbacks
class Misc(callbacks.Privmsg):
def __init__(self):
super(Misc, self).__init__()
self.invalidCommands = ircutils.FloodQueue(60)
def callPrecedence(self, irc):
return ([cb for cb in irc.callbacks if cb is not self], [])
def invalidCommand(self, irc, msg, tokens):
assert not msg.repliedTo, 'repliedTo msg in Misc.invalidCommand.'
assert self is irc.callbacks[-1], 'Misc isn\'t last callback.'
self.log.debug('Misc.invalidCommand called (tokens %s)', tokens)
# First, we check for invalidCommand floods. This is rightfully done
# here since this will be the last invalidCommand called, and thus it
# will only be called if this is *truly* an invalid command.
maximum = conf.supybot.abuse.flood.command.invalid.maximum()
self.invalidCommands.enqueue(msg)
if self.invalidCommands.len(msg) > maximum and \
not ircdb.checkCapability(msg.prefix, 'owner'):
punishment = conf.supybot.abuse.flood.command.invalid.punishment()
banmask = '*!%s@%s' % (msg.user, msg.host)
self.log.info('Ignoring %s for %s seconds due to an apparent '
'invalid command flood.', banmask, punishment)
if tokens and tokens[0] == 'Error:':
self.log.warning('Apparent error loop with another Supybot '
'observed at %s. Consider ignoring this bot '
'permanently.', log.timestamp())
ircdb.ignores.add(banmask, time.time() + punishment)
irc.reply('You\'ve given me %s invalid commands within the last '
'minute; I\'m now ignoring you for %s.' %
(maximum, utils.timeElapsed(punishment, seconds=False)))
return
# Now, for normal handling.
channel = msg.args[0]
if conf.get(conf.supybot.reply.whenNotCommand, channel):
command = tokens and tokens[0] or ''
irc.errorInvalid('command', command, repr=False)
else:
if tokens:
# echo [] will get us an empty token set, but there's no need
# to log this in that case anyway, it being a nested command.
self.log.info('Not replying to %s, not a command.' % tokens[0])
if not isinstance(irc.irc, irclib.Irc):
bracketConfig = conf.supybot.commands.nested.brackets
brackets = conf.get(bracketConfig, channel)
if brackets:
(left, right) = brackets
irc.reply(left + ' '.join(tokens) + right)
else:
pass # Let's just do nothing, I can't think of better.
def list(self, irc, msg, args, optlist, cb):
"""[--private] [<plugin>]
Lists the commands available in the given plugin. If no plugin is
given, lists the public plugins available. If --private is given,
lists the private plugins.
"""
private = False
for (option, argument) in optlist:
if option == 'private':
private = True
if not self.registryValue('listPrivatePlugins') and \
not ircdb.checkCapability(msg.prefix, 'owner'):
irc.errorNoCapability('owner')
if not cb:
def isPublic(cb):
name = cb.name()
return conf.supybot.plugins.get(name).public()
names = [cb.name() for cb in irc.callbacks
if (private and not isPublic(cb)) or
(not private and isPublic(cb))]
names.sort()
if names:
irc.reply(utils.commaAndify(names))
else:
if private:
irc.reply('There are no private plugins.')
else:
irc.reply('There are no public plugins.')
else:
if isinstance(cb, callbacks.PrivmsgRegexp) or \
not isinstance(cb, callbacks.Privmsg):
irc.error('That plugin exists, but it has no commands. '
'You may wish to check if it has any useful '
'configuration variables with the command '
'"config list supybot.plugins.%s".' % cb.name())
else:
name = callbacks.canonicalName(cb.name())
commands = []
for s in dir(cb):
if cb.isCommand(s) and \
(s != name or cb._original) and \
s == callbacks.canonicalName(s):
method = getattr(cb, s)
if hasattr(method, '__doc__') and method.__doc__:
commands.append(s)
if commands:
commands.sort()
irc.reply(utils.commaAndify(commands))
else:
irc.error('That plugin exists, but it has no '
'commands with help.')
list = wrap(list, [getopts({'private':''}), additional('plugin')])
def apropos(self, irc, msg, args, s):
"""<string>
Searches for <string> in the commands currently offered by the bot,
returning a list of the commands containing that string.
"""
commands = {}
L = []
for cb in irc.callbacks:
if isinstance(cb, callbacks.Privmsg) and \
not isinstance(cb, callbacks.PrivmsgRegexp):
for attr in dir(cb):
if s in attr and cb.isCommand(attr):
if attr == callbacks.canonicalName(attr):
commands.setdefault(attr, []).append(cb.name())
for (key, names) in commands.iteritems():
if len(names) == 1:
L.append(key)
else:
for name in names:
L.append('%s %s' % (name, key))
if L:
L.sort()
irc.reply(utils.commaAndify(L))
else:
irc.reply('No appropriate commands were found.')
apropos = wrap(apropos, ['lowered'])
def help(self, irc, msg, args, cb, command):
"""[<plugin>] [<command>]
This command gives a useful description of what <command> does.
<plugin> is only necessary if the command is in more than one plugin.
"""
def getHelp(cb):
if hasattr(cb, 'isCommand'):
if cb.isCommand(command):
irc.reply(cb.getCommandHelp(command))
else:
irc.error('There is no %s command in the %s plugin.' %
(command, cb.name()))
else:
irc.error('The %s plugin exists, but has no commands.' %
cb.name())
if cb:
if command:
getHelp(cb)
else:
irc.reply(cb.getCommandHelp(cb.name()))
elif command:
cbs = irc.findCallbackForCommand(command)
if not cbs:
irc.error('There is no command %s.' % command)
elif len(cbs) > 1:
names = sorted([cb.name() for cb in cbs])
irc.error('That command exists in the %s plugins. '
'Please specify exactly which plugin command '
'you want help with.'% utils.commaAndify(names))
else:
getHelp(cbs[0])
else:
raise callbacks.ArgumentError
help = wrap(help, [optional(('plugin', False)), additional('commandName')])
def hostmask(self, irc, msg, args, nick):
"""[<nick>]
Returns the hostmask of <nick>. If <nick> isn't given, return the
hostmask of the person giving the command.
"""
if not nick:
nick = msg.nick
irc.reply(irc.state.nickToHostmask(nick))
hostmask = wrap(hostmask, [additional('seenNick')])
def version(self, irc, msg, args):
"""takes no arguments
Returns the version of the current bot.
"""
try:
newest = webutils.getUrl('http://supybot.sf.net/version.txt')
newest ='The newest version available online is %s.'%newest.strip()
except webutils.WebError, e:
self.log.warning('Couldn\'t get website version: %r', e)
newest = 'I couldn\'t fetch the newest version ' \
'from the Supybot website.'
s = 'The current (running) version of this Supybot is %s. %s' % \
(conf.version, newest)
irc.reply(s)
version = wrap(thread(version))
def source(self, irc, msg, args):
"""takes no arguments
Returns a URL saying where to get Supybot.
"""
irc.reply('My source is at http://supybot.sf.net/')
source = wrap(source)
def plugin(self, irc, msg, args, command):
"""<command>
Returns the plugin (or plugins) <command> is in. If this command is
nested, it returns only the plugin name(s). If given as a normal
command, it returns a more verbose, user-friendly response.
"""
cbs = callbacks.findCallbackForCommand(irc, command)
if cbs:
names = [cb.name() for cb in cbs]
names.sort()
plugin = utils.commaAndify(names)
if irc.nested:
irc.reply(utils.commaAndify(names))
else:
irc.reply('The %s command is available in the %s %s.' %
(utils.quoted(command), plugin,
utils.pluralize('plugin', len(names))))
else:
irc.error('There is no such command %s.' % command)
plugin = wrap(plugin, ['commandName'])
def author(self, irc, msg, args, cb):
"""<plugin>
Returns the author of <plugin>. This is the person you should talk to
if you have ideas, suggestions, or other comments about a given plugin.
"""
if cb is None:
irc.error('That plugin does not seem to be loaded.')
return
module = sys.modules[cb.__class__.__module__]
if hasattr(module, '__author__') and module.__author__:
irc.reply(utils.mungeEmailForWeb(str(module.__author__)))
else:
irc.reply('That plugin doesn\'t have an author that claims it.')
author = wrap(author, [('plugin')])
def more(self, irc, msg, args, nick):
"""[<nick>]
If the last command was truncated due to IRC message length
limitations, returns the next chunk of the result of the last command.
If <nick> is given, it takes the continuation of the last command from
<nick> instead of the person sending this message.
"""
userHostmask = msg.prefix.split('!', 1)[1]
if nick:
try:
(private, L) = self._mores[nick]
if not private:
self._mores[userHostmask] = L[:]
else:
irc.error('%s has no public mores.' % nick)
return
except KeyError:
irc.error('Sorry, I can\'t find any mores for %s' % nick)
return
try:
L = self._mores[userHostmask]
chunk = L.pop()
if L:
chunk += ' \x02(%s)\x0F' % \
utils.nItems('message', len(L), 'more')
irc.reply(chunk, True)
except KeyError:
irc.error('You haven\'t asked me a command; perhaps you want '
'to see someone else\'s more. To do so, call this '
'command with that person\'s nick.')
except IndexError:
irc.error('That\'s all, there is no more.')
more = wrap(more, [additional('seenNick')])
def _validLastMsg(self, msg):
return msg.prefix and \
msg.command == 'PRIVMSG' and \
ircutils.isChannel(msg.args[0])
def last(self, irc, msg, args, optlist):
"""[--{from,in,on,with,without,regexp} <value>] [--nolimit]
Returns the last message matching the given criteria. --from requires
a nick from whom the message came; --in requires a channel the message
was sent to; --on requires a network the message was sent on; --with
requires some string that had to be in the message; --regexp requires
a regular expression the message must match; --nolimit returns all
the messages that can be found. By default, the channel this command is
given in is searched.
"""
predicates = {}
nolimit = False
if ircutils.isChannel(msg.args[0]):
predicates['in'] = lambda m: ircutils.strEqual(m.args[0],
msg.args[0])
for (option, arg) in optlist:
if option == 'from':
def f(m, arg=arg):
return ircutils.hostmaskPatternEqual(arg, m.nick)
predicates['from'] = f
elif option == 'in':
def f(m, arg=arg):
return ircutils.strEqual(m.args[0], arg)
predicates['in'] = f
elif option == 'on':
def f(m, arg=arg):
return m.receivedOn == arg
predicates['on'] = f
elif option == 'with':
def f(m, arg=arg):
return arg.lower() in m.args[1].lower()
predicates.setdefault('with', []).append(f)
elif option == 'without':
def f(m, arg=arg):
return arg.lower() not in m.args[1].lower()
predicates.setdefault('without', []).append(f)
elif option == 'regexp':
def f(m, arg=arg):
if ircmsgs.isAction(m):
return arg.search(ircmsgs.unAction(m))
else:
return arg.search(m.args[1])
predicates.setdefault('regexp', []).append(f)
elif option == 'nolimit':
nolimit = True
iterable = ifilter(self._validLastMsg, reversed(irc.state.history))
iterable.next() # Drop the first message.
predicates = list(utils.flatten(predicates.itervalues()))
resp = []
if irc.nested and not \
self.registryValue('last.nested.includeTimestamp'):
tsf = None
else:
tsf = self.registryValue('timestampFormat')
if irc.nested and not self.registryValue('last.nested.includeNick'):
showNick = False
else:
showNick = True
for m in iterable:
for predicate in predicates:
if not predicate(m):
break
else:
if nolimit:
resp.append(ircmsgs.prettyPrint(m,
timestampFormat=tsf,
showNick=showNick))
else:
irc.reply(ircmsgs.prettyPrint(m,
timestampFormat=tsf,
showNick=showNick))
return
if not resp:
irc.error('I couldn\'t find a message matching that criteria in '
'my history of %s messages.' % len(irc.state.history))
else:
irc.reply(utils.commaAndify(resp))
last = wrap(last, [getopts({'nolimit': '',
'on': 'something',
'with': 'something',
'from': 'something',
'without': 'something',
'in': 'callerInGivenChannel',
'regexp': 'regexpMatcher',})])
def tell(self, irc, msg, args, target, text):
"""<nick> <text>
Tells the <nick> whatever <text> is. Use nested commands to your
benefit here.
"""
if target.lower() == 'me':
target = msg.nick
if ircutils.isChannel(target):
irc.error('Dude, just give the command. No need for the tell.')
return
if not ircutils.isNick(target):
irc.errorInvalid('nick', target)
if ircutils.nickEqual(target, irc.nick):
irc.error('You just told me, why should I tell myself?',Raise=True)
if target not in irc.state.nicksToHostmasks and \
not ircdb.checkCapability(msg.prefix, 'owner'):
# We'll let owners do this.
s = 'I haven\'t seen %s, I\'ll let you do the telling.' % target
irc.error(s, Raise=True)
if irc.action:
irc.action = False
text = '* %s %s' % (irc.nick, text)
s = '%s wants me to tell you: %s' % (msg.nick, text)
irc.reply(s, to=target, private=True)
tell = wrap(tell, ['something', 'text'])
def private(self, irc, msg, args, text):
"""<text>
Replies with <text> in private. Use nested commands to your benefit
here.
"""
irc.reply(text, private=True)
private = wrap(private, ['text'])
def action(self, irc, msg, args, text):
"""<text>
Replies with <text> as an action. use nested commands to your benefit
here.
"""
if text:
irc.reply(text, action=True)
else:
raise callbacks.ArgumentError
action = wrap(action, ['text'])
def notice(self, irc, msg, args, text):
"""<text>
Replies with <text> in a notice. Use nested commands to your benefit
here. If you want a private notice, nest the private command.
"""
irc.reply(text, notice=True)
notice = wrap(notice, ['text'])
def contributors(self, irc, msg, args, cb, nick):
"""<plugin> [<nick>]
Replies with a list of people who made contributions to a given plugin.
If <nick> is specified, that person's specific contributions will
be listed. Note: The <nick> is the part inside of the parentheses
in the people listing.
"""
def getShortName(authorInfo):
"""
Take an Authors object, and return only the name and nick values
in the format 'First Last (nick)'.
"""
return '%(name)s (%(nick)s)' % authorInfo.__dict__
def buildContributorsString(longList):
"""
Take a list of long names and turn it into :
shortname[, shortname and shortname].
"""
L = [getShortName(n) for n in longList]
return utils.commaAndify(L)
def sortAuthors():
"""
Sort the list of 'long names' based on the number of contributions
associated with each.
"""
L = module.__contributors__.items()
def negativeSecondElement(x):
return -len(x[1])
utils.sortBy(negativeSecondElement, L)
return [t[0] for t in L]
def buildPeopleString(module):
"""
Build the list of author + contributors (if any) for the requested
plugin.
"""
head = 'The %s plugin' % cb.name()
author = 'has not been claimed by an author'
conjunction = 'and'
contrib = 'has no contributors listed'
hasAuthor = False
hasContribs = False
if getattr(module, '__author__', None):
author = 'was written by %s' % \
utils.mungeEmailForWeb(str(module.__author__))
hasAuthor = True
if getattr(module, '__contributors__', None):
contribs = sortAuthors()
if hasAuthor:
try:
contribs.remove(module.__author__)
except ValueError:
pass
if contribs:
contrib = '%s %s contributed to it.' % \
(buildContributorsString(contribs),
utils.has(len(contribs)))
hasContribs = True
elif hasAuthor:
contrib = 'has no additional contributors listed'
if hasContribs and not hasAuthor:
conjunction = 'but'
return ' '.join([head, author, conjunction, contrib])
def buildPersonString(module):
"""
Build the list of contributions (if any) for the requested person
for the requested plugin
"""
isAuthor = False
authorInfo = getattr(supybot.authors, nick, None)
if not authorInfo:
return 'The nick specified (%s) is not a registered ' \
'contributor' % nick
fullName = utils.mungeEmailForWeb(str(authorInfo))
contributions = []
if hasattr(module, '__contributors__'):
if authorInfo not in module.__contributors__:
return 'The %s plugin does not have \'%s\' listed as a ' \
'contributor' % (cb.name(), nick)
contributions = module.__contributors__[authorInfo]
if getattr(module, '__author__', False) == authorInfo:
isAuthor = True
# XXX Partition needs moved to utils.
(nonCommands, commands) = fix.partition(lambda s: ' ' in s,
contributions)
results = []
if commands:
results.append(
'the %s %s' %(utils.commaAndify(commands),
utils.pluralize('command',len(commands))))
if nonCommands:
results.append('the %s' % utils.commaAndify(nonCommands))
if results and isAuthor:
return '%s wrote the %s plugin and also contributed %s' % \
(fullName, cb.name(), utils.commaAndify(results))
elif results and not isAuthor:
return '%s contributed %s to the %s plugin' % \
(fullName, utils.commaAndify(results), cb.name())
elif isAuthor and not results:
return '%s wrote the %s plugin' % (fullName, cb.name())
else:
return '%s has no listed contributions for the %s plugin %s' %\
(fullName, cb.name())
# First we need to check and see if the requested plugin is loaded
module = sys.modules[cb.__class__.__module__]
if not nick:
irc.reply(buildPeopleString(module))
else:
nick = ircutils.toLower(nick)
irc.reply(buildPersonString(module))
contributors = wrap(contributors, ['plugin', additional('nick')])
Class = Misc
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

285
plugins/Misc/test.py Normal file
View File

@ -0,0 +1,285 @@
###
# 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 *
class MiscTestCase(ChannelPluginTestCase):
plugins = ('Misc', 'Utilities', 'Gameknot', 'Anonymous', 'Dict', 'User')
def testAction(self):
self.assertAction('action moos', 'moos')
def testActionDoesNotAllowEmptyString(self):
self.assertHelp('action')
self.assertHelp('action ""')
def testReplyWhenNotCommand(self):
try:
original = str(conf.supybot.reply.whenNotCommand)
conf.supybot.reply.whenNotCommand.set('True')
self.prefix = 'somethingElse!user@host.domain.tld'
self.assertRegexp('foo bar baz', 'not.*command')
finally:
conf.supybot.reply.whenNotCommand.set(original)
if network:
def testNotReplyWhenRegexpsMatch(self):
try:
orig = conf.supybot.reply.whenNotCommand()
gk = conf.supybot.plugins.Gameknot.gameSnarfer()
conf.supybot.reply.whenNotCommand.setValue(True)
conf.supybot.plugins.Gameknot.gameSnarfer.setValue(True)
self.prefix = 'somethingElse!user@host.domain.tld'
self.assertSnarfNotError(
'http://gameknot.com/chess.pl?bd=1019508')
finally:
conf.supybot.reply.whenNotCommand.setValue(orig)
conf.supybot.plugins.Gameknot.gameSnarfer.setValue(gk)
def testNotReplyWhenNotCanonicalName(self):
try:
original = str(conf.supybot.reply.whenNotCommand)
conf.supybot.reply.whenNotCommand.set('True')
self.prefix = 'somethingElse!user@host.domain.tld'
self.assertNotRegexp('STrLeN foobar', 'command')
self.assertResponse('StRlEn foobar', '6')
finally:
conf.supybot.reply.whenNotCommand.set(original)
def testHelp(self):
self.assertHelp('help list')
self.assertRegexp('help help', r'^\(\x02help')
#self.assertRegexp('help misc help', r'^\(\x02misc help')
self.assertError('help nonExistentCommand')
def testHelpDoesAmbiguityWithDefaultPlugins(self):
m = self.getMsg('help list') # Misc.list and User.list.
self.failIf(m.args[1].startswith('Error'))
def testHelpIsCaseInsensitive(self):
self.assertHelp('help LIST')
def testList(self):
self.assertNotError('list')
self.assertNotError('list Misc')
def testListIsCaseInsensitive(self):
self.assertNotError('list misc')
def testListPrivate(self):
# If Ctcp changes to public, these tests will break. So if
# the next assert fails, change the plugin we test for public/private
# to some other non-public plugin.
name = 'Anonymous'
conf.supybot.plugins.Anonymous.public.setValue(False)
self.assertNotRegexp('list', name)
self.assertRegexp('list --private', name)
conf.supybot.plugins.Anonymous.public.setValue(True)
self.assertRegexp('list', name)
self.assertNotRegexp('list --private', name)
def testListDoesNotIncludeNonCanonicalName(self):
self.assertNotRegexp('list Owner', '_exec')
def testListNoIncludeDispatcher(self):
self.assertNotRegexp('list Misc', 'misc')
def testListIncludesDispatcherIfThereIsAnOriginalCommand(self):
self.assertRegexp('list Dict', r'\bdict\b')
def testContributors(self):
# Test ability to list contributors
self.assertNotError('contributors Misc')
# Test ability to list contributions
# Verify that when a single command contribution has been made,
# the word "command" is properly not pluralized.
# Note: This will break if the listed person ever makes more than
# one contribution to the Misc plugin
self.assertRegexp('contributors Misc skorobeus', 'command')
# Test handling of pluralization of "command" when person has
# contributed more than one command to the plugin.
# -- Need to create this case, check it with the regexp 'commands'
# Test handling of invalid plugin
self.assertRegexp('contributors InvalidPlugin', 'not a valid plugin')
# Test handling of invalid person
self.assertRegexp('contributors Misc noname',
'not a registered contributor')
# Test handling of valid person with no contributions
# Note: This will break if the listed person ever makes a contribution
# to the Misc plugin
self.assertRegexp('contributors Misc bwp',
'listed as a contributor')
def testContributorsIsCaseInsensitive(self):
self.assertNotError('contributors Misc Skorobeus')
self.assertNotError('contributors Misc sKoRoBeUs')
if network:
def testVersion(self):
print '*** This test should start passing when we have our '\
'threaded issues resolved.'
self.assertNotError('version')
def testSource(self):
self.assertNotError('source')
def testPlugin(self):
self.assertRegexp('plugin plugin', 'available.*Misc')
self.assertResponse('echo [plugin plugin]', 'Misc')
def testTell(self):
m = self.getMsg('tell foo [plugin tell]')
self.failUnless('let you do' in m.args[1])
m = self.getMsg('tell #foo [plugin tell]')
self.failUnless('No need for' in m.args[1])
m = self.getMsg('tell me you love me')
self.failUnless(m.args[0] == self.nick)
def testTellDoesNotPropogateAction(self):
m = self.getMsg('tell foo [action bar]')
self.failIf(ircmsgs.isAction(m))
def testLast(self):
orig = conf.supybot.plugins.Misc.timestampFormat()
try:
conf.supybot.plugins.Misc.timestampFormat.setValue('')
self.feedMsg('foo bar baz')
self.assertResponse('last', '<%s> foo bar baz' % self.nick)
self.assertRegexp('last', '<%s> @last' % self.nick)
self.assertResponse('last --with foo', '<%s> foo bar baz' % \
self.nick)
self.assertResponse('last --without foo', '<%s> @last' % self.nick)
self.assertRegexp('last --regexp m/\s+/', 'last --without foo')
self.assertResponse('last --regexp m/bar/',
'<%s> foo bar baz' % self.nick)
self.assertResponse('last --from %s' % self.nick.upper(),
'<%s> @last --regexp m/bar/' % self.nick)
self.assertResponse('last --from %s*' % self.nick[0],
'<%s> @last --from %s' %
(self.nick, self.nick.upper()))
conf.supybot.plugins.Misc.timestampFormat.setValue('foo')
self.assertSnarfNoResponse('foo bar baz', 1)
self.assertResponse('last', 'foo <%s> foo bar baz' % self.nick)
finally:
conf.supybot.plugins.Misc.timestampFormat.setValue(orig)
def testNestedLastTimestampConfig(self):
tsConfig = conf.supybot.plugins.Misc.last.nested.includeTimestamp
orig = tsConfig()
try:
tsConfig.setValue(True)
self.feedMsg('foo bar baz')
self.assertRegexp('echo [last]',
'\[\d+:\d+:\d+\] foo bar baz')
finally:
tsConfig.setValue(orig)
def testNestedLastNickConfig(self):
nickConfig = conf.supybot.plugins.Misc.last.nested.includeNick
orig = nickConfig()
try:
nickConfig.setValue(True)
self.feedMsg('foo bar baz')
self.assertRegexp('echo [last]',
'<%s> foo bar baz' % self.nick)
finally:
nickConfig.setValue(orig)
def testMore(self):
self.assertRegexp('echo %s' % ('abc'*300), 'more')
self.assertRegexp('more', 'more')
self.assertNotRegexp('more', 'more')
def testInvalidCommand(self):
self.assertError('echo []')
def testMoreIsCaseInsensitive(self):
self.assertNotError('echo %s' % ('abc'*2000))
self.assertNotError('more')
nick = ircutils.nickFromHostmask(self.prefix)
self.assertNotError('more %s' % nick)
self.assertNotError('more %s' % nick.upper())
self.assertNotError('more %s' % nick.lower())
def testPrivate(self):
m = self.getMsg('private [list]')
self.failIf(ircutils.isChannel(m.args[0]))
def testNotice(self):
m = self.getMsg('notice [list]')
self.assertEqual(m.command, 'NOTICE')
def testNoticePrivate(self):
m = self.assertNotError('notice [private [list]]')
self.assertEqual(m.command, 'NOTICE')
self.assertEqual(m.args[0], self.nick)
m = self.assertNotError('private [notice [list]]')
self.assertEqual(m.command, 'NOTICE')
self.assertEqual(m.args[0], self.nick)
def testHostmask(self):
self.assertResponse('hostmask', self.prefix)
self.assertError('@hostmask asdf')
m = self.irc.takeMsg()
self.failIf(m is not None, m)
def testApropos(self):
self.assertNotError('apropos f')
self.assertRegexp('apropos asldkfjasdlkfja', 'No appropriate commands')
def testAproposIsNotCaseSensitive(self):
self.assertNotRegexp('apropos LIST', 'No appropriate commands')
def testAproposDoesntReturnNonCanonicalNames(self):
self.assertNotRegexp('apropos exec', '_exec')
def testRevision(self):
self.assertNotError('revision Misc')
self.assertNotError('revision Misc.py')
self.assertNotError('revision')
def testRevisionDoesNotLowerUnnecessarily(self):
self.assertNotError('load Math')
m1 = self.assertNotError('revision Math')
m2 = self.assertNotError('revision math')
self.assertEqual(m1, m2)
def testRevisionIsCaseInsensitive(self):
self.assertNotError('revision misc')
class MiscNonChannelTestCase(PluginTestCase):
plugins = ('Misc',)
def testAction(self):
self.prefix = 'something!else@somewhere.else'
self.nick = 'something'
m = self.assertAction('action foo', 'foo')
self.failIf(m.args[0] == self.irc.nick)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

47
plugins/Owner/__init__.py Normal file
View File

@ -0,0 +1,47 @@
###
# Copyright (c) 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 supybot
__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.
# Backwards compatibility.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure

55
plugins/Owner/config.py Normal file
View File

@ -0,0 +1,55 @@
###
# Copyright (c) 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 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('Owner', True)
Owner = conf.registerPlugin('Owner', True)
conf.registerGlobalValue(Owner, 'public',
registry.Boolean(True, """Determines whether this plugin is publicly
visible."""))
conf.registerGlobalValue(Owner, 'quitMsg',
registry.String('', """Determines what quit message will be used by default.
If the quit command is called without a quit message, this will be used. If
this value is empty, the nick of the person giving the quit command will be
used."""))
conf.registerGroup(conf.supybot.commands, 'renames', orderAlphabetically=True)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

675
plugins/Owner/plugin.py Normal file
View File

@ -0,0 +1,675 @@
###
# 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.
###
"""
Provides commands useful to the owner of the bot; the commands here require
their caller to have the 'owner' capability. This plugin is loaded by default.
"""
import supybot.fix as fix
import gc
import os
import imp
import sre
import sys
import getopt
import socket
import logging
import linecache
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.irclib as irclib
import supybot.drivers as drivers
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
import supybot.structures as structures
class Deprecated(ImportError):
pass
def loadPluginModule(name, ignoreDeprecation=False):
"""Loads (and returns) the module for the plugin with the given name."""
files = []
pluginDirs = conf.supybot.directories.plugins()
for dir in pluginDirs:
try:
files.extend(os.listdir(dir))
except EnvironmentError: # OSError, IOError superclass.
log.warning('Invalid plugin directory: %s; removing.',
utils.quoted(dir))
conf.supybot.directories.plugins().remove(dir)
loweredFiles = map(str.lower, files)
try:
index = loweredFiles.index(name.lower()+'.py')
name = os.path.splitext(files[index])[0]
if name in sys.modules:
m = sys.modules[name]
if not hasattr(m, 'Class'):
raise ImportError, 'Module is not a plugin.'
except ValueError: # We'd rather raise the ImportError, so we'll let go...
pass
moduleInfo = imp.find_module(name, pluginDirs)
try:
module = imp.load_module(name, *moduleInfo)
except:
sys.modules.pop(name, None)
raise
if 'deprecated' in module.__dict__ and module.deprecated:
if ignoreDeprecation:
log.warning('Deprecated plugin loaded: %s', name)
else:
raise Deprecated, 'Attempted to load deprecated plugin %s' % \
utils.quoted(name)
if module.__name__ in sys.modules:
sys.modules[module.__name__] = module
linecache.checkcache()
return module
def loadPluginClass(irc, module, register=None):
"""Loads the plugin Class from the given module into the given Irc."""
try:
cb = module.Class()
except AttributeError, e:
if 'Class' in str(e):
raise callbacks.Error, \
'This plugin module doesn\'t have a "Class" ' \
'attribute to specify which plugin should be ' \
'instantiated. If you didn\'t write this ' \
'plugin, but received it with Supybot, file ' \
'a bug with us about this error.'
else:
raise
plugin = cb.name()
public = True
if hasattr(cb, 'public'):
public = cb.public
conf.registerPlugin(plugin, register, public)
assert not irc.getCallback(plugin)
try:
renames = registerRename(plugin)()
if renames:
for command in renames:
v = registerRename(plugin, command)
newName = v()
assert newName
renameCommand(cb, command, newName)
else:
conf.supybot.commands.renames.unregister(plugin)
except registry.NonExistentRegistryEntry, e:
pass # The plugin isn't there.
irc.addCallback(cb)
return cb
###
# supybot.commands.
###
def registerDefaultPlugin(command, plugin):
command = callbacks.canonicalName(command)
conf.registerGlobalValue(conf.supybot.commands.defaultPlugins,
command, registry.String(plugin, ''))
# This must be set, or the quotes won't be removed.
conf.supybot.commands.defaultPlugins.get(command).set(plugin)
def registerRename(plugin, command=None, newName=None):
g = conf.registerGlobalValue(conf.supybot.commands.renames, plugin,
registry.SpaceSeparatedSetOfStrings([], """Determines what commands
in this plugin are to be renamed."""))
if command is not None:
g().add(command)
v = conf.registerGlobalValue(g, command, registry.String('', ''))
if newName is not None:
v.setValue(newName) # In case it was already registered.
return v
else:
return g
def renameCommand(cb, name, newName):
assert not hasattr(cb, newName), 'Cannot rename over existing attributes.'
assert newName == callbacks.canonicalName(newName), \
'newName must already be canonicalized.'
if name != newName:
method = getattr(cb.__class__, name)
setattr(cb.__class__, newName, method)
delattr(cb.__class__, name)
registerDefaultPlugin('list', 'Misc')
registerDefaultPlugin('help', 'Misc')
registerDefaultPlugin('ignore', 'Admin')
registerDefaultPlugin('reload', 'Owner')
registerDefaultPlugin('enable', 'Owner')
registerDefaultPlugin('disable', 'Owner')
registerDefaultPlugin('unignore', 'Admin')
registerDefaultPlugin('capabilities', 'User')
registerDefaultPlugin('addcapability', 'Admin')
registerDefaultPlugin('removecapability', 'Admin')
class holder(object):
pass
# This is used so we can support a "log" command as well as a "self.log"
# Logger.
class LogProxy(object):
"""<text>
Logs <text> to the global Supybot log at critical priority. Useful for
marking logfiles for later searching.
"""
__name__ = 'log' # Necessary for help.
def __init__(self, log):
self.log = log
self.im_func = holder()
self.im_func.func_name = 'log'
def __call__(self, irc, msg, args, text):
log.critical(text)
irc.replySuccess()
__call__ = wrap(__call__, ['text'])
def __getattr__(self, attr):
return getattr(self.log, attr)
class Owner(callbacks.Privmsg):
# This plugin must be first; its priority must be lowest; otherwise odd
# things will happen when adding callbacks.
def __init__(self, *args, **kwargs):
self.__parent = super(Owner, self)
self.__parent.__init__()
# Setup log object/command.
self.log = LogProxy(self.log)
# Setup command flood detection.
self.commands = ircutils.FloodQueue(60)
# Setup Irc objects, connected to networks. If world.ircs is already
# populated, chances are that we're being reloaded, so don't do this.
if not world.ircs:
for network in conf.supybot.networks():
try:
self._connect(network)
except socket.error, e:
self.log.error('Could not connect to %s: %s.', network, e)
except Exception, e:
self.log.exception('Exception connecting to %s:', network)
self.log.error('Could not connect to %s: %s.', network, e)
# Setup plugins and default plugins for commands.
for (name, s) in registry._cache.iteritems():
if 'alwaysLoadDefault' in name or 'alwaysLoadImportant' in name:
continue
if name.startswith('supybot.plugins'):
try:
(_, _, name) = registry.split(name)
except ValueError: # unpack list of wrong size.
continue
# This is just for the prettiness of the configuration file.
# There are no plugins that are all-lowercase, so we'll at
# least attempt to capitalize them.
if name == name.lower():
name = name.capitalize()
conf.registerPlugin(name)
if name.startswith('supybot.commands.defaultPlugins'):
try:
(_, _, _, name) = registry.split(name)
except ValueError: # unpack list of wrong size.
continue
registerDefaultPlugin(name, s)
def callPrecedence(self, irc):
return ([], [cb for cb in irc.callbacks if cb is not self])
def outFilter(self, irc, msg):
if msg.command == 'PRIVMSG' and not world.testing:
if ircutils.strEqual(msg.args[0], irc.nick):
self.log.warning('Tried to send a message to myself: %r.', msg)
return None
return msg
def isCommand(self, name):
return name == 'log' or \
self.__parent.isCommand(name)
def reset(self):
# This has to be done somewhere, I figure here is as good place as any.
callbacks.Privmsg._mores.clear()
self.__parent.reset()
def _connect(self, network, serverPort=None, password=''):
try:
group = conf.supybot.networks.get(network)
(server, port) = group.servers()[0]
except (registry.NonExistentRegistryEntry, IndexError):
if serverPort is None:
raise ValueError, 'connect requires a (server, port) ' \
'if the network is not registered.'
conf.registerNetwork(network, password)
serverS = '%s:%s' % serverPort
conf.supybot.networks.get(network).servers.append(serverS)
assert conf.supybot.networks.get(network).servers()
self.log.info('Creating new Irc for %s.', network)
newIrc = irclib.Irc(network)
for irc in world.ircs:
if irc != newIrc:
newIrc.state.history = irc.state.history
driver = drivers.newDriver(newIrc)
return newIrc
def do001(self, irc, msg):
self.log.info('Loading plugins (connected to %s).', irc.network)
alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant()
important = conf.supybot.commands.defaultPlugins.importantPlugins()
for (name, value) in conf.supybot.plugins.getValues(fullNames=False):
if irc.getCallback(name) is None:
load = value()
if not load and name in important:
if alwaysLoadImportant:
s = '%s is configured not to be loaded, but is being '\
'loaded anyway because ' \
'supybot.plugins.alwaysLoadImportant is True.'
self.log.warning(s, name)
load = True
if load:
if not irc.getCallback(name):
# This is debug because each log logs its beginning.
self.log.debug('Loading %s.' % name)
try:
m = loadPluginModule(name, ignoreDeprecation=True)
loadPluginClass(irc, m)
except callbacks.Error, e:
# This is just an error message.
log.warning(str(e))
except ImportError, e:
log.warning('Failed to load %s: %s.', name, e)
except Exception, e:
log.exception('Failed to load %s:', name)
else:
# Let's import the module so configuration is preserved.
try:
_ = loadPluginModule(name)
except Exception, e:
log.debug('Attempted to load %s to preserve its '
'configuration, but load failed: %s',
name, e)
world.starting = False
def do376(self, irc, msg):
networkGroup = conf.supybot.networks.get(irc.network)
for channel in networkGroup.channels():
irc.queueMsg(networkGroup.channels.join(channel))
do422 = do377 = do376
def doPrivmsg(self, irc, msg):
assert self is irc.callbacks[0], 'Owner isn\'t first callback.'
if ircmsgs.isCtcp(msg):
return
s = callbacks.addressed(irc.nick, msg)
if s:
ignored = ircdb.checkIgnored(msg.prefix)
if ignored:
self.log.info('Ignoring command from %s.' % msg.prefix)
return
try:
tokens = callbacks.tokenize(s, channel=msg.args[0])
self.Proxy(irc, msg, tokens)
except SyntaxError, e:
irc.queueMsg(callbacks.error(msg, str(e)))
def announce(self, irc, msg, args, text):
"""<text>
Sends <text> to all channels the bot is currently on and not
lobotomized in.
"""
u = ircdb.users.getUser(msg.prefix)
text = 'Announcement from my owner (%s): %s' % (u.name, text)
for channel in irc.state.channels:
c = ircdb.channels.getChannel(channel)
if not c.lobotomized:
irc.queueMsg(ircmsgs.privmsg(channel, text))
irc.noReply()
announce = wrap(announce, ['text'])
def defaultplugin(self, irc, msg, args, optlist, command, plugin):
"""[--remove] <command> [<plugin>]
Sets the default plugin for <command> to <plugin>. If --remove is
given, removes the current default plugin for <command>. If no plugin
is given, returns the current default plugin set for <command>.
"""
remove = False
for (option, arg) in optlist:
if option == 'remove':
remove = True
cbs = callbacks.findCallbackForCommand(irc, command)
if remove:
try:
conf.supybot.commands.defaultPlugins.unregister(command)
irc.replySuccess()
except registry.NonExistentRegistryEntry:
s = 'I don\'t have a default plugin set for that command.'
irc.error(s)
elif not cbs:
irc.errorInvalid('command', command)
elif plugin:
if not plugin.isCommand(command):
irc.errorInvalid('command in the %s plugin' % plugin, command)
registerDefaultPlugin(command, plugin.name())
irc.replySuccess()
else:
try:
irc.reply(conf.supybot.commands.defaultPlugins.get(command)())
except registry.NonExistentRegistryEntry:
s = 'I don\'t have a default plugin set for that command.'
irc.error(s)
defaultplugin = wrap(defaultplugin, [getopts({'remove': ''}),
'commandName',
additional('plugin')])
def ircquote(self, irc, msg, args, s):
"""<string to be sent to the server>
Sends the raw string given to the server.
"""
try:
m = ircmsgs.IrcMsg(s)
except Exception, e:
irc.error(utils.exnToString(e))
else:
irc.queueMsg(m)
irc.noReply()
ircquote = wrap(ircquote, ['text'])
def quit(self, irc, msg, args, text):
"""[<text>]
Exits the bot with the QUIT message <text>. If <text> is not given,
the default quit message (supybot.plugins.Owner.quitMsg) will be used.
If there is no default quitMsg set, your nick will be used.
"""
text = text or self.registryValue('quitMsg') or msg.nick
irc.noReply()
m = ircmsgs.quit(text)
world.upkeep()
for irc in world.ircs[:]:
irc.queueMsg(m)
irc.die()
quit = wrap(quit, [additional('text')])
def flush(self, irc, msg, args):
"""takes no arguments
Runs all the periodic flushers in world.flushers. This includes
flushing all logs and all configuration changes to disk.
"""
world.flush()
irc.replySuccess()
flush = wrap(flush)
def upkeep(self, irc, msg, args, level):
"""[<level>]
Runs the standard upkeep stuff (flushes and gc.collects()). If given
a level, runs that level of upkeep (currently, the only supported
level is "high", which causes the bot to flush a lot of caches as well
as do normal upkeep stuff.
"""
L = []
if level == 'high':
L.append('Regexp cache flushed: %s cleared.' %
utils.nItems('regexp', len(sre._cache)))
sre.purge()
L.append('Pattern cache flushed: %s cleared.' %
utils.nItems('compiled pattern',
len(ircutils._patternCache)))
ircutils._patternCache.clear()
L.append('hostmaskPatternEqual cache flushed: %s cleared.' %
utils.nItems('result',
len(ircutils._hostmaskPatternEqualCache)))
ircutils._hostmaskPatternEqualCache.clear()
L.append('ircdb username cache flushed: %s cleared.' %
utils.nItems('username to id mapping',
len(ircdb.users._nameCache)))
ircdb.users._nameCache.clear()
L.append('ircdb hostmask cache flushed: %s cleared.' %
utils.nItems('hostmask to id mapping',
len(ircdb.users._hostmaskCache)))
ircdb.users._hostmaskCache.clear()
L.append('linecache line cache flushed: %s cleared.' %
utils.nItems('line', len(linecache.cache)))
linecache.clearcache()
sys.exc_clear()
collected = world.upkeep()
if gc.garbage:
L.append('Garbage! %r.' % gc.garbage)
L.append('%s collected.' % utils.nItems('object', collected))
irc.reply(' '.join(L))
upkeep = wrap(upkeep, [additional(('literal', ['high']))])
def load(self, irc, msg, args, optlist, name):
"""[--deprecated] <plugin>
Loads the plugin <plugin> from any of the directories in
conf.supybot.directories.plugins; usually this includes the main
installed directory and 'plugins' in the current directory.
--deprecated is necessary if you wish to load deprecated plugins.
"""
ignoreDeprecation = False
for (option, argument) in optlist:
if option == 'deprecated':
ignoreDeprecation = True
if name.endswith('.py'):
name = name[:-3]
if irc.getCallback(name):
irc.error('%s is already loaded.' % name.capitalize())
return
try:
module = loadPluginModule(name, ignoreDeprecation)
except Deprecated:
irc.error('%s is deprecated. Use --deprecated '
'to force it to load.' % name.capitalize())
return
except ImportError, e:
if name in str(e):
irc.error('No plugin named %s exists.' % utils.dqrepr(name))
else:
irc.error(str(e))
return
cb = loadPluginClass(irc, module)
name = cb.name() # Let's normalize this.
conf.registerPlugin(name, True)
irc.replySuccess()
load = wrap(load, [getopts({'deprecated': ''}), 'something'])
def reload(self, irc, msg, args, name):
"""<plugin>
Unloads and subsequently reloads the plugin by name; use the 'list'
command to see a list of the currently loaded plugins.
"""
callbacks = irc.removeCallback(name)
if callbacks:
module = sys.modules[callbacks[0].__module__]
if hasattr(module, 'reload'):
x = module.reload()
try:
module = loadPluginModule(name)
if hasattr(module, 'reload'):
module.reload(x)
for callback in callbacks:
callback.die()
del callback
gc.collect() # This makes sure the callback is collected.
callback = loadPluginClass(irc, module)
irc.replySuccess()
except ImportError:
for callback in callbacks:
irc.addCallback(callback)
irc.error('No plugin %s exists.' % name)
else:
irc.error('There was no plugin %s.' % name)
reload = wrap(reload, ['something'])
def unload(self, irc, msg, args, name):
"""<plugin>
Unloads the callback by name; use the 'list' command to see a list
of the currently loaded callbacks. Obviously, the Owner plugin can't
be unloaded.
"""
if ircutils.strEqual(name, self.name()):
irc.error('You can\'t unload the %s plugin.' % name)
return
# Let's do this so even if the plugin isn't currently loaded, it doesn't
# stay attempting to load.
conf.registerPlugin(name, False)
callbacks = irc.removeCallback(name)
if callbacks:
for callback in callbacks:
callback.die()
del callback
gc.collect()
irc.replySuccess()
else:
irc.error('There was no plugin %s.' % name)
unload = wrap(unload, ['something'])
def defaultcapability(self, irc, msg, args, action, capability):
"""{add|remove} <capability>
Adds or removes (according to the first argument) <capability> from the
default capabilities given to users (the configuration variable
supybot.capabilities stores these).
"""
if action == 'add':
conf.supybot.capabilities().add(capability)
irc.replySuccess()
elif action == 'remove':
try:
conf.supybot.capabilities().remove(capability)
irc.replySuccess()
except KeyError:
if ircdb.isAntiCapability(capability):
irc.error('That capability wasn\'t in '
'supybot.capabilities.')
else:
anticap = ircdb.makeAntiCapability(capability)
conf.supybot.capabilities().add(anticap)
irc.replySuccess()
defaultcapability = wrap(defaultcapability,
[('literal', ['add','remove']), 'capability'])
def disable(self, irc, msg, args, plugin, command):
"""[<plugin>] <command>
Disables the command <command> for all users (including the owners).
If <plugin> is given, only disables the <command> from <plugin>. If
you want to disable a command for most users but not for yourself, set
a default capability of -plugin.command or -command (if you want to
disable the command in all plugins).
"""
if command in ('enable', 'identify'):
irc.error('You can\'t disable %s.' % command)
return
if plugin:
if plugin.isCommand(command):
pluginCommand = '%s.%s' % (plugin.name(), command)
conf.supybot.commands.disabled().add(pluginCommand)
else:
irc.error('%s is not a command in the %s plugin.' %
(command, plugin.name()))
return
self._disabled.add(command, plugin.name())
else:
conf.supybot.commands.disabled().add(command)
self._disabled.add(command)
irc.replySuccess()
disable = wrap(disable, [optional('plugin'), 'commandName'])
def enable(self, irc, msg, args, plugin, command):
"""[<plugin>] <command>
Enables the command <command> for all users. If <plugin>
if given, only enables the <command> from <plugin>. This command is
the inverse of disable.
"""
try:
if plugin:
command = '%s.%s' % (plugin.name(), command)
self._disabled.remove(command, plugin.name())
else:
self._disabled.remove(command)
conf.supybot.commands.disabled().remove(command)
irc.replySuccess()
except KeyError:
irc.error('That command wasn\'t disabled.')
enable = wrap(enable, [optional('plugin'), 'commandName'])
def rename(self, irc, msg, args, plugin, command, newName):
"""<plugin> <command> <new name>
Renames <command> in <plugin> to the <new name>.
"""
if not plugin.isCommand(command):
what = 'command in the %s plugin' % plugin.name()
irc.errorInvalid(what, command)
if hasattr(plugin, newName):
irc.error('The %s plugin already has an attribute named %s.' %
(plugin, newName))
return
registerRename(plugin.name(), command, newName)
renameCommand(plugin, command, newName)
irc.replySuccess()
rename = wrap(rename, ['plugin', 'commandName', 'commandName'])
def unrename(self, irc, msg, args, plugin):
"""<plugin>
Removes all renames in <plugin>. The plugin will be reloaded after
this command is run.
"""
try:
conf.supybot.commands.renames.unregister(plugin.name())
except registry.NonExistentRegistryEntry:
irc.errorInvalid('plugin', plugin.name())
self.reload(irc, msg, [plugin.name()]) # This makes the replySuccess.
unrename = wrap(unrename, ['plugin'])
Class = Owner
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

109
plugins/Owner/test.py Normal file
View File

@ -0,0 +1,109 @@
###
# 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.conf as conf
import supybot.plugins.Owner as Owner
class OwnerTestCase(PluginTestCase, PluginDocumentation):
plugins = ('Utilities', 'Relay', 'Network', 'Admin', 'Channel')
def testHelpLog(self):
self.assertHelp('help log')
def testSrcAmbiguity(self):
self.assertError('addcapability foo bar')
def testIrcquote(self):
self.assertResponse('ircquote PRIVMSG %s :foo' % self.irc.nick, 'foo')
def testFlush(self):
self.assertNotError('flush')
def testUpkeep(self):
self.assertNotError('upkeep')
def testLoad(self):
self.assertError('load Owner')
self.assertError('load owner')
self.assertNotError('load Alias')
self.assertNotError('list Owner')
def testReload(self):
self.assertError('reload Alias')
self.assertNotError('load Alias')
self.assertNotError('reload ALIAS')
self.assertNotError('reload ALIAS')
def testUnload(self):
self.assertError('unload Foobar')
self.assertNotError('load Alias')
self.assertNotError('unload Alias')
self.assertError('unload Alias')
self.assertNotError('load ALIAS')
self.assertNotError('unload ALIAS')
def testDisable(self):
self.assertError('disable enable')
self.assertError('disable identify')
def testEnable(self):
self.assertError('enable enable')
def testEnableIsCaseInsensitive(self):
self.assertNotError('disable Foo')
self.assertNotError('enable foo')
def testRename(self):
self.assertError('rename admin ignore IGNORE')
self.assertError('rename admin ignore ig-nore')
self.assertNotError('rename admin removecapability rmcap')
self.assertNotRegexp('list admin', 'removecapability')
self.assertRegexp('list admin', 'rmcap')
self.assertNotError('reload admin')
self.assertNotRegexp('list admin', 'removecapability')
self.assertRegexp('list admin', 'rmcap')
self.assertNotError('unrename admin')
self.assertRegexp('list admin', 'removecapability')
self.assertNotRegexp('list admin', 'rmcap')
def testDefaultPluginErrorsWhenCommandNotInPlugin(self):
self.assertError('defaultplugin foobar owner')
class FunctionsTestCase(SupyTestCase):
def testLoadPluginModule(self):
self.assertRaises(ImportError, Owner.loadPluginModule, 'asldj')
self.failUnless(Owner.loadPluginModule('Owner'))
self.failUnless(Owner.loadPluginModule('owner'))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

47
plugins/User/__init__.py Normal file
View File

@ -0,0 +1,47 @@
###
# Copyright (c) 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 supybot
__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.
# Backwards compatibility.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure

48
plugins/User/config.py Normal file
View File

@ -0,0 +1,48 @@
###
# Copyright (c) 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 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('User', True)
User = conf.registerPlugin('User')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(User, 'someConfigVariableName',
# registry.Boolean(False, """Help for someConfigVariableName."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

434
plugins/User/plugin.py Normal file
View File

@ -0,0 +1,434 @@
###
# 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.
###
"""
Provides commands useful to users in general. This plugin is loaded by default.
"""
import supybot.fix as fix
import re
import getopt
import fnmatch
from itertools import imap, ifilter
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
from supybot.commands import *
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
class User(callbacks.Privmsg):
def _checkNotChannel(self, irc, msg, password=' '):
if password and ircutils.isChannel(msg.args[0]):
raise callbacks.Error, conf.supybot.replies.requiresPrivacy()
def list(self, irc, msg, args, optlist, glob):
"""[--capability=<capability>] [<glob>]
Returns the valid registered usernames matching <glob>. If <glob> is
not given, returns all registered usernames.
"""
predicates = []
for (option, arg) in optlist:
if option == 'capability':
def p(u, cap=arg):
try:
return u._checkCapability(cap)
except KeyError:
return False
predicates.append(p)
if glob:
r = re.compile(fnmatch.translate(glob), re.I)
def p(u):
return r.match(u.name) is not None
predicates.append(p)
users = []
for u in ircdb.users.itervalues():
for predicate in predicates:
if not predicate(u):
break
else:
users.append(u.name)
if users:
utils.sortBy(str.lower, users)
irc.reply(utils.commaAndify(users))
else:
if predicates:
irc.reply('There are no matching registered users.')
else:
irc.reply('There are no registered users.')
list = wrap(list, [getopts({'capability':'capability'}),
additional('glob')])
def register(self, irc, msg, args, name, password):
"""<name> <password>
Registers <name> with the given password <password> and the current
hostmask of the person registering. You shouldn't register twice; if
you're not recognized as a user but you've already registered, use the
addhostmask command to add another hostmask to your already-registered
user, or use the identify command to identify just for a session.
This command (and all other commands that include a password) must be
sent to the bot privately, not in a channel.
"""
addHostmask = True
try:
ircdb.users.getUserId(name)
irc.error('That name is already assigned to someone.', Raise=True)
except KeyError:
pass
if ircutils.isUserHostmask(name):
irc.errorInvalid('username', name,
'Hostmasks are not valid usernames.', Raise=True)
try:
u = ircdb.users.getUser(msg.prefix)
if u._checkCapability('owner'):
addHostmask = False
else:
irc.error('Your hostmask is already registered to %s' % u.name)
return
except KeyError:
pass
user = ircdb.users.newUser()
user.name = name
user.setPassword(password)
if addHostmask:
user.addHostmask(msg.prefix)
ircdb.users.setUser(user)
irc.replySuccess()
register = wrap(register, ['private', 'something', 'something'])
def unregister(self, irc, msg, args, user, password):
"""<name> [<password>]
Unregisters <name> from the user database. If the user giving this
command is an owner user, the password is not necessary.
"""
try:
caller = ircdb.users.getUser(msg.prefix)
isOwner = caller._checkCapability('owner')
except KeyError:
caller = None
isOwner = False
if not conf.supybot.databases.users.allowUnregistration():
if not caller or not isOwner:
self.log.warning('%s tried to unregister user %s.',
msg.prefix, user.name)
irc.error('This command has been disabled. You\'ll have to '
'ask the owner of this bot to unregister your user.')
if isOwner or user.checkPassword(password):
ircdb.users.delUser(user.id)
irc.replySuccess()
else:
irc.error(conf.supybot.replies.incorrectAuthentication())
unregister = wrap(unregister, ['private', 'otherUser',
additional('anything')])
def changename(self, irc, msg, args, user, newname, password):
"""<name> <new name> [<password>]
Changes your current user database name to the new name given.
<password> is only necessary if the user isn't recognized by hostmask.
If you include the <password> parameter, this message must be sent
to the bot privately (not on a channel).
"""
try:
id = ircdb.users.getUserId(newname)
irc.error('%s is already registered.' % utils.quoted(newname))
return
except KeyError:
pass
if user.checkHostmask(msg.prefix) or user.checkPassword(password):
user.name = newname
ircdb.users.setUser(user)
irc.replySuccess()
changename = wrap(changename, ['private', 'otherUser', 'something',
additional('something', '')])
def addhostmask(self, irc, msg, args, user, hostmask, password):
"""[<name>] [<hostmask>] [<password>]
Adds the hostmask <hostmask> to the user specified by <name>. The
<password> may only be required if the user is not recognized by
hostmask. If you include the <password> parameter, this message must
be sent to the bot privately (not on a channel). <password> is also
not required if an owner user is giving the command on behalf of some
other user. If <hostmask> is not given, it defaults to your current
hostmask. If <name> is not given, it defaults to your currently
identified name.
"""
if not hostmask:
hostmask = msg.prefix
if not ircutils.isUserHostmask(hostmask):
irc.errorInvalid('hostmask', hostmask, 'Make sure your hostmask '
'includes a nick, then an exclamation point (!), then '
'a user, then an at symbol (@), then a host. Feel '
'free to use wildcards (* and ?, which work just like '
'they do on the command line) in any of these parts.',
Raise=True)
try:
otherId = ircdb.users.getUserId(hostmask)
if otherId != user.id:
irc.error('That hostmask is already registered.', Raise=True)
except KeyError:
pass
if not user.checkPassword(password) and \
not user.checkHostmask(msg.prefix):
try:
u = ircdb.users.getUser(msg.prefix)
except KeyError:
irc.error(conf.supybot.replies.incorrectAuthentication(),
Raise=True)
if not u._checkCapability('owner'):
irc.error(conf.supybot.replies.incorrectAuthentication(),
Raise=True)
try:
user.addHostmask(hostmask)
except ValueError, e:
irc.error(str(e), Raise=True)
try:
ircdb.users.setUser(user)
except ValueError, e:
irc.error(str(e), Raise=True)
irc.replySuccess()
addhostmask = wrap(addhostmask, [first('otherUser', 'user'),
optional('something'),
additional('something', '')])
def removehostmask(self, irc, msg, args, user, hostmask, password):
"""<name> <hostmask> [<password>]
Removes the hostmask <hostmask> from the record of the user specified
by <name>. If the hostmask is 'all' then all hostmasks will be
removed. The <password> may only be required if the user is not
recognized by his hostmask. If you include the <password> parameter,
this message must be sent to the bot privately (not on a channel).
"""
if not user.checkPassword(password) and \
not user.checkHostmask(msg.prefix):
u = ircdb.users.getUser(msg.prefix)
if not u._checkCapability('owner'):
irc.error(conf.supybot.replies.incorrectAuthentication())
return
try:
s = ''
if hostmask == 'all':
user.hostmasks.clear()
s = 'All hostmasks removed.'
else:
user.removeHostmask(hostmask)
except KeyError:
irc.error('There was no such hostmask.')
return
ircdb.users.setUser(user)
irc.replySuccess(s)
removehostmask = wrap(removehostmask, ['private', 'otherUser', 'something',
additional('something', '')])
def setpassword(self, irc, msg, args, user, password,newpassword):
"""<name> <old password> <new password>
Sets the new password for the user specified by <name> to
<new password>. Obviously this message must be sent to the bot
privately (not in a channel). If the requesting user is an owner user
(and the user whose password is being changed isn't that same owner
user), then <old password> needn't be correct.
"""
u = ircdb.users.getUser(msg.prefix)
if user.checkPassword(password) or \
(u._checkCapability('owner') and not u == user):
user.setPassword(newpassword)
ircdb.users.setUser(user)
irc.replySuccess()
else:
irc.error(conf.supybot.replies.incorrectAuthentication())
setpassword = wrap(setpassword, ['otherUser', 'something', 'something'])
def username(self, irc, msg, args, hostmask):
"""<hostmask|nick>
Returns the username of the user specified by <hostmask> or <nick> if
the user is registered.
"""
if ircutils.isNick(hostmask):
try:
hostmask = irc.state.nickToHostmask(hostmask)
except KeyError:
irc.error('I haven\'t seen %s.' % hostmask, Raise=True)
try:
user = ircdb.users.getUser(hostmask)
irc.reply(user.name)
except KeyError:
irc.error('I don\'t know who that is.')
username = wrap(username, [first('nick', 'hostmask')])
def hostmasks(self, irc, msg, args, name):
"""[<name>]
Returns the hostmasks of the user specified by <name>; if <name> isn't
specified, returns the hostmasks of the user calling the command.
"""
def getHostmasks(user):
hostmasks = map(repr, user.hostmasks)
hostmasks.sort()
return utils.commaAndify(hostmasks)
try:
user = ircdb.users.getUser(msg.prefix)
if name:
if name != user.name and \
not ircdb.checkCapability(msg.prefix, 'owner'):
irc.error('You may only retrieve your own hostmasks.',
Raise=True)
else:
try:
user = ircdb.users.getUser(name)
irc.reply(getHostmasks(user))
except KeyError:
irc.errorNoUser()
else:
irc.reply(getHostmasks(user))
except KeyError:
irc.errorNotRegistered()
hostmasks = wrap(hostmasks, ['private', additional('something')])
def capabilities(self, irc, msg, args, user):
"""[<name>]
Returns the capabilities of the user specified by <name>; if <name>
isn't specified, returns the hostmasks of the user calling the command.
"""
irc.reply('[%s]' % '; '.join(user.capabilities))
capabilities = wrap(capabilities, [first('otherUser', 'user')])
def identify(self, irc, msg, args, user, password):
"""<name> <password>
Identifies the user as <name>. This command (and all other
commands that include a password) must be sent to the bot privately,
not in a channel.
"""
if user.checkPassword(password):
try:
user.addAuth(msg.prefix)
ircdb.users.setUser(user)
irc.replySuccess()
except ValueError:
irc.error('Your secure flag is true and your hostmask '
'doesn\'t match any of your known hostmasks.')
else:
self.log.warning('Failed identification attempt by %s (password '
'did not match for %s).', msg.prefix, user.name)
irc.error(conf.supybot.replies.incorrectAuthentication())
identify = wrap(identify, ['private', 'otherUser', 'something'])
def unidentify(self, irc, msg, args, user):
"""takes no arguments
Un-identifies you. Note that this may not result in the desired
effect of causing the bot not to recognize you anymore, since you may
have added hostmasks to your user that can cause the bot to continue to
recognize you.
"""
user.clearAuth()
ircdb.users.setUser(user)
irc.replySuccess('If you remain recognized after giving this command, '
'you\'re being recognized by hostmask, rather than '
'by password. You must remove whatever hostmask is '
'causing you to be recognized in order not to be '
'recognized.')
unidentify = wrap(unidentify, ['user'])
def whoami(self, irc, msg, args):
"""takes no arguments
Returns the name of the user calling the command.
"""
try:
user = ircdb.users.getUser(msg.prefix)
irc.reply(user.name)
except KeyError:
irc.reply('I don\'t recognize you.')
whoami = wrap(whoami)
def setsecure(self, irc, msg, args, user, password, value):
"""<password> [<True|False>]
Sets the secure flag on the user of the person sending the message.
Requires that the person's hostmask be in the list of hostmasks for
that user in addition to the password being correct. When the secure
flag is set, the user *must* identify before he can be recognized.
If a specific True/False value is not given, it inverts the current
value.
"""
if value is None:
value = not user.secure
if user.checkPassword(password) and \
user.checkHostmask(msg.prefix, useAuth=False):
user.secure = value
ircdb.users.setUser(user)
irc.reply('Secure flag set to %s' % value)
else:
irc.error(conf.supybot.replies.incorrectAuthentication())
setsecure = wrap(setsecure, ['private', 'user', 'something',
additional('boolean')])
def stats(self, irc, msg, args):
"""takes no arguments
Returns some statistics on the user database.
"""
users = 0
owners = 0
admins = 0
hostmasks = 0
for user in ircdb.users.itervalues():
users += 1
hostmasks += len(user.hostmasks)
try:
if user._checkCapability('owner'):
owners += 1
elif user._checkCapability('admin'):
admins += 1
except KeyError:
pass
irc.reply('I have %s registered users '
'with %s registered hostmasks; '
'%s and %s.' % (users, hostmasks,
utils.nItems('owner', owners),
utils.nItems('admin', admins)))
stats = wrap(stats)
Class = User
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

111
plugins/User/test.py Normal file
View File

@ -0,0 +1,111 @@
###
# 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.world as world
import supybot.ircdb as ircdb
class UserTestCase(PluginTestCase, PluginDocumentation):
plugins = ('User',)
prefix1 = 'somethingElse!user@host.tld'
prefix2 = 'EvensomethingElse!user@host.tld'
def testHostmasks(self):
self.assertError('hostmasks')
original = self.prefix
self.prefix = self.prefix1
self.assertNotError('register foo bar')
self.prefix = original
self.assertError('hostmasks foo')
self.assertNotError('addhostmask foo [hostmask] bar')
self.assertNotError('hostmasks foo')
self.assertNotRegexp('hostmasks foo', 'IrcSet')
def testRegisterUnregister(self):
self.prefix = self.prefix1
self.assertNotError('register foo bar')
self.assertError('register foo baz')
self.failUnless(ircdb.users.getUserId('foo'))
self.assertNotError('unregister foo bar')
self.assertRaises(KeyError, ircdb.users.getUserId, 'foo')
def testList(self):
self.prefix = self.prefix1
self.assertNotError('register foo bar')
self.assertResponse('user list', 'foo')
self.prefix = self.prefix2
self.assertNotError('register biff quux')
self.assertResponse('user list', 'biff and foo')
self.assertResponse('user list f', 'biff and foo')
self.assertResponse('user list f*', 'foo')
self.assertResponse('user list *f', 'biff')
self.assertNotError('unregister biff quux')
self.assertResponse('user list', 'foo')
self.assertNotError('unregister foo bar')
self.assertRegexp('user list', 'no registered users')
self.assertRegexp('user list asdlfkjasldkj', 'no matching registered')
def testListHandlesCaps(self):
self.prefix = self.prefix1
self.assertNotError('register Foo bar')
self.assertResponse('user list', 'Foo')
self.assertResponse('user list f*', 'Foo')
def testChangeUsername(self):
self.prefix = self.prefix1
self.assertNotError('register foo bar')
self.prefix = self.prefix2
self.assertNotError('register bar baz')
self.prefix = self.prefix1
self.assertError('changename foo bar')
self.assertNotError('changename foo baz')
def testSetpassword(self):
self.prefix = self.prefix1
self.assertNotError('register foo bar')
password = ircdb.users.getUser(self.prefix).password
self.assertNotEqual(password, 'bar')
self.assertNotError('setpassword foo bar baz')
self.assertNotEqual(ircdb.users.getUser(self.prefix).password,password)
self.assertNotEqual(ircdb.users.getUser(self.prefix).password, 'baz')
def testStats(self):
self.assertNotError('user stats')
self.assertNotError('load Lart')
self.assertNotError('user stats')
def testUserPluginAndUserList(self):
self.prefix = self.prefix1
self.assertNotError('register Foo bar')
self.assertResponse('user list', 'Foo')
self.assertNotError('load Seen')
self.assertResponse('user list', 'Foo')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

350
scripts/supybot Normal file
View File

@ -0,0 +1,350 @@
#!/usr/bin/env python
###
# Copyright (c) 2003-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.
###
"""
This is the main program to run Supybot.
"""
import re
import os
import sys
import atexit
import shutil
import signal
import cStringIO as StringIO
if sys.version_info < (2, 3, 0):
sys.stderr.write('This program requires Python >= 2.3.0\n')
sys.exit(-1)
def _termHandler(signalNumber, stackFrame):
raise SystemExit, 'Signal #%s.' % signalNumber
signal.signal(signal.SIGTERM, _termHandler)
import time
import optparse
import textwrap
started = time.time()
import supybot
import supybot.utils as utils
import supybot.registry as registry
import supybot.questions as questions
def main():
import supybot.conf as conf
import supybot.world as world
import supybot.drivers as drivers
import supybot.schedule as schedule
# We schedule this event rather than have it actually run because if there
# is a failure between now and the time it takes the Owner plugin to load
# all the various plugins, our registry file might be wiped. That's bad.
interrupted = False
when = conf.supybot.upkeepInterval()
schedule.addPeriodicEvent(world.upkeep, when, name='upkeep', now=False)
world.startedAt = started
while world.ircs:
try:
drivers.run()
except KeyboardInterrupt:
if interrupted:
# Interrupted while waiting for queues to clear. Let's clear
# them ourselves.
for irc in world.ircs:
irc._reallyDie()
continue
else:
interrupted = True
log.info('Exiting due to Ctrl-C. '
'If the bot doesn\'t exit within a few seconds, '
'feel free to press Ctrl-C again to make it exit '
'without flushing its message queues.')
world.upkeep()
for irc in world.ircs:
quitmsg = conf.supybot.plugins.Owner.quitMsg() or \
'Ctrl-C at console.'
irc.queueMsg(ircmsgs.quit(quitmsg))
irc.die()
except SystemExit, e:
s = str(e)
if s:
log.info('Exiting due to %s', s)
break
except:
try: # Ok, now we're *REALLY* paranoid!
log.exception('Exception raised out of drivers.run:')
except Exception, e:
print 'Exception raised in log.exception. This is *really*'
print 'bad. Hopefully it won\'t happen again, but tell us'
print 'about it anyway, this is a significant problem.'
print 'Anyway, here\'s the exception: %s'% utils.exnToString(e)
except:
print 'Man, this really sucks. Not only did log.exception'
print 'raise an exception, but freaking-a, it was a string'
print 'exception. People who raise string exceptions should'
print 'die a slow, painful death.'
now = time.time()
seconds = now - world.startedAt
log.info('Total uptime: %s.', utils.timeElapsed(seconds))
(user, system, _, _, _) = os.times()
log.info('Total CPU time taken: %s seconds.', user+system)
log.info('No more Irc objects, exiting.')
version = '0.80.0'
if __name__ == '__main__':
###
# Options:
# -p (profiling)
# -O (optimizing)
# -n, --nick (nick)
# --startup (commands to run onStart)
# --connect (commands to run afterConnect)
# --config (configuration values)
parser = optparse.OptionParser(usage='Usage: %prog [options] configFile',
version='supybot %s' % version)
parser.add_option('-P', '--profile', action='store_true', dest='profile',
help='enables profiling')
parser.add_option('-O', action='count', dest='optimize',
help='-O optimizes asserts out of the code; ' \
'-OO optimizes asserts and uses psyco.')
parser.add_option('-n', '--nick', action='store',
dest='nick', default='',
help='nick the bot should use')
parser.add_option('-u', '--user', action='store',
dest='user', default='',
help='full username the bot should use')
parser.add_option('-i', '--ident', action='store',
dest='ident', default='',
help='ident the bot should use')
parser.add_option('-d', '--daemon', action='store_true',
dest='daemon',
help='Determines whether the bot will daemonize. '
'This is a no-op on non-POSIX systems.')
parser.add_option('', '--allow-default-owner', action='store_true',
dest='allowDefaultOwner',
help='Determines whether the bot will allow its '
'defaultCapabilities not to include "-owner", thus '
'giving all users the owner capability by default. '
' This is dumb, hence we require a command-line '
'option. Don\'t do this.')
parser.add_option('', '--allow-root', action='store_true',
dest='allowRoot',
help='Determines whether the bot will be allowed to run '
'as root. You don\'t want this. Don\'t do it. '
'Even if you think you want it, you don\'t. '
'You\'re probably dumb if you do this.')
parser.add_option('', '--debug', action='store_true', dest='debug',
help='Determines whether some extra debugging stuff will '
'be logged in this script.')
(options, args) = parser.parse_args()
if os.name == 'posix':
if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot:
sys.stderr.write('Dude, don\'t even try to run this as root.\n')
sys.exit(-1)
if len(args) > 1:
parser.error("""Only one configuration option should be specified.""")
elif not args:
parser.error(utils.normalizeWhitespace("""It seems you've given me no
configuration file. If you have a configuration file, be sure to tell
its filename. If you don't have a configuration file, read
docs/GETTING_STARTED and follow its directions."""))
else:
registryFilename = args.pop()
try:
# The registry *MUST* be opened before importing log or conf.
registry.open(registryFilename)
shutil.copy(registryFilename, registryFilename + '.bak')
except registry.InvalidRegistryFile, e:
s = '%s in %s. Please fix this error and start supybot again.' % \
(e, registryFilename)
s = textwrap.fill(s)
sys.stderr.write(s)
sys.stderr.write(os.linesep)
raise
sys.exit(-1)
except EnvironmentError, e:
sys.stderr.write(str(e))
sys.stderr.write(os.linesep)
sys.exit(-1)
try:
import supybot.log as log
except supybot.registry.InvalidRegistryValue, e:
# This is raised here because supybot.log imports supybot.conf.
name = e.value._name
errmsg = textwrap.fill('%s: %s' % (name, e),
width=78, subsequent_indent=' '*len(name))
sys.stderr.write(errmsg)
sys.stderr.write('\n')
sys.stderr.write('Please fix this error in your configuration file '
'and restart your bot.\n')
sys.exit(-1)
import supybot.conf as conf
import supybot.world as world
world.starting = True
def closeRegistry():
# We only print if world.dying so we don't see these messages during
# upkeep.
logger = log.debug
if world.dying:
logger = log.info
logger('Writing registry file to %s', registryFilename)
registry.close(conf.supybot, registryFilename)
logger('Finished writing registry file.')
world.flushers.append(closeRegistry)
world.registryFilename = registryFilename
nick = options.nick or conf.supybot.nick()
user = options.user or conf.supybot.user()
ident = options.ident or conf.supybot.ident()
networks = conf.supybot.networks()
if not networks:
questions.output("""No networks defined. Perhaps you should re-run the
wizard?""", fd=sys.stderr)
# XXX We should turn off logging here for a prettier presentation.
sys.exit(-1)
if options.optimize:
# This doesn't work anymore.
__builtins__.__debug__ = False
if options.optimize > 1:
try:
import psyco
psyco.full()
except ImportError:
log.warning('Psyco isn\'t installed, cannot -OO.')
if os.name == 'posix' and options.daemon:
def fork():
child = os.fork()
if child != 0:
if options.debug:
print 'Parent exiting, child PID: %s' % child
# We must us os._exit instead of sys.exit so atexit handlers
# don't run. They shouldn't be dangerous, but they're ugly.
os._exit(0)
fork()
os.setsid()
# What the heck does this do? I wonder if it breaks anything...
os.umask(0)
# Let's not do this for now (at least until I can make sure it works):
# Actually, let's never do this -- we'll always have files open in the
# bot directories, so they won't be able to be unmounted anyway.
# os.chdir('/')
fork()
# Since this is the indicator that no writing should be done to stdout,
# we'll set it to True before closing stdout et alii.
conf.daemonized = True
# Closing stdin shouldn't cause problems. We'll let it raise an
# exception if it does.
sys.stdin.close()
# Closing these two might cause problems; we log writes to them as
# level WARNING on upkeep.
sys.stdin.close()
sys.stdout.close()
sys.stderr.close()
sys.stdout = StringIO.StringIO()
sys.stderr = StringIO.StringIO()
# We have to be really methodical here.
os.close(0)
os.close(1)
os.close(2)
fd = os.open('/dev/null', os.O_RDWR)
os.dup2(fd, 0)
os.dup2(fd, 1)
os.dup2(fd, 2)
signal.signal(signal.SIGHUP, signal.SIG_IGN)
log.info('Completed daemonization. Current PID: %s', os.getpid())
# Let's write the PID file. This has to go after daemonization, obviously.
pidFile = conf.supybot.pidFile()
if pidFile:
try:
fd = file(pidFile, 'w')
pid = os.getpid()
fd.write('%s\n' % pid)
fd.close()
def removePidFile():
try:
os.remove(pidFile)
except EnvironmentError, e:
log.error('Could not remove pid file: %s', e)
atexit.register(removePidFile)
except EnvironmentError, e:
log.error('Error opening pid file %s: %s', pidFile, e)
conf.allowDefaultOwner = options.allowDefaultOwner
if not os.path.exists(conf.supybot.directories.log()):
os.mkdir(conf.supybot.directories.log())
if not os.path.exists(conf.supybot.directories.conf()):
os.mkdir(conf.supybot.directories.conf())
if not os.path.exists(conf.supybot.directories.data()):
os.mkdir(conf.supybot.directories.data())
if not os.path.exists(conf.supybot.directories.data.tmp()):
os.mkdir(conf.supybot.directories.tmp())
userdataFilename = os.path.join(conf.supybot.directories.conf(),
'userdata.conf')
# Let's open this now since we've got our directories setup.
if not os.path.exists(userdataFilename):
fd = file(userdataFilename, 'w')
fd.write('\n')
fd.close()
registry.open(userdataFilename)
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.drivers as drivers
import supybot.callbacks as callbacks
import supybot.plugins.Owner as Owner
owner = Owner.Class()
irclib._callbacks.append(owner)
if options.profile:
import profile
world.profiling = True
profile.run('main()', '%s-%i.prof' % (nick, time.time()))
else:
main()
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

142
scripts/supybot-adduser Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python
###
# 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 supybot
import supybot.fix as fix
from supybot.questions import *
import os
import sys
import optparse
def main():
import supybot.log as log
import supybot.conf as conf
conf.supybot.log.stdout.setValue(False)
parser = optparse.OptionParser(usage='Usage: %prog [options] <users.conf>',
version='supybot %s' % conf.version)
parser.add_option('-u', '--username', action='store', default='',
dest='name',
help='username for the user.')
parser.add_option('-p', '--password', action='store', default='',
dest='password',
help='password for the user.')
parser.add_option('-x', '--hashed', action='store_const', const=1,
default=0, dest='hashed',
help='hash encrypt the password.')
parser.add_option('-n', '--plain', action='store_const', const=2,
default=0, dest='hashed',
help='store the password in plain text.')
parser.add_option('-c', '--capability', action='append',
dest='capabilities', metavar='CAPABILITY',
help='capability the user should have; '
'this option may be given multiple times.')
(options, args) = parser.parse_args()
if len(args) is not 1:
parser.error('Specify the users.conf file you\'d like to use. '
'Be sure *not* to specify your registry file, generated '
'by supybot-wizard. This is not the file you want. '
'Instead, take a look in your conf directory (usually '
'named "conf") and take a gander at the file '
'"users.conf". That\'s the one you want.')
filename = os.path.abspath(args[0])
conf.supybot.directories.log.setValue('/')
conf.supybot.directories.conf.setValue('/')
conf.supybot.directories.data.setValue('/')
conf.supybot.directories.plugins.setValue(['/'])
conf.supybot.databases.users.filename.setValue(filename)
import supybot.ircdb as ircdb
if not options.name:
name = ''
while not name:
name = something('What is the user\'s name?')
try:
# Check to see if the user is already in the database.
_ = ircdb.users.getUser(name)
# Uh oh. That user already exists;
# otherwise we'd have KeyError'ed.
output('That user already exists. Try another name.')
name = ''
except KeyError:
# Good. No such user exists. We'll pass.
pass
else:
try:
# Same as above. We exit here instead.
_ = ircdb.users.getUser(options.name)
output('That user already exists. Try another name.')
sys.exit(-1)
except KeyError:
name = options.name
if not options.password:
password = getpass('What is %s\'s password? ' % name)
else:
password = options.password
if options.hashed is 0:
hashed = yn('Do you want the password to be hashed instead of '
'storing it as plain text?', default=False)
elif options.hashed is 1:
hashed = True
else:
hashed = False
if not options.capabilities:
capabilities = []
prompt = 'Would you like to give %s a capability?' % name
while yn(prompt):
capabilities.append(anything('What capability?'))
prompt = 'Would you like to give %s another capability?' % name
else:
capabilities = options.capabilities
user = ircdb.users.newUser()
user.name = name
user.setPassword(password, hashed=hashed)
for capability in capabilities:
user.addCapability(capability)
ircdb.users.setUser(user)
ircdb.users.flush()
#os.system('cat %s' % filename) # Was this here just for debugging?
ircdb.users.close()
print 'User %s added.' % name
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass

296
scripts/supybot-newplugin Normal file
View File

@ -0,0 +1,296 @@
#!/usr/bin/env python
###
# 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 supybot
import os
import sys
import os.path
import optparse
def error(s):
sys.stderr.write(textwrap.fill(s))
sys.stderr.write(os.linesep)
sys.exit(-1)
if sys.version_info < (2, 3, 0):
error('This script requires Python 2.3 or newer.')
import supybot.conf as conf
from supybot.questions import *
copyright = '''
###
# Copyright (c) 2004, %s
# 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.
###
'''.strip() # Here we use strip() instead of lstrip() on purpose.
pluginTemplate = '''
%s
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
class %s(%s):
"""Add the help for "@help %s" here."""
%s
Class = %s
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
'''.lstrip() # This removes the newlines that precede and follow the text.
configTemplate = '''
%s
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(%r, True)
%s = conf.registerPlugin(%r)
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(%s, 'someConfigVariableName',
# registry.Boolean(False, """Help for someConfigVariableName."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78
'''.lstrip()
__init__Template = '''
%s
"""
Add a description of the plugin (to be presented to the user inside the wizard)
here.
"""
import supybot
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.authors.unknown
# 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.
# For backwards compatibility with the old Plugin format.
if hasattr(plugin, '__doc__') and plugin.__doc__:
__doc__ = plugin.__doc__
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
'''.lstrip()
testTemplate = '''
%s
from supybot.test import *
class %sTestCase(PluginTestCase):
plugins = (%r,)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
'''.lstrip()
readmeTemplate = '''
Insert a description of your plugin here, with any notes, etc. about using it.
'''.lstrip()
def main():
global copyright
parser = optparse.OptionParser(usage='Usage: %prog [options]',
version='Supybot %s' % conf.version)
parser.add_option('-r', '--regexp', action='store_true', dest='regexp',
help='uses a regexp-based callback.')
parser.add_option('-n', '--name', action='store', dest='name',
help='sets the name for the plugin.')
parser.add_option('-t', '--thread', action='store_true', dest='threaded',
help='makes the plugin threaded.')
parser.add_option('', '--real-name', action='store', dest='realName',
help='Determines what real name the copyright is '
'assigned to.')
(options, args) = parser.parse_args()
if options.name:
name = options.name
if options.regexp:
kind = 'regexp'
else:
kind = 'command'
if options.threaded:
threaded = True
else:
threaded = False
if options.realName:
realName = options.realName
else:
name = something('What should the name of the plugin be?')
if name.endswith('.py'):
name = name[:-3]
while name[0].islower():
print 'Plugin names must begin with a capital.'
name = something('What should the name of the plugin be?')
if name.endswith('.py'):
name = name[:-3]
if os.path.exists(name):
error('A file or directory named %s already exists; remove or '
'rename it and run this program again.' % name)
print textwrap.dedent("""
Supybot offers two major types of plugins: command-based and
regexp-based. Command-based plugins are the kind of plugins
you've seen most when you've used supybot. They're also the most
featureful and easiest to write. Commands can be nested, for
instance, whereas regexp-based callbacks can't do nesting.
That doesn't mean that you'll never want regexp-based callbacks.
They offer a flexibility that command-based callbacks don't
offer; however, they don't tie into the whole system as well.
If you need to combine a command-based callback with some
regexp-based methods, you can do so by subclassing
callbacks.PrivmsgCommandAndRegexp and then adding a class-level
attribute "regexps" that is a sets.Set of methods that are
regexp-based. But you'll have to do that yourself after this
wizard is finished.)""").strip()
print
kind = expect('Do you want a command-based plugin' \
' or a regexp-based plugin?', ['command', 'regexp'])
print textwrap.fill(textwrap.dedent("""
Sometimes you'll want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a significant amount
of time to run, you'll want to thread them so they don't block the
entire bot.""").strip())
print
threaded = yn('Does your plugin need to be threaded?')
realName = something(textwrap.dedent("""
What is your real name, so I can fill in the copyright and license
appropriately?
""").strip())
if threaded:
threaded = 'threaded = True'
else:
threaded = 'pass'
if kind == 'command':
className = 'callbacks.Privmsg'
else:
className = 'callbacks.PrivmsgRegexp'
if name.endswith('.py'):
name = name[:-3]
while name[0].islower():
print 'Plugin names must begin with a capital.'
name = something('What should the name of the plugin be?')
if name.endswith('.py'):
name = name[:-3]
copyright %= realName
# Make the directory.
os.mkdir(name)
def writeFile(filename, s):
fd = file(os.path.join(name, filename), 'w')
try:
fd.write(s)
finally:
fd.close()
writeFile('plugin.py', pluginTemplate % (copyright, name, className,
name, threaded, name))
writeFile('config.py', configTemplate % (copyright, name, name, name, name))
writeFile('__init__.py', __init__Template % copyright)
writeFile('test.py', testTemplate % (copyright, name, name))
writeFile('README.txt', readmeTemplate)
print 'Your new plugin template is in the %s directory.' % name
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print
output("""It looks like you cancelled out of this script before it was
finished. Obviously, nothing was written, but just run this script
again whenever you want to generate a template for a plugin.""")
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

618
scripts/supybot-wizard Normal file
View File

@ -0,0 +1,618 @@
#!/usr/bin/env python
###
# Copyright (c) 2003-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 os
import sys
if sys.version_info < (2, 3, 0):
sys.stderr.write('This program requires Python >= 2.3.0\n')
sys.exit(-1)
import supybot
import supybot.fix as fix
import re
import sets
import time
import pydoc
import pprint
import socket
import logging
import optparse
from itertools import imap
import supybot.ansi as ansi
import supybot.utils as utils
import supybot.ircutils as ircutils
import supybot.registry as registry
import supybot.questions as questions
from supybot.questions import output, yn, anything, something, expect, getpass
def getPlugins(pluginDirs):
filenames = []
for pluginDir in pluginDirs:
try:
filenames.extend(os.listdir(pluginDir))
except OSError:
continue
plugins = sets.Set([])
for filename in filenames:
if filename.endswith('.py') and filename[0].isupper():
plugins.add(os.path.splitext(filename)[0])
plugins.discard('Owner')
plugins = list(plugins)
plugins.sort()
return plugins
def loadPlugin(name):
import supybot.Owner as Owner
try:
module = Owner.loadPluginModule(name)
if hasattr(module, 'Class'):
return module
else:
output("""That plugin loaded fine, but didn't seem to be a real
Supybot plugin; there was no Class variable to tell us what class
to load when we load the plugin. We'll skip over it for now, but
you can always add it later.""")
return None
except Exception, e:
output("""We encountered a bit of trouble trying to load plugin %r.
Python told us %r. We'll skip over it for now, you can always add it
later.""" % (name, utils.exnToString(e)))
return None
def describePlugin(module, showUsage):
if module.__doc__:
output(module.__doc__, unformatted=False)
elif hasattr(module.Class, '__doc__'):
output(module.Class.__doc__, unformatted=False)
else:
output("""Unfortunately, this plugin doesn't seem to have any
documentation. Sorry about that.""")
if showUsage:
if hasattr(module, 'example'):
if yn('This plugin has a usage example. '
'Would you like to see it?', default=False):
pydoc.pager(module.example)
else:
output("""This plugin has no usage example.""")
def clearLoadedPlugins(plugins, pluginRegistry):
for plugin in plugins:
try:
pluginKey = pluginRegistry.get(plugin)
if pluginKey():
plugins.remove(plugin)
except registry.NonExistentRegistryEntry:
continue
_windowsVarRe = re.compile(r'%(\w+)%')
def getDirectoryName(default, basedir=os.curdir):
done = False
while not done:
dir = something('What directory do you want to use?',
default=os.path.join(basedir, default))
orig_dir = dir
dir = os.path.expanduser(dir)
dir = _windowsVarRe.sub(r'$\1', dir)
dir = os.path.expandvars(dir)
dir = os.path.abspath(dir)
try:
os.makedirs(dir)
done = True
except OSError, e:
if e.args[0] != 17: # File exists.
output("""Sorry, I couldn't make that directory for some
reason. The Operating System told me %s. You're going to
have to pick someplace else.""" % e)
else:
done = True
return (dir, os.path.dirname(orig_dir))
def main():
import supybot.log as log
import supybot.conf as conf
log._stdoutHandler.setLevel(100) # *Nothing* gets through this!
parser = optparse.OptionParser(usage='Usage: %prog [options]',
version='Supybot %s' % conf.version)
parser.add_option('', '--allow-root', action='store_true',
dest='allowRoot',
help='Determines whether the wizard will be allowed to '
'run as root. You don\'t want this. Don\'t do it.'
' Even if you think you want it, you don\'t. '
'You\'re probably dumb if you do this.')
parser.add_option('', '--no-network', action='store_false',
dest='network',
help='Determines whether the wizard will be allowed to '
'run without a network connection.')
(options, args) = parser.parse_args()
if os.name == 'posix':
if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot:
sys.stderr.write('Please, don\'t run this as root.\n')
sys.exit(-1)
filename = ''
if args:
parser.error('This program takes no non-option arguments.')
output("""This is a wizard to help you start running supybot. What it
will do is create a single Python file whose effect will be that of
starting an IRC bot with the options you select here. So hold on tight
and be ready to be interrogated :)""")
output("""First of all, we can bold the questions you're asked so you can
easily distinguish the mostly useless blather (like this) from the
questions that you actually have to answer.""")
if yn('Would you like to try this bolding?', default=True):
questions.useBold = True
if not yn('Do you see this in bold?'):
output("""Sorry, it looks like your terminal isn't ANSI compliant.
Try again some other day, on some other terminal :)""")
questions.useBold = False
else:
output("""Great!""")
###
# Preliminary questions.
###
output("""We've got some preliminary things to get out of the way before
we can really start asking you questions that directly relate to what your
bot is going to be like.""")
# Advanced?
output("""We want to know if you consider yourself an advanced Supybot
user because some questions are just utterly boring and useless for new
users. Others might not make sense unless you've used Supybot for some
time.""")
advanced = yn('Are you an advanced Supybot user?', default=False)
### Directories.
# We set these variables in cache because otherwise conf and log will
# create directories for the default values, which might not be what the
# user wants.
output("""Now we've got to ask you some questions about where some of
your directories are (or, perhaps, will be :)). If you're running this
wizard from the directory you'll actually be starting your bot from and
don't mind creating some directories in the current directory, then just
don't give answers to these questions and we'll create the directories we
need right here in this directory.""")
# conf.supybot.directories.log
output("""Your bot will need to put his logs somewhere. Do you have any
specific place you'd like them? If not, just press enter and we'll make
a directory named "logs" right here.""")
(logDir, basedir) = getDirectoryName('logs')
conf.supybot.directories.log.setValue(logDir)
# conf.supybot.directories.data
output("""Your bot will need to put various data somewhere. Things like
databases, downloaded files, etc. Do you have any specific place you'd
like the bot to put these things? If not, just press enter and we'll make
a directory named "data" right here.""")
(dataDir, basedir) = getDirectoryName('data', basedir=basedir)
conf.supybot.directories.data.setValue(dataDir)
# conf.supybot.directories.conf
output("""Your bot must know where to find his configuration files. It'll
probably only make one or two, but it's gotta have some place to put them.
Where should that place be? If you don't care, just press enter and we'll
make a directory right here named "conf" where it'll store his stuff. """)
(confDir, basedir) = getDirectoryName('conf', basedir=basedir)
conf.supybot.directories.conf.setValue(confDir)
# pluginDirs
output("""Your bot will also need to know where to find his plugins at.
Of course, he already knows where the plugins that he came with are, but
your own personal plugins that you write for will probably be somewhere
else.""")
pluginDirs = conf.supybot.directories.plugins()
output("""Currently, the bot knows about the following directories:""")
output(utils.commaAndify(pluginDirs))
while yn('Would you like to add another plugin directory?',
default=False):
(pluginDir, _) = getDirectoryName('plugins', basedir=basedir)
if pluginDir not in pluginDirs:
pluginDirs.append(pluginDir)
conf.supybot.directories.plugins.setValue(pluginDirs)
output("Good! We're done with the directory stuff.")
###
# Bot stuff
###
output("""Now we're going to ask you things that actually relate to the
bot you'll be running.""")
network = None
while not network:
output("""First, we need to know the name of the network you'd like to
connect to. Not the server host, mind you, but the name of the
network. If you plan to connect to irc.freenode.net, for instance, you
should answer this question with 'freenode' (without the quotes).""")
network = something('What IRC network will you be connecting to?')
if '.' in network:
output("""There shouldn't be a '.' in the network name. Remember,
this is the network name, not the actual server you plan to connect
to.""")
network = None
elif not registry.isValidRegistryName(network):
output("""That's not a valid name for one reason or another. Please
pick a simpler name, one more likely to be valid.""")
network = None
conf.supybot.networks.setValue([network])
network = conf.registerNetwork(network)
defaultServer = None
server = None
ip = None
while not ip:
serverString = something('What server would you like to connect to?',
default=defaultServer)
if options.network:
try:
output("""Looking up %s...""" % serverString)
ip = socket.gethostbyname(serverString)
except:
output("""Sorry, I couldn't find that server. Perhaps you
misspelled it? Also, be sure not to put the port in the
server's name -- we'll ask you about that later.""")
else:
ip = 'no network available'
output("""Found %s (%s).""" % (serverString, ip))
output("""IRC Servers almost always accept connections on port
6667. They can, however, accept connections anywhere their admin
feels like he wants to accept connections from.""")
if yn('Does this server require connection on a non-standard port?',
default=False):
port = 0
while not port:
port = something('What port is that?')
try:
i = int(port)
if not (0 < i < 65536):
raise ValueError
except ValueError:
output("""That's not a valid port.""")
port = 0
else:
port = 6667
server = ':'.join([serverString, str(port)])
network.servers.setValue([server])
# conf.supybot.nick
# Force the user into specifying a nick if he didn't have one already
while True:
nick = something('What nick would you like your bot to use?',
default=None)
try:
conf.supybot.nick.set(nick)
break
except registry.InvalidRegistryValue:
output("""That's not a valid nick. Go ahead and pick another.""")
# conf.supybot.user
if advanced:
output("""If you've ever done a /whois on a person, you know that IRC
provides a way for users to show the world their full name. What would
you like your bot's full name to be? If you don't care, just press
enter and it'll be the same as your bot's nick.""")
user = ''
user = something('What would you like your bot\'s full name to be?',
default=nick)
conf.supybot.user.set(user)
# conf.supybot.ident (if advanced)
defaultIdent = 'supybot'
if advanced:
output("""IRC servers also allow you to set your ident, which they
might need if they can't find your identd server. What would you
like your ident to be? If you don't care, press enter and we'll
use 'supybot'. In fact, we prefer that you
do this, because it provides free advertising for Supybot when users
/whois your bot. But, of course, it's your call.""")
while True:
ident = something('What would you like your bot\'s ident to be?',
default=defaultIdent)
try:
conf.supybot.ident.set(ident)
break
except registry.InvalidRegistryValue:
output("""That was not a valid ident.
Go ahead and pick another.""")
else:
conf.supybot.ident.set(defaultIdent)
# conf.supybot.password
output("""Some servers require a password to connect to them. Most
public servers don't. If you try to connect to a server and for some
reason it just won't work, it might be that you need to set a
password.""")
if yn('Do you want to set such a password?', default=False):
network.password.set(getpass())
# conf.supybot.networks.<network>.channels
output("""Of course, having an IRC bot isn't the most useful thing in the
world unless you can make that bot join some channels.""")
if yn('Do you want your bot to join some channels when he connects?',
default=True):
defaultChannels = ' '.join(network.channels())
output("""Separate channels with spaces. If the channel is locked
with a key, follow the channel name with the key separated
by a comma. For example:
#supybot #mychannel,mykey #otherchannel""");
while True:
channels = something('What channels?', default=defaultChannels)
try:
network.channels.set(channels)
break
except registry.InvalidRegistryValue:
# FIXME: say which ones weren't channels.
output("""Not all of those are valid IRC channels. Be sure to
prefix the channel with # (or +, or !, or &, but no one uses
those channels, really). Be sure the channel key (if you are
supplying one) does not contain a comma.""")
else:
network.channels.setValue([])
###
# Plugins
###
def configurePlugin(module, advanced):
if hasattr(module, 'configure'):
output("""Beginning configuration for %s...""" %
module.Class.__name__)
module.configure(advanced)
print # Blank line :)
output("""Done!""")
else:
conf.registerPlugin(module.__name__, currentValue=True)
plugins = getPlugins(pluginDirs)
for s in ('Admin', 'User', 'Channel', 'Misc', 'Config'):
configurePlugin(loadPlugin(s), advanced)
clearLoadedPlugins(plugins, conf.supybot.plugins)
output("""Now we're going to run you through plugin configuration. There's
a variety of plugins in supybot by default, but you can create and
add your own, of course. We'll allow you to take a look at the known
plugins' descriptions and configure them
if you like what you see.""")
# bulk
addedBulk = False
if advanced and yn('Would you like to add plugins en masse first?'):
addedBulk = True
output("""The available plugins are: %s.""" % \
utils.commaAndify(plugins))
output("""What plugins would you like to add? If you've changed your
mind and would rather not add plugins in bulk like this, just press
enter and we'll move on to the individual plugin configuration.""")
massPlugins = anything('Separate plugin names by spaces:')
for name in re.split(r',?\s+', massPlugins):
module = loadPlugin(name)
if module is not None:
configurePlugin(module, advanced)
clearLoadedPlugins(plugins, conf.supybot.plugins)
# individual
if yn('Would you like to look at plugins individually?'):
output("""Next comes your opportunity to learn more about the plugins
that are available and select some (or all!) of them to run in your
bot. Before you have to make a decision, of course, you'll be able to
see a short description of the plugin and, if you choose, an example
session with the plugin. Let's begin.""")
# until we get example strings again, this will default to false
#showUsage =yn('Would you like the option of seeing usage examples?')
showUsage = False
name = expect('What plugin would you like to look at?',
plugins, acceptEmpty=True)
while name:
module = loadPlugin(name)
if module is not None:
describePlugin(module, showUsage)
if yn('Would you like to load this plugin?', default=True):
configurePlugin(module, advanced)
clearLoadedPlugins(plugins, conf.supybot.plugins)
if not yn('Would you like add another plugin?'):
break
name = expect('What plugin would you like to look at?', plugins)
###
# Sundry
###
output("""Although supybot offers a supybot-adduser script, with which
you can add users to your bot's user database, it's *very* important that
you have an owner user for you bot.""")
if yn('Would you like to add an owner user for your bot?', default=True):
import supybot.ircdb as ircdb
name = something('What should the owner\'s username be?')
try:
id = ircdb.users.getUserId(name)
u = ircdb.users.getUser(id)
if u._checkCapability('owner'):
output("""That user already exists, and has owner capabilities
already. Perhaps you added it before? """)
if yn('Do you want to remove its owner capability?',
default=False):
u.removeCapability('owner')
ircdb.users.setUser(id, u)
else:
output("""That user already exists, but doesn't have owner
capabilities.""")
if yn('Do you want to add to it owner capabilities?',
default=False):
u.addCapability('owner')
ircdb.users.setUser(id, u)
except KeyError:
password = getpass('What should the owner\'s password be?')
u = ircdb.users.newUser()
u.name = name
u.setPassword(password)
u.addCapability('owner')
ircdb.users.setUser(u)
output("""Of course, when you're in an IRC channel you can address the bot
by its nick and it will respond, if you give it a valid command (it may or
may not respond, depending on what your config variable replyWhenNotCommand
is set to). But your bot can also respond to a short "prefix character,"
so instead of saying "bot: do this," you can say, "@do this" and achieve
the same effect. Of course, you don't *have* to have a prefix char, but
if the bot ends up participating significantly in your channel, it'll ease
things.""")
if yn('Would you like to set the prefix char(s) for your bot? ',
default=True):
output("""Enter any characters you want here, but be careful: they
should be rare enough that people don't accidentally address the bot
(simply because they'll probably be annoyed if they do address the bot
on accident). You can even have more than one. I (jemfinch) am quite
partial to @, but that's because I've been using it since my ocamlbot
days.""")
import supybot.callbacks as callbacks
c = ''
while not c:
try:
c = anything('What would you like your bot\'s prefix '
'character(s) to be?')
conf.supybot.reply.whenAddressedBy.chars.set(c)
except registry.InvalidRegistryValue, e:
output(str(e))
c = ''
else:
conf.supybot.reply.whenAddressedBy.chars.set('')
###
# logging variables.
###
# conf.supybot.log.stdout
output("""By default, your bot will log not only to files in the logs
directory you gave it, but also to stdout. We find this useful for
debugging, and also just for the pretty output (it's colored!)""")
stdout = not yn('Would you like to turn off this logging to stdout?',
default=False)
conf.supybot.log.stdout.setValue(stdout)
if conf.supybot.log.stdout():
# conf.something
output("""Some terminals may not be able to display the pretty colors
logged to stderr. By default, though, we turn the colors off for
Windows machines and leave it on for *nix machines.""")
if os.name is not 'nt':
conf.supybot.log.stdout.colorized.setValue(
not yn('Would you like to turn this colorization off?',
default=False))
if advanced:
output("""Here's some stuff you only get to choose if you're an
advanced user :)""")
# conf.supybot.log.level
output("""Your bot can handle debug messages at several priorities,
CRITICAL, ERROR, WARNING, INFO, and DEBUG, in decreasing order of
priority. By default, your bot will log all of these priorities except
DEBUG. You can, however, specify that it only log messages above a
certain priority level.""")
priority = str(conf.supybot.log.level)
logLevel = something('What would you like the minimum priority to be?'
' Just press enter to accept the default.',
default=priority).lower()
while logLevel not in ['debug','info','warning','error','critical']:
output("""That's not a valid priority. Valid priorities include
'DEBUG', 'INFO', 'WARNING', 'ERROR', and 'CRITICAL'""")
logLevel = something('What would you like the minimum priority to '
'be? Just press enter to accept the default.',
default=priority).lower()
conf.supybot.log.level.set(logLevel)
# conf.supybot.databases.plugins.channelSpecific
output("""Many plugins in Supybot are channel-specific. Their
databases, likewise, are specific to each channel the bot is in. Many
people don't want this, so we have one central location in which to
say that you would prefer all databases for all channels to be shared.
This variable, supybot.databases.plugins.channelSpecific, is that
place.""")
conf.supybot.databases.plugins.channelSpecific.setValue(
not yn('Would you like plugin databases to be shared by all '
'channels, rather than specific to each channel the '
'bot is in?'))
output("""There are a lot of options we didn't ask you about simply
because we'd rather you get up and running and have time
left to play around with your bot. But come back and see
us! When you've played around with your bot enough to
know what you like, what you don't like, what you'd like
to change, then take a look at your configuration file
when your bot isn't running and read the comments,
tweaking values to your heart's desire.""")
# Let's make sure that src/ plugins are loaded.
conf.registerPlugin('Admin', True)
conf.registerPlugin('Channel', True)
conf.registerPlugin('Config', True)
conf.registerPlugin('Misc', True)
conf.registerPlugin('User', True)
###
# Write the registry
###
conf.supybot.debug.generated.setValue('%s; %s' %
(
if not filename:
filename = '%s.conf' % nick
registry.close(conf.supybot, filename)
# Done!
output("""All done! Your new bot configuration is %s. If you're running
a *nix based OS, you can probably start your bot with the command line
"supybot %s". If you're not running a *nix or similar machine, you'll
just have to start it like you start all your other Python scripts.""" % \
(filename, filename))
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
# We may still be using bold text when exiting during a prompt
if questions.useBold:
import supybot.ansi as ansi
print ansi.RESET
print
print
output("""Well, it looks like you canceled out of the wizard before
it was done. Unfortunately, I didn't get to write anything to file.
Please run the wizard again to completion.""")

150
setup.py Normal file
View File

@ -0,0 +1,150 @@
#!/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.
###
import sys
if sys.version_info < (2, 3, 0):
sys.stderr.write("Supybot requires Python 2.3 or newer.\n")
sys.exit(-1)
import textwrap
clean = False
while '--clean' in sys.argv:
clean = True
sys.argv.remove('--clean')
import glob
import shutil
import os.path
def normalizeWhitespace(s):
return ' '.join(s.split())
try:
from distutils.core import setup
from distutils.sysconfig import get_python_lib
except ImportError, e:
s = normalizeWhitespace("""Supybot requires the distutils package to
install. This package is normally included with Python, but for some
unfathomable reason, many distributions to take it out of standard Python
and put it in another package, usually caled 'python-dev' or python-devel'
or something similar. This is one of the dumbest things a distribution can
do, because it means that developers cannot rely on *STANDARD* Python
modules to be present on systems of that distribution. Complain to your
distribution, and loudly. If you how much of our time we've wasted telling
people to install what should be included by default with Python you'd
understand why we're unhappy about this. Anyway, to reiterate, install the
development package for Python that your distribution supplies.""")
sys.stderr.write(os.linesep*2)
sys.stderr.write(textwrap.fill(s))
sys.stderr.write(os.linesep*2)
sys.exit(-1)
srcFiles = glob.glob(os.path.join('src', '*.py'))
pluginFiles = glob.glob(os.path.join('plugins', '*.py'))
if clean:
previousInstall = os.path.join(get_python_lib(), 'supybot')
if os.path.exists(previousInstall):
try:
print 'Removing current installation.'
shutil.rmtree(previousInstall)
except Exception, e:
print 'Couldn\'t remove former installation: %s' % e
sys.exit(-1)
plugins = [
'Admin',
'Channel',
'Config',
'Misc',
'Owner',
'User',
]
packages = ['supybot',
'supybot.drivers',
'supybot.plugins',] + \
['supybot.plugins.'+s for s in plugins]
package_dir = {'supybot': 'src',
'supybot.plugins': 'plugins',
'supybot.drivers': 'src/drivers',}
for plugin in plugins:
package_dir['supybot.plugins.' + plugin] = 'plugins/' + plugin
version = '0.80.0'
setup(
# Metadata
name='supybot',
version=version,
author='Jeremy Fincher',
url='http://supybot.com/',
author_email='jemfinch@supybot.com',
download_url='http://www.sf.net/project/showfiles.php?group_id=58965',
description='A flexible and extensible Python IRC bot and framework.',
long_description=normalizeWhitespace("""A robust, full-featured Python IRC
bot with a clean and flexible plugin API. Equipped with a complete ACL
system for specifying user permissions with as much as per-command
granularity. Batteries are included in the form of numerous plugins
already written."""),
classifiers = [
'Development Status :: 4 - Beta',
'Environment :: Console',
'Environment :: No Input/Output (Daemon)',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Topic :: Communications :: Chat :: Internet Relay Chat',
'Natural Language :: English',
'Operating System :: OS Independent',
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python',
],
# Installation data
packages=packages,
package_dir=package_dir,
scripts=['scripts/supybot',
'scripts/supybot-wizard',
'scripts/supybot-adduser',
'scripts/supybot-newplugin']
)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

111
src/ansi.py Normal file
View File

@ -0,0 +1,111 @@
###
# 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.
###
"""
ansi.py
ANSI Terminal Interface
Color Usage:
print RED + 'this is red' + RESET
print BOLD + GREEN + WHITEBG + 'this is bold green on white' + RESET
def move(new_x, new_y): 'Move cursor to new_x, new_y'
def moveUp(lines): 'Move cursor up # of lines'
def moveDown(lines): 'Move cursor down # of lines'
def moveForward(chars): 'Move cursor forward # of chars'
def moveBack(chars): 'Move cursor backward # of chars'
def save(): 'Saves cursor position'
def restore(): 'Restores cursor position'
def clear(): 'Clears screen and homes cursor'
def clrtoeol(): 'Clears screen to end of line'
"""
################################
# C O L O R C O N S T A N T S #
################################
BLACK = '\033[30m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'
WHITE = '\033[37m'
RESET = '\033[0;0m'
BOLD = '\033[1m'
REVERSE = '\033[2m'
BLACKBG = '\033[40m'
REDBG = '\033[41m'
GREENBG = '\033[42m'
YELLOWBG = '\033[43m'
BLUEBG = '\033[44m'
MAGENTABG = '\033[45m'
CYANBG = '\033[46m'
WHITEBG = '\033[47m'
#def move(new_x, new_y):
# 'Move cursor to new_x, new_y'
# print '\033[' + str(new_x) + ';' + str(new_y) + 'H'
#
#def moveUp(lines):
# 'Move cursor up # of lines'
# print '\033[' + str(lines) + 'A'
#
#def moveDown(lines):
# 'Move cursor down # of lines'
# print '\033[' + str(lines) + 'B'
#
#def moveForward(chars):
# 'Move cursor forward # of chars'
# print '\033[' + str(chars) + 'C'
#
#def moveBack(chars):
# 'Move cursor backward # of chars'
# print '\033[' + str(chars) + 'D'
#
#def save():
# 'Saves cursor position'
# print '\033[s'
#
#def restore():
# 'Restores cursor position'
# print '\033[u'
#
#def clear():
# 'Clears screen and homes cursor'
# print '\033[2J'
#
#def clrtoeol():
# 'Clears screen to end of line'
# print '\033[K'
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

1394
src/callbacks.py Normal file

File diff suppressed because it is too large Load Diff

475
src/cdb.py Normal file
View File

@ -0,0 +1,475 @@
###
# 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.
###
"""
Database module, similar to dbhash. Uses a format similar to (if not entirely
the same as) DJB's CDB <http://cr.yp.to/cdb.html>.
"""
import supybot.fix as fix
import os
import sys
import sets
import struct
import os.path
import cPickle as pickle
import supybot.utils as utils
def hash(s):
"""DJB's hash function for CDB."""
h = 5381
for c in s:
h = ((h + (h << 5)) ^ ord(c)) & 0xFFFFFFFFL
return h
def unpack2Ints(s):
"""Returns two ints unpacked from the binary string s."""
return struct.unpack('<LL', s)
def pack2Ints(i, j):
"""Returns a packed binary string from the two ints."""
return struct.pack('<LL', i, j)
def dump(map, fd=sys.stdout):
"""Dumps a dictionary-structure in CDB format."""
for (key, value) in map.iteritems():
fd.write('+%s,%s:%s->%s\n' % (len(key), len(value), key, value))
def open(filename, mode='r', **kwargs):
"""Opens a database; used for compatibility with other database modules."""
if mode == 'r':
return Reader(filename, **kwargs)
elif mode == 'w':
return ReaderWriter(filename, **kwargs)
elif mode == 'c':
if os.path.exists(filename):
return ReaderWriter(filename, **kwargs)
else:
maker = Maker(filename)
maker.finish()
return ReaderWriter(filename, **kwargs)
elif mode == 'n':
maker = Maker(filename)
maker.finish()
return ReaderWriter(filename, **kwargs)
else:
raise ValueError, 'Invalid flag: %s' % mode
def shelf(filename, *args, **kwargs):
"""Opens a new shelf database object."""
if os.path.exists(filename):
return Shelf(filename, *args, **kwargs)
else:
maker = Maker(filename)
maker.finish()
return Shelf(filename, *args, **kwargs)
def _readKeyValue(fd):
klen = 0
dlen = 0
s = initchar = fd.read(1)
if s == '':
return (None, None, None)
s = fd.read(1)
while s != ',':
klen = 10 * klen + int(s)
s = fd.read(1)
s = fd.read(1)
while s != ':':
dlen = 10 * dlen + int(s)
s = fd.read(1)
key = fd.read(klen)
assert fd.read(2) == '->'
value = fd.read(dlen)
assert fd.read(1) == '\n'
return (initchar, key, value)
def make(dbFilename, readFilename=None):
"""Makes a database from the filename, otherwise uses stdin."""
if readFilename is None:
readfd = sys.stdin
else:
readfd = file(readFilename, 'r')
maker = Maker(dbFilename)
while 1:
(initchar, key, value) = _readKeyValue(readfd)
if initchar is None:
break
assert initchar == '+'
maker.add(key, value)
readfd.close()
maker.finish()
class Maker(object):
"""Class for making CDB databases."""
def __init__(self, filename):
self.fd = utils.transactionalFile(filename)
self.filename = filename
self.fd.seek(2048)
self.hashPointers = [(0, 0)] * 256
#self.hashes = [[]] * 256 # Can't use this, [] stays the same...
self.hashes = []
for _ in xrange(256):
self.hashes.append([])
def add(self, key, data):
"""Adds a key->value pair to the database."""
h = hash(key)
hashPointer = h % 256
startPosition = self.fd.tell()
self.fd.write(pack2Ints(len(key), len(data)))
self.fd.write(key)
self.fd.write(data)
self.hashes[hashPointer].append((h, startPosition))
def finish(self):
"""Finishes the current Maker object.
Writes the remainder of the database to disk.
"""
for i in xrange(256):
hash = self.hashes[i]
self.hashPointers[i] = (self.fd.tell(), self._serializeHash(hash))
self._serializeHashPointers()
self.fd.flush()
self.fd.close()
def _serializeHash(self, hash):
hashLen = len(hash) * 2
a = [(0, 0)] * hashLen
for (h, pos) in hash:
i = (h / 256) % hashLen
while a[i] != (0, 0):
i = (i + 1) % hashLen
a[i] = (h, pos)
for (h, pos) in a:
self.fd.write(pack2Ints(h, pos))
return hashLen
def _serializeHashPointers(self):
self.fd.seek(0)
for (hashPos, hashLen) in self.hashPointers:
self.fd.write(pack2Ints(hashPos, hashLen))
class Reader(utils.IterableMap):
"""Class for reading from a CDB database."""
def __init__(self, filename):
self.filename = filename
self.fd = file(filename, 'r')
self.loop = 0
self.khash = 0
self.kpos = 0
self.hpos = 0
self.hslots = 0
self.dpos = 0
self.dlen = 0
def close(self):
self.fd.close()
def _read(self, len, pos):
self.fd.seek(pos)
return self.fd.read(len)
def _match(self, key, pos):
return self._read(len(key), pos) == key
def iteritems(self):
# uses loop/hslots in a strange, non-re-entrant manner.
(self.loop,) = struct.unpack('<i', self._read(4, 0))
self.hslots = 2048
while self.hslots < self.loop:
(klen, dlen) = unpack2Ints(self._read(8, self.hslots))
dpos = self.hslots + 8 + klen
ret = (self._read(klen, self.hslots+8), self._read(dlen, dpos))
self.hslots = dpos + dlen
yield ret
self.loop = 0
self.hslots = 0
def _findnext(self, key):
if not self.loop:
self.khash = hash(key)
(self.hpos, self.hslots) = unpack2Ints(self._read(8,
(self.khash * 8) & 2047))
if not self.hslots:
return False
self.kpos = self.hpos + (((self.khash / 256) % self.hslots) * 8)
while self.loop < self.hslots:
(h, p) = unpack2Ints(self._read(8, self.kpos))
if p == 0:
return False
self.loop += 1
self.kpos += 8
if self.kpos == self.hpos + (self.hslots * 8):
self.kpos = self.hpos
if h == self.khash:
(u, self.dlen) = unpack2Ints(self._read(8, p))
if u == len(key):
if self._match(key, p+8):
self.dpos = p + 8 + u
return True
return False
def _find(self, key, loop=0):
self.loop = loop
return self._findnext(key)
def _getCurrentData(self):
return self._read(self.dlen, self.dpos)
def find(self, key, loop=0):
if self._find(key, loop=loop):
return self._getCurrentData()
else:
try:
return self.default
except AttributeError:
raise KeyError, key
def findall(self, key):
ret = []
while self._findnext(key):
ret.append(self._getCurrentData())
return ret
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def __len__(self):
(start,) = struct.unpack('<i', self._read(4, 0))
self.fd.seek(0, 2)
return ((self.fd.tell() - start) / 16)
has_key = _find
__contains__ = has_key
__getitem__ = find
class ReaderWriter(utils.IterableMap):
"""Uses a journal to pretend that a CDB is writable database."""
def __init__(self, filename, journalName=None, maxmods=0):
if journalName is None:
journalName = filename + '.journal'
self.journalName = journalName
self.maxmods = maxmods
self.mods = 0
self.filename = filename
self._readJournal()
self._openFiles()
self.adds = {}
self.removals = sets.Set()
def _openFiles(self):
self.cdb = Reader(self.filename)
self.journal = file(self.journalName, 'w')
def _closeFiles(self):
self.cdb.close()
self.journal.close()
def _journalRemoveKey(self, key):
s = '-%s,%s:%s->%s\n' % (len(key), 0, key, '')
self.journal.write(s)
self.journal.flush()
def _journalAddKey(self, key, value):
s = '+%s,%s:%s->%s\n' % (len(key), len(value), key, value)
self.journal.write(s)
self.journal.flush()
def _readJournal(self):
removals = sets.Set()
adds = {}
try:
fd = file(self.journalName, 'r')
while 1:
(initchar, key, value) = _readKeyValue(fd)
if initchar is None:
break
elif initchar == '+':
if key in removals:
removals.remove(key)
adds[key] = value
elif initchar == '-':
if key in adds:
del adds[key]
removals.add(key)
fd.close()
except IOError:
pass
if removals or adds:
maker = Maker(self.filename)
cdb = Reader(self.filename)
for (key, value) in cdb.iteritems():
if key in removals:
continue
elif key in adds:
value = adds[key]
if value is not None:
maker.add(key, value)
adds[key] = None
else:
maker.add(key, value)
for (key, value) in adds.iteritems():
if value is not None:
maker.add(key, value)
cdb.close()
maker.finish()
if os.path.exists(self.journalName):
os.remove(self.journalName)
def close(self):
self.flush()
self._closeFiles()
def flush(self):
self._closeFiles()
self._readJournal()
self._openFiles()
def _flushIfOverLimit(self):
if self.maxmods:
if isinstance(self.maxmods, int):
if self.mods > self.maxmods:
self.flush()
self.mods = 0
elif isinstance(self.maxmods, float):
assert 0 <= self.maxmods
if self.mods / max(len(self.cdb), 100) > self.maxmods:
self.flush()
self.mods = 0
def __getitem__(self, key):
if key in self.removals:
raise KeyError, key
else:
try:
return self.adds[key]
except KeyError:
return self.cdb[key] # If this raises KeyError, we lack key.
def __delitem__(self, key):
if key in self.removals:
raise KeyError, key
else:
if key in self.adds and key in self.cdb:
self._journalRemoveKey(key)
del self.adds[key]
self.removals.add(key)
elif key in self.adds:
self._journalRemoveKey(key)
del self.adds[key]
elif key in self.cdb:
self._journalRemoveKey(key)
else:
raise KeyError, key
self.mods += 1
self._flushIfOverLimit()
def __setitem__(self, key, value):
if key in self.removals:
self.removals.remove(key)
self._journalAddKey(key, value)
self.adds[key] = value
self.mods += 1
self._flushIfOverLimit()
def __contains__(self, key):
if key in self.removals:
return False
else:
return key in self.adds or key in self.cdb
has_key = __contains__
def iteritems(self):
already = sets.Set()
for (key, value) in self.cdb.iteritems():
if key in self.removals or key in already:
continue
elif key in self.adds:
already.add(key)
yield (key, self.adds[key])
else:
yield (key, value)
for (key, value) in self.adds.iteritems():
if key not in already:
yield (key, value)
def setdefault(self, key, value):
try:
return self[key]
except KeyError:
self[key] = value
return value
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
class Shelf(ReaderWriter):
"""Uses pickle to mimic the shelf module."""
def __getitem__(self, key):
return pickle.loads(ReaderWriter.__getitem__(self, key))
def __setitem__(self, key, value):
ReaderWriter.__setitem__(self, key, pickle.dumps(value, True))
def iteritems(self):
for (key, value) in ReaderWriter.iteritems(self):
yield (key, pickle.loads(value))
if __name__ == '__main__':
if sys.argv[0] == 'cdbdump':
if len(sys.argv) == 2:
fd = file(sys.argv[1], 'r')
else:
fd = sys.stdin
db = Reader(fd)
dump(db)
elif sys.argv[0] == 'cdbmake':
if len(sys.argv) == 2:
make(sys.argv[1])
else:
make(sys.argv[1], sys.argv[2])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

893
src/commands.py Normal file
View File

@ -0,0 +1,893 @@
###
# 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.
###
"""
Includes wrappers for commands.
"""
import supybot.fix as fix
import time
import types
import getopt
import threading
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
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.webutils as webutils
import supybot.callbacks as callbacks
import supybot.structures as structures
###
# Non-arg wrappers -- these just change the behavior of a command without
# changing the arguments given to it.
###
# Thread has to be a non-arg wrapper because by the time we're parsing and
# validating arguments, we're inside the function we'd want to thread.
def thread(f):
"""Makes sure a command spawns a thread when called."""
def newf(self, irc, msg, args, *L, **kwargs):
if world.isMainThread():
t = callbacks.CommandThread(target=irc._callCommand,
args=(f.func_name, self),
kwargs=kwargs)
t.start()
else:
f(self, irc, msg, args, *L, **kwargs)
return utils.changeFunctionName(newf, f.func_name, f.__doc__)
class UrlSnarfThread(world.SupyThread):
def __init__(self, *args, **kwargs):
assert 'url' in kwargs
kwargs['name'] = 'Thread #%s (for snarfing %s)' % \
(world.threadsSpawned, kwargs.pop('url'))
super(UrlSnarfThread, self).__init__(*args, **kwargs)
self.setDaemon(True)
def run(self):
try:
super(UrlSnarfThread, self).run()
except webutils.WebError, e:
log.debug('Exception in urlSnarfer: %s' % utils.exnToString(e))
class SnarfQueue(ircutils.FloodQueue):
timeout = conf.supybot.snarfThrottle
def key(self, channel):
return channel
_snarfed = SnarfQueue()
class SnarfIrc(object):
def __init__(self, irc, channel, url):
self.irc = irc
self.url = url
self.channel = channel
def __getattr__(self, attr):
return getattr(self.irc, attr)
def reply(self, *args, **kwargs):
_snarfed.enqueue(self.channel, self.url)
return self.irc.reply(*args, **kwargs)
# This lock is used to serialize the calls to snarfers, so
# earlier snarfers are guaranteed to beat out later snarfers.
_snarfLock = threading.Lock()
def urlSnarfer(f):
"""Protects the snarfer from loops (with other bots) and whatnot."""
def newf(self, irc, msg, match, *L, **kwargs):
url = match.group(0)
channel = msg.args[0]
if not irc.isChannel(channel):
return
if ircdb.channels.getChannel(channel).lobotomized:
self.log.info('Not snarfing in %s: lobotomized.', channel)
return
if _snarfed.has(channel, url):
self.log.info('Throttling snarf of %s in %s.', url, channel)
return
irc = SnarfIrc(irc, channel, url)
def doSnarf():
_snarfLock.acquire()
try:
if msg.repliedTo:
self.log.debug('Not snarfing, msg is already repliedTo.')
return
f(self, irc, msg, match, *L, **kwargs)
finally:
_snarfLock.release()
if threading.currentThread() is not world.mainThread:
doSnarf()
else:
L = list(L)
t = UrlSnarfThread(target=doSnarf, url=url)
t.start()
newf = utils.changeFunctionName(newf, f.func_name, f.__doc__)
return newf
###
# Converters, which take irc, msg, args, and a state object, and build up the
# validated and converted args for the method in state.args.
###
# This is just so we can centralize this, since it may change.
def _int(s):
base = 10
if s.startswith('0x'):
base = 16
s = s[2:]
elif s.startswith('0b'):
base = 2
s = s[2:]
elif s.startswith('0') and len(s) > 1:
base = 8
s = s[1:]
try:
return int(s, base)
except ValueError:
if base == 10:
return int(float(s))
else:
raise
def getInt(irc, msg, args, state, type='integer', p=None):
try:
i = _int(args[0])
if p is not None:
if not p(i):
irc.errorInvalid(type, args[0])
state.args.append(i)
del args[0]
except ValueError:
irc.errorInvalid(type, args[0])
def getNonInt(irc, msg, args, state, type='non-integer value'):
try:
i = _int(args[0])
irc.errorInvalid(type, args[0])
except ValueError:
state.args.append(args.pop(0))
def getLong(irc, msg, args, state, type='long'):
getInt(irc, msg, args, state, type)
state.args[-1] = long(state.args[-1])
def getFloat(irc, msg, args, state, type='floating point number'):
try:
state.args.append(float(args[0]))
del args[0]
except ValueError:
irc.errorInvalid(type, args[0])
def getPositiveInt(irc, msg, args, state, *L):
getInt(irc, msg, args, state,
p=lambda i: i>0, type='positive integer', *L)
def getNonNegativeInt(irc, msg, args, state, *L):
getInt(irc, msg, args, state,
p=lambda i: i>=0, type='non-negative integer', *L)
def getIndex(irc, msg, args, state):
getInt(irc, msg, args, state, type='index')
if state.args[-1] > 0:
state.args[-1] -= 1
def getId(irc, msg, args, state, kind=None):
type = 'id'
if kind is not None and not kind.endswith('id'):
type = kind + ' id'
original = args[0]
try:
args[0] = args[0].lstrip('#')
getInt(irc, msg, args, state, type=type)
except Exception, e:
args[0] = original
raise
def getExpiry(irc, msg, args, state):
now = int(time.time())
try:
expires = _int(args[0])
if expires:
expires += now
state.args.append(expires)
del args[0]
except ValueError:
irc.errorInvalid('number of seconds', args[0])
def getBoolean(irc, msg, args, state):
try:
state.args.append(utils.toBool(args[0]))
del args[0]
except ValueError:
irc.errorInvalid('boolean', args[0])
def getNetworkIrc(irc, msg, args, state, errorIfNoMatch=False):
if args:
for otherIrc in world.ircs:
if otherIrc.network.lower() == args[0].lower():
state.args.append(otherIrc)
del args[0]
return
if errorIfNoMatch:
raise callbacks.ArgumentError
else:
state.args.append(irc)
def getHaveOp(irc, msg, args, state, action='do that'):
if state.channel not in irc.state.channels:
irc.error('I\'m not even in %s.' % state.channel, Raise=True)
if not irc.state.channels[state.channel].isOp(irc.nick):
irc.error('I need to be opped to %s.' % action, Raise=True)
def validChannel(irc, msg, args, state):
if irc.isChannel(args[0]):
state.args.append(args.pop(0))
else:
irc.errorInvalid('channel', args[0])
def getHostmask(irc, msg, args, state):
if ircutils.isUserHostmask(args[0]):
state.args.append(args.pop(0))
else:
try:
hostmask = irc.state.nickToHostmask(args[0])
state.args.append(hostmask)
del args[0]
except KeyError:
irc.errorInvalid('nick or hostmask', args[0])
def getBanmask(irc, msg, args, state):
getHostmask(irc, msg, args, state)
# XXX Channel-specific stuff.
state.args[-1] = ircutils.banmask(state.args[-1])
def getUser(irc, msg, args, state):
try:
state.args.append(ircdb.users.getUser(msg.prefix))
except KeyError:
irc.errorNotRegistered(Raise=True)
def getOtherUser(irc, msg, args, state):
if ircutils.isUserHostmask(args[0]):
irc.errorNoUser(args[0])
try:
state.args.append(ircdb.users.getUser(args[0]))
del args[0]
except KeyError:
try:
getHostmask(irc, msg, [args[0]], state)
hostmask = state.args.pop()
state.args.append(ircdb.users.getUser(hostmask))
del args[0]
except (KeyError, callbacks.Error):
irc.errorNoUser(name=args[0])
def _getRe(f):
def get(irc, msg, args, state, convert=True):
original = args[:]
s = args.pop(0)
def isRe(s):
try:
_ = f(s)
return True
except ValueError:
return False
try:
while len(s) < 512 and not isRe(s):
s += ' ' + args.pop(0)
if len(s) < 512:
if convert:
state.args.append(f(s))
else:
state.args.append(s)
else:
irc.errorInvalid('regular expression', s)
except IndexError:
args[:] = original
irc.errorInvalid('regular expression', s)
return get
getMatcher = _getRe(utils.perlReToPythonRe)
getReplacer = _getRe(utils.perlReToReplacer)
def getNick(irc, msg, args, state):
if ircutils.isNick(args[0]):
if 'nicklen' in irc.state.supported:
if len(args[0]) > irc.state.supported['nicklen']:
irc.errorInvalid('nick', args[0],
'That nick is too long for this server.')
state.args.append(args.pop(0))
else:
irc.errorInvalid('nick', args[0])
def getSeenNick(irc, msg, args, state, errmsg=None):
try:
_ = irc.state.nickToHostmask(args[0])
state.args.append(args.pop(0))
except KeyError:
if errmsg is None:
errmsg = 'I haven\'t seen %s.' % args[0]
irc.error(errmsg, Raise=True)
def getChannel(irc, msg, args, state):
if args and irc.isChannel(args[0]):
channel = args.pop(0)
elif irc.isChannel(msg.args[0]):
channel = msg.args[0]
else:
state.log.debug('Raising ArgumentError because there is no channel.')
raise callbacks.ArgumentError
state.channel = channel
state.args.append(channel)
def getChannelDb(irc, msg, args, state, **kwargs):
channelSpecific = conf.supybot.databases.plugins.channelSpecific
try:
getChannel(irc, msg, args, state, **kwargs)
channel = channelSpecific.getChannelLink(state.channel)
state.channel = channel
state.args[-1] = channel
except (callbacks.ArgumentError, IndexError):
if channelSpecific():
raise
channel = channelSpecific.link()
if not conf.get(channelSpecific.link.allow, channel):
log.warning('channelSpecific.link is globally set to %s, but '
'%s disallows linking to its db.' % (channel, channel))
raise
else:
channel = channelSpecific.getChannelLink(channel)
state.channel = channel
state.args.append(channel)
def inChannel(irc, msg, args, state):
if not state.channel:
getChannel(irc, msg, args, state)
if state.channel not in irc.state.channels:
irc.error('I\'m not in %s.' % state.channel, Raise=True)
def onlyInChannel(irc, msg, args, state):
if not (irc.isChannel(msg.args[0]) and msg.args[0] in irc.state.channels):
irc.error('This command may only be given in a channel that I am in.',
Raise=True)
else:
state.channel = msg.args[0]
state.args.append(state.channel)
def callerInGivenChannel(irc, msg, args, state):
channel = args[0]
if irc.isChannel(channel):
if channel in irc.state.channels:
if msg.nick in irc.state.channels[channel].users:
state.args.append(args.pop(0))
else:
irc.error('You must be in %s.' % channel, Raise=True)
else:
irc.error('I\'m not in %s.' % channel, Raise=True)
else:
irc.errorInvalid('channel', args[0])
def nickInChannel(irc, msg, args, state):
inChannel(irc, msg, args, state)
if args[0] not in irc.state.channels[state.channel].users:
irc.error('%s is not in %s.' % (args[0], state.channel), Raise=True)
state.args.append(args.pop(0))
def getChannelOrNone(irc, msg, args, state):
try:
getChannel(irc, msg, args, state)
except callbacks.ArgumentError:
state.args.append(None)
def checkChannelCapability(irc, msg, args, state, cap):
if not state.channel:
getChannel(irc, msg, args, state)
cap = ircdb.canonicalCapability(cap)
cap = ircdb.makeChannelCapability(state.channel, cap)
if not ircdb.checkCapability(msg.prefix, cap):
irc.errorNoCapability(cap, Raise=True)
def getLowered(irc, msg, args, state):
state.args.append(ircutils.toLower(args.pop(0)))
def getSomething(irc, msg, args, state, errorMsg=None, p=None):
if p is None:
p = lambda _: True
if not args[0] or not p(args[0]):
if errorMsg is None:
errorMsg = 'You must not give the empty string as an argument.'
irc.error(errorMsg, Raise=True)
else:
state.args.append(args.pop(0))
def getSomethingNoSpaces(irc, msg, args, state, *L):
def p(s):
return len(s.split(None, 1)) == 1
getSomething(irc, msg, args, state, p=p, *L)
def private(irc, msg, args, state):
if irc.isChannel(msg.args[0]):
irc.errorRequiresPrivacy(Raise=True)
def public(irc, msg, args, state, errmsg=None):
if not irc.isChannel(msg.args[0]):
if errmsg is None:
errmsg = 'This message must be sent in a channel.'
irc.error(errmsg, Raise=True)
def checkCapability(irc, msg, args, state, cap):
cap = ircdb.canonicalCapability(cap)
if not ircdb.checkCapability(msg.prefix, cap):
irc.errorNoCapability(cap, Raise=True)
def owner(irc, msg, args, state):
checkCapability(irc, msg, args, state, 'owner')
def admin(irc, msg, args, state):
checkCapability(irc, msg, args, state, 'admin')
def anything(irc, msg, args, state):
state.args.append(args.pop(0))
def getGlob(irc, msg, args, state):
glob = args.pop(0)
if '*' not in glob and '?' not in glob:
glob = '*%s*' % glob
state.args.append(glob)
def getUrl(irc, msg, args, state):
if webutils.urlRe.match(args[0]):
state.args.append(args.pop(0))
else:
irc.errorInvalid('url', args[0])
def getHttpUrl(irc, msg, args, state):
if webutils.urlRe.match(args[0]) and args[0].startswith('http://'):
state.args.append(args.pop(0))
else:
irc.errorInvalid('http url', args[0])
def getNow(irc, msg, args, state):
state.args.append(int(time.time()))
def getCommandName(irc, msg, args, state):
if ' ' in args[0]:
irc.errorInvalid('command name', args[0])
else:
state.args.append(callbacks.canonicalName(args.pop(0)))
def getIp(irc, msg, args, state):
if utils.isIP(args[0]):
state.args.append(args.pop(0))
else:
irc.errorInvalid('ip', args[0])
def getLetter(irc, msg, args, state):
if len(args[0]) == 1:
state.args.append(args.pop(0))
else:
irc.errorInvalid('letter', args[0])
def getMatch(irc, msg, args, state, regexp, errmsg):
m = regexp.search(args[0])
if m is not None:
state.args.append(m)
del args[0]
else:
irc.error(errmsg, Raise=True)
def getLiteral(irc, msg, args, state, literals, errmsg=None):
# ??? Should we allow abbreviations?
if isinstance(literals, basestring):
literals = (literals,)
abbrevs = utils.abbrev(literals)
if args[0] in abbrevs:
state.args.append(abbrevs[args.pop(0)])
elif errmsg is not None:
irc.error(errmsg, Raise=True)
else:
raise callbacks.ArgumentError
def getTo(irc, msg, args, state):
if args[0].lower() == 'to':
args.pop(0)
def getPlugin(irc, msg, args, state, require=True):
cb = irc.getCallback(args[0])
if cb is not None:
state.args.append(cb)
del args[0]
elif require:
irc.errorInvalid('plugin', args[0])
else:
state.args.append(None)
def getIrcColor(irc, msg, args, state):
if args[0] in ircutils.mircColors:
state.args.append(ircutils.mircColors[args.pop(0)])
else:
irc.errorInvalid('irc color')
def getText(irc, msg, args, state):
if args:
state.args.append(' '.join(args))
args[:] = []
else:
raise IndexError
wrappers = ircutils.IrcDict({
'id': getId,
'ip': getIp,
'int': getInt,
'index': getIndex,
'color': getIrcColor,
'now': getNow,
'url': getUrl,
'httpUrl': getHttpUrl,
'long': getLong,
'float': getFloat,
'nonInt': getNonInt,
'positiveInt': getPositiveInt,
'nonNegativeInt': getNonNegativeInt,
'letter': getLetter,
'haveOp': getHaveOp,
'expiry': getExpiry,
'literal': getLiteral,
'to': getTo,
'nick': getNick,
'seenNick': getSeenNick,
'channel': getChannel,
'inChannel': inChannel,
'onlyInChannel': onlyInChannel,
'nickInChannel': nickInChannel,
'networkIrc': getNetworkIrc,
'callerInGivenChannel': callerInGivenChannel,
'plugin': getPlugin,
'boolean': getBoolean,
'lowered': getLowered,
'anything': anything,
'something': getSomething,
'filename': getSomething, # XXX Check for validity.
'commandName': getCommandName,
'text': getText,
'glob': getGlob,
'somethingWithoutSpaces': getSomethingNoSpaces,
'capability': getSomethingNoSpaces,
'channelDb': getChannelDb,
'hostmask': getHostmask,
'banmask': getBanmask,
'user': getUser,
'matches': getMatch,
'public': public,
'private': private,
'otherUser': getOtherUser,
'regexpMatcher': getMatcher,
'validChannel': validChannel,
'regexpReplacer': getReplacer,
'owner': owner,
'admin': admin,
'checkCapability': checkCapability,
'checkChannelCapability': checkChannelCapability,
})
def addConverter(name, wrapper):
wrappers[name] = wrapper
class UnknownConverter(KeyError):
pass
def getConverter(name):
try:
return wrappers[name]
except KeyError, e:
raise UnknownConverter, str(e)
def callConverter(name, irc, msg, args, state, *L):
getConverter(name)(irc, msg, args, state, *L)
###
# Contexts. These determine what the nature of conversions is; whether they're
# defaulted, or many of them are allowed, etc. Contexts should be reusable;
# i.e., they should not maintain state between calls.
###
def contextify(spec):
if not isinstance(spec, context):
spec = context(spec)
return spec
def setDefault(state, default):
if callable(default):
state.args.append(default())
else:
state.args.append(default)
class context(object):
def __init__(self, spec):
self.args = ()
self.spec = spec # for repr
if isinstance(spec, tuple):
assert spec, 'tuple spec must not be empty.'
self.args = spec[1:]
self.converter = getConverter(spec[0])
elif spec is None:
self.converter = getConverter('anything')
elif isinstance(spec, basestring):
self.args = ()
self.converter = getConverter(spec)
else:
assert isinstance(spec, context)
self.converter = spec
def __call__(self, irc, msg, args, state):
log.debug('args before %r: %r', self, args)
self.converter(irc, msg, args, state, *self.args)
log.debug('args after %r: %r', self, args)
def __repr__(self):
return '<%s for %s>' % (self.__class__.__name__, self.spec)
class rest(context):
def __call__(self, irc, msg, args, state):
if args:
original = args[:]
args[:] = [' '.join(args)]
try:
super(rest, self).__call__(irc, msg, args, state)
except Exception, e:
args[:] = original
else:
raise callbacks.ArgumentError
# additional means: Look for this (and make sure it's of this type). If
# there are no arguments for us to check, then use our default.
class additional(context):
def __init__(self, spec, default=None):
self.__parent = super(additional, self)
self.__parent.__init__(spec)
self.default = default
def __call__(self, irc, msg, args, state):
try:
self.__parent.__call__(irc, msg, args, state)
except IndexError:
log.debug('Got IndexError, returning default.')
setDefault(state, self.default)
# optional means: Look for this, but if it's not the type I'm expecting or
# there are no arguments for us to check, then use the default value.
class optional(additional):
def __call__(self, irc, msg, args, state):
try:
super(optional, self).__call__(irc, msg, args, state)
except (callbacks.ArgumentError, callbacks.Error), e:
log.debug('Got %s, returning default.', utils.exnToString(e))
setDefault(state, self.default)
class any(context):
def __init__(self, spec, continueOnError=False):
self.__parent = super(any, self)
self.__parent.__init__(spec)
self.continueOnError = continueOnError
def __call__(self, irc, msg, args, state):
st = state.essence()
try:
while args:
self.__parent.__call__(irc, msg, args, st)
except IndexError:
pass
except (callbacks.ArgumentError, callbacks.Error), e:
if not self.continueOnError:
raise
else:
log.debug('Got %s, returning default.', utils.exnToString(e))
pass
state.args.append(st.args)
class many(any):
def __call__(self, irc, msg, args, state):
super(many, self).__call__(irc, msg, args, state)
if not state.args[-1]:
state.args.pop()
raise callbacks.ArgumentError
class first(context):
def __init__(self, *specs, **kw):
if 'default' in kw:
self.default = kw.pop('default')
assert not kw, 'Bad kwargs for first.__init__'
self.specs = map(contextify, specs)
def __call__(self, irc, msg, args, state):
for spec in self.specs:
try:
spec(irc, msg, args, state)
return
except Exception, e:
continue
if hasattr(self, 'default'):
state.args.append(self.default)
else:
raise e
class reverse(context):
def __call__(self, irc, msg, args, state):
args[:] = args[::-1]
super(reverse, self).__call__(irc, msg, args, state)
args[:] = args[::-1]
class commalist(context):
def __call__(self, irc, msg, args, state):
original = args[:]
st = state.essence()
trailingComma = True
try:
while trailingComma:
arg = args.pop(0)
if not arg.endswith(','):
trailingComma = False
for part in arg.split(','):
if part: # trailing commas
super(commalist, self).__call__(irc, msg, [part], st)
state.args.append(st.args)
except Exception, e:
args[:] = original
raise
class getopts(context):
"""The empty string indicates that no argument is taken; None indicates
that there is no converter for the argument."""
def __init__(self, getopts):
self.spec = getopts # for repr
self.getopts = {}
self.getoptL = []
for (name, spec) in getopts.iteritems():
if spec == '':
self.getoptL.append(name)
self.getopts[name] = None
else:
self.getoptL.append(name + '=')
self.getopts[name] = contextify(spec)
log.debug('getopts: %r', self.getopts)
log.debug('getoptL: %r', self.getoptL)
def __call__(self, irc, msg, args, state):
log.debug('args before %r: %r', self, args)
(optlist, rest) = getopt.getopt(args, '', self.getoptL)
getopts = []
for (opt, arg) in optlist:
opt = opt[2:] # Strip --
log.debug('opt: %r, arg: %r', opt, arg)
context = self.getopts[opt]
if context is not None:
st = state.essence()
context(irc, msg, [arg], st)
assert len(st.args) == 1
getopts.append((opt, st.args[0]))
else:
getopts.append((opt, True))
state.args.append(getopts)
args[:] = rest
log.debug('args after %r: %r', self, args)
###
# This is our state object, passed to converters along with irc, msg, and args.
###
class State(object):
log = log
def __init__(self, types):
self.args = []
self.kwargs = {}
self.types = types
self.channel = None
def essence(self):
st = State(self.types)
for (attr, value) in self.__dict__.iteritems():
if attr not in ('args', 'kwargs'):
setattr(st, attr, value)
return st
def __repr__(self):
return '%s(args=%r, kwargs=%r, channel=%r)' % (self.__class__.__name__,
self.args, self.kwargs,
self.channel)
###
# This is a compiled Spec object.
###
class Spec(object):
def _state(self, types, attrs={}):
st = State(types)
st.__dict__.update(attrs)
st.allowExtra = self.allowExtra
return st
def __init__(self, types, allowExtra=False):
self.types = types
self.allowExtra = allowExtra
utils.mapinto(contextify, self.types)
def __call__(self, irc, msg, args, stateAttrs={}):
state = self._state(self.types[:], stateAttrs)
while state.types:
context = state.types.pop(0)
try:
context(irc, msg, args, state)
except IndexError:
raise callbacks.ArgumentError
if args and not state.allowExtra:
log.debug('args and not self.allowExtra: %r', args)
raise callbacks.ArgumentError
return state
def wrap(f, specList=[], **kw):
spec = Spec(specList, **kw)
def newf(self, irc, msg, args, **kwargs):
state = spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log})
self.log.debug('State before call: %s' % state)
f(self, irc, msg, args, *state.args, **state.kwargs)
return utils.changeFunctionName(newf, f.func_name, f.__doc__)
__all__ = [
# Contexts.
'any', 'many',
'optional', 'additional',
'rest', 'getopts',
'first', 'reverse',
'commalist',
# Converter helpers.
'getConverter', 'addConverter', 'callConverter',
# Decorators.
'urlSnarfer', 'thread',
# Functions.
'wrap',
# Stuff for testing.
'Spec',
]
# This doesn't work. Suck.
## if world.testing:
## __all__.append('Spec')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

995
src/conf.py Normal file
View File

@ -0,0 +1,995 @@
###
# 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 supybot.fix as fix
import os
import sys
import time
import socket
import string
import supybot.cdb as cdb
import supybot.utils as utils
import supybot.registry as registry
import supybot.ircutils as ircutils
installDir = os.path.dirname(sys.modules[__name__].__file__)
_srcDir = installDir
_pluginsDir = os.path.join(installDir, 'plugins')
###
# version: This should be pretty obvious.
###
version = '0.80.0'
###
# *** The following variables are affected by command-line options. They are
# not registry variables for a specific reason. Do *not* change these to
# registry variables without first consulting people smarter than yourself.
###
###
# daemonized: This determines whether or not the bot has been daemonized
# (i.e., set to run in the background). Obviously, this defaults
# to False. A command-line option for obvious reasons.
###
daemonized = False
###
# allowDefaultOwner: True if supybot.capabilities is allowed not to include
# '-owner' -- that is, if all users should be automatically
# recognized as owners. That would suck, hence we require a
# command-line option to allow this stupidity.
###
allowDefaultOwner = False
###
# The standard registry.
###
supybot = registry.Group()
supybot.setName('supybot')
def registerGroup(Group, name, group=None, **kwargs):
if kwargs:
group = registry.Group(**kwargs)
return Group.register(name, group)
def registerGlobalValue(group, name, value):
value.channelValue = False
return group.register(name, value)
def registerChannelValue(group, name, value):
value._supplyDefault = True
value.channelValue = True
g = group.register(name, value)
gname = g._name.lower()
for name in registry._cache.iterkeys():
if name.lower().startswith(gname) and len(gname) < len(name):
name = name[len(gname)+1:] # +1 for .
parts = registry.split(name)
if len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]):
# This gets the channel values so they always persist.
g.get(parts[0])()
def registerPlugin(name, currentValue=None, public=True):
group = registerGlobalValue(supybot.plugins, name,
registry.Boolean(False, """Determines whether this plugin is loaded by
default.""", showDefault=False))
supybot.plugins().add(name)
registerGlobalValue(group, 'public',
registry.Boolean(public, """Determines whether this plugin is
publicly visible."""))
if currentValue is not None:
supybot.plugins.get(name).setValue(currentValue)
registerGroup(users.plugins, name)
return group
def get(group, channel=None):
if group.channelValue and \
channel is not None and ircutils.isChannel(channel):
return group.get(channel)()
else:
return group()
###
# The user info registry.
###
users = registry.Group()
users.setName('users')
registerGroup(users, 'plugins', orderAlphabetically=True)
def registerUserValue(group, name, value):
assert group._name.startswith('users')
value._supplyDefault = True
group.register(name, value)
class ValidNick(registry.String):
"""Value must be a valid IRC nick."""
def setValue(self, v):
if not ircutils.isNick(v):
self.error()
else:
registry.String.setValue(self, v)
class ValidNicks(registry.SpaceSeparatedListOf):
Value = ValidNick
class ValidNickAllowingPercentS(ValidNick):
"""Value must be a valid IRC nick, with the possible exception of a %s
in it."""
def setValue(self, v):
# If this works, it's a valid nick, aside from the %s.
try:
ValidNick.setValue(self, v.replace('%s', ''))
# It's valid aside from the %s, we'll let it through.
registry.String.setValue(self, v)
except registry.InvalidRegistryValue:
self.error()
class ValidNicksAllowingPercentS(ValidNicks):
Value = ValidNickAllowingPercentS
class ValidChannel(registry.String):
"""Value must be a valid IRC channel name."""
def setValue(self, v):
if ',' in v:
# To prevent stupid users from: a) trying to add a channel key
# with a comma in it, b) trying to add channels separated by
# commas instead of spaces
try:
(channel, _) = v.split(',')
except ValueError:
self.error()
else:
channel = v
if not ircutils.isChannel(channel):
self.error()
else:
registry.String.setValue(self, v)
registerGlobalValue(supybot, 'nick',
ValidNick('supybot', """Determines the bot's default nick."""))
registerGlobalValue(supybot.nick, 'alternates',
ValidNicksAllowingPercentS(['%s`', '%s_'], """Determines what alternative
nicks will be used if the primary nick (supybot.nick) isn't available. A
%s in this nick is replaced by the value of supybot.nick when used. If no
alternates are given, or if all are used, the supybot.nick will be perturbed
appropriately until an unused nick is found."""))
registerGlobalValue(supybot, 'ident',
ValidNick('supybot', """Determines the bot's ident string, if the server
doesn't provide one by default."""))
class VersionIfEmpty(registry.String):
def __call__(self):
ret = registry.String.__call__(self)
if not ret:
ret = 'Supybot %s' % version
return ret
registerGlobalValue(supybot, 'user',
VersionIfEmpty('', """Determines the user the bot sends to the server.
A standard user using the current version of the bot will be generated if
this is left empty."""))
class Networks(registry.SpaceSeparatedSetOfStrings):
List = ircutils.IrcSet
registerGlobalValue(supybot, 'networks',
Networks([], """Determines what networks the bot will connect to.""",
orderAlphabetically=True))
class Servers(registry.SpaceSeparatedListOfStrings):
def normalize(self, s):
if ':' not in s:
s += ':6667'
return s
def convert(self, s):
s = self.normalize(s)
(server, port) = s.split(':')
port = int(port)
return (server, port)
def __call__(self):
L = registry.SpaceSeparatedListOfStrings.__call__(self)
return map(self.convert, L)
def __str__(self):
return ' '.join(registry.SpaceSeparatedListOfStrings.__call__(self))
def append(self, s):
L = registry.SpaceSeparatedListOfStrings.__call__(self)
L.append(s)
class SpaceSeparatedSetOfChannels(registry.SpaceSeparatedListOf):
sorted = True
List = ircutils.IrcSet
Value = ValidChannel
def join(self, channel):
import ircmsgs # Don't put this globally! It's recursive.
key = self.key.get(channel)()
if key:
return ircmsgs.join(channel, key)
else:
return ircmsgs.join(channel)
def registerNetwork(name, password=''):
network = registerGroup(supybot.networks, name)
registerGlobalValue(network, 'password', registry.String(password,
"""Determines what password will be used on %s. Yes, we know that
technically passwords are server-specific and not network-specific,
but this is the best we can do right now.""" % name, private=True))
registryServers = registerGlobalValue(network, 'servers', Servers([],
"""Determines what servers the bot will connect to for %s. Each will
be tried in order, wrapping back to the first when the cycle is
completed.""" % name))
registerGlobalValue(network, 'channels', SpaceSeparatedSetOfChannels([],
"""Determines what channels the bot will join only on %s.""" % name))
registerChannelValue(network.channels, 'key', registry.String('',
"""Determines what key (if any) will be used to join the channel."""))
return network
# Let's fill our networks.
for (name, s) in registry._cache.iteritems():
if name.startswith('supybot.networks.'):
parts = name.split('.')
name = parts[2]
if name != 'default':
registerNetwork(name)
###
# Reply/error tweaking.
###
registerGroup(supybot, 'reply')
registerGroup(supybot.reply, 'format')
registerChannelValue(supybot.reply.format, 'time',
registry.String('%I:%M %p, %B %d, %Y', """Determines how timestamps printed
for human reading should be formatted. Refer to the Python documentation
for the time module to see valid formatting characters for time
formats."""))
registerGroup(supybot.reply.format.time, 'elapsed')
registerChannelValue(supybot.reply.format.time.elapsed, 'short',
registry.Boolean(False, """Determines whether elapsed times will be given
as "1 day, 2 hours, 3 minutes, and 15 seconds" or as "1d 2h 3m 15s"."""))
originalTimeElapsed = utils.timeElapsed
def timeElapsed(*args, **kwargs):
kwargs['short'] = supybot.reply.format.time.elapsed.short()
return originalTimeElapsed(*args, **kwargs)
utils.timeElapsed = timeElapsed
registerGlobalValue(supybot.reply, 'maximumLength',
registry.Integer(512*256, """Determines the absolute maximum length of the
bot's reply -- no reply will be passed through the bot with a length
greater than this."""))
registerChannelValue(supybot.reply, 'mores',
registry.Boolean(True, """Determines whether the bot will break up long
messages into chunks and allow users to use the 'more' command to get the
remaining chunks."""))
registerChannelValue(supybot.reply.mores, 'maximum',
registry.PositiveInteger(50, """Determines what the maximum number of
chunks (for use with the 'more' command) will be."""))
registerChannelValue(supybot.reply.mores, 'length',
registry.NonNegativeInteger(0, """Determines how long individual chunks
will be. If set to 0, uses our super-tweaked,
get-the-most-out-of-an-individual-message default."""))
registerChannelValue(supybot.reply.mores, 'instant',
registry.PositiveInteger(1, """Determines how many mores will be sent
instantly (i.e., without the use of the more command, immediately when
they are formed). Defaults to 1, which means that a more command will be
required for all but the first chunk."""))
registerGlobalValue(supybot.reply, 'oneToOne',
registry.Boolean(True, """Determines whether the bot will send
multi-message replies in a single message or in multiple messages. For
safety purposes (so the bot is less likely to flood) it will normally send
everything in a single message, using mores if necessary."""))
registerChannelValue(supybot.reply, 'whenNotCommand',
registry.Boolean(True, """Determines whether the bot will reply with an
error message when it is addressed but not given a valid command. If this
value is False, the bot will remain silent, as long as no other plugins
override the normal behavior."""))
registerGroup(supybot.reply, 'error')
registerGlobalValue(supybot.reply.error, 'detailed',
registry.Boolean(False, """Determines whether error messages that result
from bugs in the bot will show a detailed error message (the uncaught
exception) or a generic error message."""))
registerChannelValue(supybot.reply.error, 'inPrivate',
registry.Boolean(False, """Determines whether the bot will send error
messages to users in private. You might want to do this in order to keep
channel traffic to minimum. This can be used in combination with
supybot.reply.errorWithNotice."""))
registerChannelValue(supybot.reply.error, 'withNotice',
registry.Boolean(False, """Determines whether the bot will send error
messages to users via NOTICE instead of PRIVMSG. You might want to do this
so users can ignore NOTICEs from the bot and not have to see error
messages; or you might want to use it in combination with
supybot.reply.errorInPrivate so private errors don't open a query window
in most IRC clients."""))
registerChannelValue(supybot.reply.error, 'noCapability',
registry.Boolean(False, """Determines whether the bot will send an error
message to users who attempt to call a command for which they do not have
the necessary capability. You may wish to make this True if you don't want
users to understand the underlying security system preventing them from
running certain commands."""))
registerChannelValue(supybot.reply, 'inPrivate',
registry.Boolean(False, """Determines whether the bot will reply privately
when replying in a channel, rather than replying to the whole channel."""))
registerChannelValue(supybot.reply, 'withNotice',
registry.Boolean(False, """Determines whether the bot will reply with a
notice when replying in a channel, rather than replying with a privmsg as
normal."""))
# XXX: User value.
registerGlobalValue(supybot.reply, 'withNoticeWhenPrivate',
registry.Boolean(False, """Determines whether the bot will reply with a
notice when it is sending a private message, in order not to open a /query
window in clients. This can be overridden by individual users via the user
configuration variable reply.withNoticeWhenPrivate."""))
registerChannelValue(supybot.reply, 'withNickPrefix',
registry.Boolean(True, """Determines whether the bot will always prefix the
user's nick to its reply to that user's command."""))
registerChannelValue(supybot.reply, 'whenNotAddressed',
registry.Boolean(False, """Determines whether the bot should attempt to
reply to all messages even if they don't address it (either via its nick
or a prefix character). If you set this to True, you almost certainly want
to set supybot.reply.whenNotCommand to False."""))
registerChannelValue(supybot.reply, 'requireChannelCommandsToBeSentInChannel',
registry.Boolean(False, """Determines whether the bot will allow you to
send channel-related commands outside of that channel. Sometimes people
find it confusing if a channel-related command (like Filter.outfilter)
changes the behavior of the channel but was sent outside the channel
itself."""))
registerGlobalValue(supybot, 'followIdentificationThroughNickChanges',
registry.Boolean(False, """Determines whether the bot will unidentify
someone when that person changes his or her nick. Setting this to True
will cause the bot to track such changes. It defaults to False for a
little greater security."""))
registerGlobalValue(supybot, 'alwaysJoinOnInvite',
registry.Boolean(False, """Determines whether the bot will always join a
channel when it's invited. If this value is False, the bot will only join
a channel if the user inviting it has the 'admin' capability (or if it's
explicitly told to join the channel using the Admin.join command)"""))
registerChannelValue(supybot.reply, 'showSimpleSyntax',
registry.Boolean(False, """Supybot normally replies with the full help
whenever a user misuses a command. If this value is set to True, the bot
will only reply with the syntax of the command (the first line of the
help) rather than the full help."""))
class ValidPrefixChars(registry.String):
"""Value must contain only ~!@#$%^&*()_-+=[{}]\\|'\";:,<.>/?"""
def setValue(self, v):
if v.translate(string.ascii, '`~!@#$%^&*()_-+=[{}]\\|\'";:,<.>/?'):
self.error()
registry.String.setValue(self, v)
registerGroup(supybot.reply, 'whenAddressedBy')
registerChannelValue(supybot.reply.whenAddressedBy, 'chars',
ValidPrefixChars('', """Determines what prefix characters the bot will
reply to. A prefix character is a single character that the bot will use
to determine what messages are addressed to it; when there are no prefix
characters set, it just uses its nick. Each character in this string is
interpreted individually; you can have multiple prefix chars
simultaneously, and if any one of them is used as a prefix the bot will
assume it is being addressed."""))
registerChannelValue(supybot.reply.whenAddressedBy, 'strings',
registry.SpaceSeparatedSetOfStrings([], """Determines what strings the bot
will reply to when they are at the beginning of the message. Whereas
prefix.chars can only be one character (although there can be many of
them), this variable is a space-separated list of strings, so you can
set something like '@@ ??' and the bot will reply when a message is
prefixed by either @@ or ??."""))
registerChannelValue(supybot.reply.whenAddressedBy, 'nick',
registry.Boolean(True, """Determines whether the bot will reply when people
address it by its nick, rather than with a prefix character."""))
registerChannelValue(supybot.reply.whenAddressedBy.nick, 'atEnd',
registry.Boolean(False, """Determines whether the bot will reply when
people address it by its nick at the end of the message, rather than at
the beginning."""))
registerChannelValue(supybot.reply.whenAddressedBy, 'nicks',
registry.SpaceSeparatedSetOfStrings([], """Determines what extra nicks the
bot will always respond to when addressed by, even if its current nick is
something else."""))
###
# Replies
###
registerGroup(supybot, 'replies')
registerChannelValue(supybot.replies, 'success',
registry.NormalizedString("""The operation succeeded.""", """Determines
what message the bot replies with when a command succeeded. If this
configuration variable is empty, no success message will be sent."""))
registerChannelValue(supybot.replies, 'error',
registry.NormalizedString("""An error has occurred and has been logged.
Please contact this bot's administrator for more information.""", """
Determines what error message the bot gives when it wants to be
ambiguous."""))
registerChannelValue(supybot.replies, 'incorrectAuthentication',
registry.NormalizedString("""Your hostmask doesn't match or your password
is wrong.""", """Determines what message the bot replies with when someone
tries to use a command that requires being identified or having a password
and neither credential is correct."""))
# XXX: This should eventually check that there's one and only one %s here.
registerChannelValue(supybot.replies, 'noUser',
registry.NormalizedString("""I can't find %s in my user
database. If you didn't give a user name, then I might not know what your
user is, and you'll need to identify before this command might work.""",
"""Determines what error message the bot replies with when someone tries
to accessing some information on a user the bot doesn't know about."""))
registerChannelValue(supybot.replies, 'notRegistered',
registry.NormalizedString("""You must be registered to use this command.
If you are already registered, you must either identify (using the identify
command) or add a hostmask matching your current hostmask (using the
addhostmask command).""", """Determines what error message the bot replies
with when someone tries to do something that requires them to be registered
but they're not currently recognized."""))
registerChannelValue(supybot.replies, 'noCapability',
registry.NormalizedString("""You don't have the %s capability. If you
think that you should have this capability, be sure that you are identified
before trying again. The 'whoami' command can tell you if you're
identified.""", """Determines what error message is given when the bot is
telling someone they aren't cool enough to use the command they tried to
use."""))
registerChannelValue(supybot.replies, 'genericNoCapability',
registry.NormalizedString("""You're missing some capability you need.
This could be because you actually possess the anti-capability for the
capability that's required of you, or because the channel provides that
anti-capability by default, or because the global capabilities include
that anti-capability. Or, it could be because the channel or
supybot.capabilities.default is set to False, meaning that no commands are
allowed unless explicitly in your capabilities. Either way, you can't do
what you want to do.""",
"""Determines what generic error message is given when the bot is telling
someone that they aren't cool enough to use the command they tried to use,
and the author of the code calling errorNoCapability didn't provide an
explicit capability for whatever reason."""))
registerChannelValue(supybot.replies, 'requiresPrivacy',
registry.NormalizedString("""That operation cannot be done in a
channel.""", """Determines what error messages the bot sends to people who
try to do things in a channel that really should be done in private."""))
registerChannelValue(supybot.replies, 'possibleBug',
registry.NormalizedString("""This may
be a bug. If you think it is, please file a bug report at
<http://sourceforge.net/tracker/?func=add&group_id=58965&atid=489447>.""",
"""Determines what message the bot sends when it thinks you've encountered
a bug that the developers don't know about."""))
###
# End supybot.replies.
###
registerGlobalValue(supybot, 'snarfThrottle',
registry.Float(10.0, """A floating point number of seconds to throttle
snarfed URLs, in order to prevent loops between two bots snarfing the same
URLs and having the snarfed URL in the output of the snarf message."""))
registerGlobalValue(supybot, 'upkeepInterval',
registry.PositiveInteger(3600, """Determines the number of seconds between
running the upkeep function that flushes (commits) open databases, collects
garbage, and records some useful statistics at the debugging level."""))
registerGlobalValue(supybot, 'flush',
registry.Boolean(True, """Determines whether the bot will periodically
flush data and configuration files to disk. Generally, the only time
you'll want to set this to False is when you want to modify those
configuration files by hand and don't want the bot to flush its current
version over your modifications. Do note that if you change this to False
inside the bot, your changes won't be flushed. To make this change
permanent, you must edit the registry yourself."""))
###
# supybot.commands. For stuff relating to commands.
###
registerGroup(supybot, 'commands')
class ValidQuotes(registry.Value):
"""Value must consist solely of \", ', and ` characters."""
def setValue(self, v):
if [c for c in v if c not in '"`\'']:
self.error()
super(ValidQuotes, self).setValue(v)
def __str__(self):
return str(self.value)
registerChannelValue(supybot.commands, 'quotes',
ValidQuotes('"', """Determines what characters are valid for quoting
arguments to commands in order to prevent them from being tokenized.
"""))
# This is a GlobalValue because bot owners should be able to say, "There will
# be no nesting at all on this bot." Individual channels can just set their
# brackets to the empty string.
registerGlobalValue(supybot.commands, 'nested',
registry.Boolean(True, """Determines whether the bot will allow nested
commands, which rule. You definitely should keep this on."""))
registerGlobalValue(supybot.commands.nested, 'maximum',
registry.PositiveInteger(10, """Determines what the maximum number of
nested commands will be; users will receive an error if they attempt
commands more nested than this."""))
class ValidBrackets(registry.OnlySomeStrings):
validStrings = ('', '[]', '<>', '{}', '()')
registerChannelValue(supybot.commands.nested, 'brackets',
ValidBrackets('[]', """Supybot allows you to specify what brackets are used
for your nested commands. Valid sets of brackets include [], <>, and {}
(). [] has strong historical motivation, as well as being the brackets
that don't require shift. <> or () might be slightly superior because they
cannot occur in a nick. If this string is empty, nested commands will
not be allowed in this channel."""))
registerChannelValue(supybot.commands.nested, 'pipeSyntax',
registry.Boolean(False, """Supybot allows nested commands. Enabling this
option will allow nested commands with a syntax similar to UNIX pipes, for
example: 'bot: foo | bar'."""))
registerGroup(supybot.commands, 'defaultPlugins',
orderAlphabetically=True, help=utils.normalizeWhitespace("""Determines
what commands have default plugins set, and which plugins are set to
be the default for each of those commands."""))
registerGlobalValue(supybot.commands.defaultPlugins, 'importantPlugins',
registry.SpaceSeparatedSetOfStrings(
['Admin', 'Channel', 'Config', 'Misc', 'Owner', 'User'],
"""Determines what plugins automatically get precedence over all other
plugins when selecting a default plugin for a command. By default,
this includes the standard loaded plugins. You probably shouldn't
change this if you don't know what you're doing; if you do know what
you're doing, then also know that this set is case-sensitive."""))
# supybot.commands.disabled moved to callbacks for canonicalName.
###
# supybot.abuse. For stuff relating to abuse of the bot.
###
registerGroup(supybot, 'abuse')
registerGroup(supybot.abuse, 'flood')
registerGlobalValue(supybot.abuse.flood, 'command',
registry.Boolean(True, """Determines whether the bot will defend itself
against command-flooding."""))
registerGlobalValue(supybot.abuse.flood.command, 'maximum',
registry.PositiveInteger(12, """Determines how many commands users are
allowed per minute. If a user sends more than this many commands in any
60 second period, he or she will be ignored for
supybot.abuse.flood.command.punishment seconds."""))
registerGlobalValue(supybot.abuse.flood.command, 'punishment',
registry.PositiveInteger(300, """Determines how many seconds the bot
will ignore users who flood it with commands."""))
registerGlobalValue(supybot.abuse.flood.command, 'invalid',
registry.Boolean(True, """Determines whether the bot will defend itself
against invalid command-flooding."""))
registerGlobalValue(supybot.abuse.flood.command.invalid, 'maximum',
registry.PositiveInteger(5, """Determines how many invalid commands users
are allowed per minute. If a user sends more than this many invalid
commands in any 60 second period, he or she will be ignored for
supybot.abuse.flood.command.invalid.punishment seconds. Typically, this
value is lower than supybot.abuse.flood.command.maximum, since it's far
less likely (and far more annoying) for users to flood with invalid
commands than for them to flood with valid commands."""))
registerGlobalValue(supybot.abuse.flood.command.invalid, 'punishment',
registry.PositiveInteger(600, """Determines how many seconds the bot
will ignore users who flood it with invalid commands. Typically, this
value is higher than supybot.abuse.flood.command.punishment, since it's far
less likely (and far more annoying) for users to flood witih invalid
commands than for them to flood with valid commands."""))
###
# supybot.drivers. For stuff relating to Supybot's drivers (duh!)
###
registerGroup(supybot, 'drivers')
registerGlobalValue(supybot.drivers, 'poll',
registry.PositiveFloat(1.0, """Determines the default length of time a
driver should block waiting for input."""))
class ValidDriverModule(registry.OnlySomeStrings):
validStrings = ('default', 'Socket', 'Twisted')
registerGlobalValue(supybot.drivers, 'module',
ValidDriverModule('default', """Determines what driver module the bot will
use. socketDrivers, a simple driver based on timeout sockets, is used by
default because it's simple and stable. asyncoreDrivers is a bit older
(and less well-maintained) but allows you to integrate with asyncore-based
applications. twistedDrivers is very stable and simple, and if you've got
Twisted installed, is probably your best bet."""))
###
# supybot.directories, for stuff relating to directories.
###
# XXX This shouldn't make directories willy-nilly. As it is now, if it's
# configured, it'll still make the default directories, I think.
class Directory(registry.String):
def __call__(self):
# ??? Should we perhaps always return an absolute path here?
v = super(Directory, self).__call__()
if not os.path.exists(v):
os.mkdir(v)
return v
def dirize(self, filename):
myself = self()
if os.path.isabs(filename):
filename = os.path.abspath(filename)
selfAbs = os.path.abspath(myself)
commonPrefix = os.path.commonprefix([selfAbs, filename])
filename = filename[len(commonPrefix):]
elif not os.path.isabs(myself):
if filename.startswith(myself):
filename = filename[len(myself):]
filename = filename.lstrip(os.path.sep) # Stupid os.path.join!
return os.path.join(myself, filename)
class DataFilename(registry.String):
def __call__(self):
v = super(DataFilename, self).__call__()
dataDir = supybot.directories.data()
if not v.startswith(dataDir):
v = os.path.basename(v)
v = os.path.join(dataDir, v)
self.setValue(v)
return v
class DataFilenameDirectory(DataFilename, Directory):
def __call__(self):
v = DataFilename.__call__(self)
v = Directory.__call__(self)
return v
registerGroup(supybot, 'directories')
registerGlobalValue(supybot.directories, 'conf',
Directory('conf', """Determines what directory configuration data is
put into."""))
registerGlobalValue(supybot.directories, 'data',
Directory('data', """Determines what directory data is put into."""))
registerGlobalValue(supybot.directories, 'backup',
Directory('backup', """Determines what directory backup data is put
into."""))
registerGlobalValue(supybot.directories.data, 'tmp',
DataFilenameDirectory('tmp', """Determines what directory temporary files
are put into."""))
# Remember, we're *meant* to replace this nice little wrapper.
def transactionalFile(*args, **kwargs):
kwargs['tmpDir'] = supybot.directories.data.tmp()
kwargs['backupDir'] = supybot.directories.backup()
return utils.AtomicFile(*args, **kwargs)
utils.transactionalFile = transactionalFile
class PluginDirectories(registry.CommaSeparatedListOfStrings):
def __call__(self):
v = registry.CommaSeparatedListOfStrings.__call__(self)
if _pluginsDir not in v:
v.append(_pluginsDir)
return v
registerGlobalValue(supybot.directories, 'plugins',
PluginDirectories([], """Determines what directories the bot will
look for plugins in. Accepts a comma-separated list of strings. This
means that to add another directory, you can nest the former value and add
a new one. E.g. you can say: bot: 'config supybot.directories.plugins
[config supybot.directories.plugins], newPluginDirectory'."""))
registerGlobalValue(supybot, 'plugins',
registry.SpaceSeparatedSetOfStrings([], """Determines what plugins will
be loaded.""", orderAlphabetically=True))
registerGlobalValue(supybot.plugins, 'alwaysLoadImportant',
registry.Boolean(True, """Determines whether the bot will always load
important plugins (Admin, Channel, Config, Misc, Owner, and User)
regardless of what their configured state is. Generally, if these plugins
are configured not to load, you didn't do it on purpose, and you still
want them to load. Users who don't want to load these plugins are smart
enough to change the value of this variable appropriately :)"""))
###
# supybot.databases. For stuff relating to Supybot's databases (duh!)
###
class Databases(registry.SpaceSeparatedListOfStrings):
def __call__(self):
v = super(Databases, self).__call__()
if not v:
v = ['anydbm', 'cdb', 'flat', 'pickle']
if 'sqlite' in sys.modules:
v.insert(0, 'sqlite')
return v
def serialize(self):
return ' '.join(self.value)
registerGlobalValue(supybot, 'databases',
Databases([], """Determines what databases are available for use. If this
value is not configured (that is, if its value is empty) then sane defaults
will be provided."""))
registerGroup(supybot.databases, 'users')
registerGlobalValue(supybot.databases.users, 'filename',
registry.String('users.conf', """Determines what filename will be used for
the users database. This file will go into the directory specified by the
supybot.directories.conf variable."""))
registerGlobalValue(supybot.databases.users, 'timeoutIdentification',
registry.Integer(0, """Determines how long it takes identification to time
out. If the value is less than or equal to zero, identification never
times out."""))
registerGlobalValue(supybot.databases.users, 'allowUnregistration',
registry.Boolean(False, """Determines whether the bot will allow users to
unregister their users. This can wreak havoc with already-existing
databases, so by default we don't allow it. Enable this at your own risk.
(Do also note that this does not prevent the owner of the bot from using
the unregister command.)
"""))
registerGroup(supybot.databases, 'ignores')
registerGlobalValue(supybot.databases.ignores, 'filename',
registry.String('ignores.conf', """Determines what filename will be used
for the ignores database. This file will go into the directory specified
by the supybot.directories.conf variable."""))
registerGroup(supybot.databases, 'channels')
registerGlobalValue(supybot.databases.channels, 'filename',
registry.String('channels.conf', """Determines what filename will be used
for the channels database. This file will go into the directory specified
by the supybot.directories.conf variable."""))
# TODO This will need to do more in the future (such as making sure link.allow
# will let the link occur), but for now let's just leave it as this.
class ChannelSpecific(registry.Boolean):
def getChannelLink(self, channel):
channelSpecific = supybot.databases.plugins.channelSpecific
channels = [channel]
def hasLinkChannel(channel):
if not get(channelSpecific, channel):
lchannel = get(channelSpecific.link, channel)
if not get(channelSpecific.link.allow, lchannel):
return False
return channel != lchannel
return False
lchannel = channel
while hasLinkChannel(lchannel):
lchannel = get(channelSpecific.link, lchannel)
if lchannel not in channels:
channels.append(lchannel)
else:
# Found a cyclic link. We'll just use the current channel
lchannel = channel
break
return lchannel
registerGroup(supybot.databases, 'plugins')
registerChannelValue(supybot.databases.plugins, 'channelSpecific',
ChannelSpecific(True, """Determines whether database-based plugins that
can be channel-specific will be so. This can be overridden by individual
channels. Do note that the bot needs to be restarted immediately after
changing this variable or your db plugins may not work for your channel;
also note that you may wish to set
supybot.databases.plugins.channelSpecific.link appropriately if you wish
to share a certain channel's databases globally."""))
registerChannelValue(supybot.databases.plugins.channelSpecific, 'link',
ValidChannel('#', """Determines what channel global (non-channel-specific)
databases will be considered a part of. This is helpful if you've been
running channel-specific for awhile and want to turn the databases for
your primary channel into global databases. If
supybot.databases.plugins.channelSpecific.link.allow prevents linking, the
current channel will be used. Do note that the bot needs to be restarted
immediately after changing this variable or your db plugins may not work
for your channel."""))
registerChannelValue(supybot.databases.plugins.channelSpecific.link, 'allow',
registry.Boolean(True, """Determines whether another channel's global
(non-channel-specific) databases will be allowed to link to this channel's
databases. Do note that the bot needs to be restarted immediately after
changing this variable or your db plugins may not work for your channel.
"""))
class CDB(registry.Boolean):
def connect(self, filename):
basename = os.path.basename(filename)
journalName = supybot.directories.data.tmp.dirize(basename+'.journal')
return cdb.open(filename, 'c',
journalName=journalName,
maxmods=self.maximumModifications())
registerGroup(supybot.databases, 'types')
registerGlobalValue(supybot.databases.types, 'cdb', CDB(True, """Determines
whether CDB databases will be allowed as a database implementation."""))
registerGlobalValue(supybot.databases.types.cdb, 'maximumModifications',
registry.Float(0.5, """Determines how often CDB databases will have their
modifications flushed to disk. When the number of modified records is
greater than this part of the number of unmodified records, the database
will be entirely flushed to disk."""))
# XXX Configuration variables for dbi, sqlite, flat, mysql, etc.
###
# Protocol information.
###
originalIsNick = ircutils.isNick
def isNick(s, strictRfc=None, **kw):
if strictRfc is None:
strictRfc = supybot.protocols.irc.strictRfc()
return originalIsNick(s, strictRfc=strictRfc, **kw)
ircutils.isNick = isNick
###
# supybot.protocols
###
registerGroup(supybot, 'protocols')
###
# supybot.protocols.irc
###
registerGroup(supybot.protocols, 'irc')
registerGlobalValue(supybot.protocols.irc, 'strictRfc',
registry.Boolean(False, """Determines whether the bot will strictly follow
the RFC; currently this only affects what strings are considered to be
nicks. If you're using a server or a network that requires you to message
a nick such as services@this.network.server then you you should set this to
False."""))
registerGlobalValue(supybot.protocols.irc, 'umodes',
registry.String('', """Determines what user modes the bot will request from
the server when it first connects. Many people might choose +i; some
networks allow +x, which indicates to the auth services on those networks
that you should be given a fake host."""))
registerGlobalValue(supybot.protocols.irc, 'vhost',
registry.String('', """Determines what vhost the bot will bind to before
connecting to the IRC server."""))
registerGlobalValue(supybot.protocols.irc, 'maxHistoryLength',
registry.Integer(1000, """Determines how many old messages the bot will
keep around in its history. Changing this variable will not take effect
until the bot is restarted."""))
registerGlobalValue(supybot.protocols.irc, 'throttleTime',
registry.Float(1.0, """A floating point number of seconds to throttle
queued messages -- that is, messages will not be sent faster than once per
throttleTime seconds."""))
registerGlobalValue(supybot.protocols.irc, 'ping',
registry.Boolean(True, """Determines whether the bot will send PINGs to the
server it's connected to in order to keep the connection alive and discover
earlier when it breaks. Really, this option only exists for debugging
purposes: you always should make it True unless you're testing some strange
server issues."""))
registerGlobalValue(supybot.protocols.irc.ping, 'interval',
registry.Integer(120, """Determines the number of seconds between sending
pings to the server, if pings are being sent to the server."""))
registerGlobalValue(supybot.protocols.irc, 'queueDuplicateMessages',
registry.Boolean(False, """Determines whether the bot will allow duplicate
messages to be queued for delivery to the server. This is a safety
mechanism put in place to prevent plugins from sending the same message
multiple times; most of the time it doesn't matter, but when it does,
you'll probably want it to disallowed."""))
###
# supybot.protocols.http
###
registerGroup(supybot.protocols, 'http')
registerGlobalValue(supybot.protocols.http, 'peekSize',
registry.PositiveInteger(4096, """Determines how many bytes the bot will
'peek' at when looking through a URL for a doctype or title or something
similar. It'll give up after it reads this many bytes, even if it hasn't
found what it was looking for."""))
registerGlobalValue(supybot.protocols.http, 'proxy',
registry.String('', """Determines what proxy all HTTP requests should go
through. The value should be of the form 'host:port'."""))
###
# Especially boring stuff.
###
registerGlobalValue(supybot, 'defaultIgnore',
registry.Boolean(False, """Determines whether the bot will ignore
unregistered users by default. Of course, that'll make it particularly
hard for those users to register or identify with the bot, but that's your
problem to solve."""))
class IP(registry.String):
"""Value must be a valid IP."""
def setValue(self, v):
if v and not (utils.isIP(v) or utils.isIPV6(v)):
self.error()
else:
registry.String.setValue(self, v)
registerGlobalValue(supybot, 'externalIP',
IP('', """A string that is the external IP of the bot. If this is the empty
string, the bot will attempt to find out its IP dynamically (though
sometimes that doesn't work, hence this variable)."""))
class SocketTimeout(registry.PositiveInteger):
"""Value must be an integer greater than supybot.drivers.poll and must be
greater than or equal to 1."""
def setValue(self, v):
if v < supybot.drivers.poll() or v < 1:
self.error()
registry.PositiveInteger.setValue(self, v)
socket.setdefaulttimeout(self.value)
registerGlobalValue(supybot, 'defaultSocketTimeout',
SocketTimeout(10, """Determines what the default timeout for socket objects
will be. This means that *all* sockets will timeout when this many seconds
has gone by (unless otherwise modified by the author of the code that uses
the sockets)."""))
registerGlobalValue(supybot, 'pidFile',
registry.String('', """Determines what file the bot should write its PID
(Process ID) to, so you can kill it more easily. If it's left unset (as is
the default) then no PID file will be written. A restart is required for
changes to this variable to take effect."""))
###
# Debugging options.
###
registerGroup(supybot, 'debug')
registerGlobalValue(supybot.debug, 'threadAllCommands',
registry.Boolean(False, """Determines whether the bot will automatically
thread all commands."""))
registerGlobalValue(supybot.debug, 'flushVeryOften',
registry.Boolean(False, """Determines whether the bot will automatically
flush all flushers *very* often. Useful for debugging when you don't know
what's breaking or when, but think that it might be logged."""))
registerGlobalValue(supybot.debug, 'generated',
registry.String('$Id: conf.py,v 1.237 2005/01/17 04:54:17 jamessan Exp $; %s' % time.ctime(), """Determines when this
configuration file was generated; it should be modified by
supybot-wizard"""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

440
src/dbi.py Normal file
View File

@ -0,0 +1,440 @@
###
# 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.
###
"""
Module for some slight database-independence for simple databases.
"""
import supybot.fix as fix
import csv
import math
import sets
import random
from itertools import ilen
import supybot.cdb as cdb
import supybot.utils as utils
class Error(Exception):
"""General error for this module."""
class NoRecordError(KeyError):
pass
class InvalidDBError(Exception):
pass
class MappingInterface(object):
"""This is a class to represent the underlying representation of a map
from integer keys to strings."""
def __init__(self, filename, **kwargs):
"""Feel free to ignore the filename."""
raise NotImplementedError
def get(id):
"""Gets the record matching id. Raises NoRecordError otherwise."""
raise NotImplementedError
def set(id, s):
"""Sets the record matching id to s."""
raise NotImplementedError
def add(self, s):
"""Adds a new record, returning a new id for it."""
raise NotImplementedError
def remove(self, id):
"Returns and removes the record with the given id from the database."
raise NotImplementedError
def __iter__(self):
"Return an iterator over (id, s) pairs. Not required to be ordered."
raise NotImplementedError
def flush(self):
"""Flushes current state to disk."""
raise NotImplementedError
def close(self):
"""Flushes current state to disk and invalidates the Mapping."""
raise NotImplementedError
def vacuum(self):
"Cleans up in the database, if possible. Not required to do anything."
pass
class DirMapping(MappingInterface):
def __init__(self, filename, **kwargs):
self.dirname = filename
if not os.path.exists(self.dirname):
os.mkdir(self.dirname)
if not os.path.exists(os.path.join(self.dirname, 'max')):
self._setMax(1)
def _setMax(self, id):
fd = file(os.path.join(self.dirname, 'max'), 'w')
try:
fd.write(str(id))
finally:
fd.close()
def _getMax(self):
fd = file(os.path.join(self.dirname, 'max'))
try:
i = int(fd.read())
return i
finally:
fd.close()
def _makeFilename(self, id):
return os.path.join(self.dirname, str(id))
def get(id):
try:
fd = file(self._makeFilename(id))
return fd.read()
except EnvironmentError, e:
exn = NoRecordError(id)
exn.realException = e
raise exn
def set(id, s):
fd = file(self._makeFilename(id), 'w')
fd.write(s)
fd.close()
def add(self, s):
id = self._getMax()
fd = file(self._makeFilename(id), 'w')
try:
fd.write(s)
return id
finally:
fd.close()
def remove(self, id):
try:
os.remove(self._makeFilename(id))
except EnvironmentError, e:
raise NoRecordError, id
class FlatfileMapping(MappingInterface):
def __init__(self, filename, maxSize=10**6):
self.filename = filename
try:
fd = file(self.filename)
strId = fd.readline().rstrip()
self.maxSize = len(strId)
try:
self.currentId = int(strId)
except ValueError:
raise Error, 'Invalid file for FlatfileMapping: %s' % filename
except EnvironmentError, e:
# File couldn't be opened.
self.maxSize = int(math.log10(maxSize))
self.currentId = 0
self._incrementCurrentId()
def _canonicalId(self, id):
if id is not None:
return str(id).zfill(self.maxSize)
else:
return '-'*self.maxSize
def _incrementCurrentId(self, fd=None):
fdWasNone = fd is None
if fdWasNone:
fd = file(self.filename, 'a')
fd.seek(0)
self.currentId += 1
fd.write(self._canonicalId(self.currentId))
fd.write('\n')
if fdWasNone:
fd.close()
def _splitLine(self, line):
line = line.rstrip('\r\n')
(id, s) = line.split(':', 1)
return (id, s)
def _joinLine(self, id, s):
return '%s:%s\n' % (self._canonicalId(id), s)
def add(self, s):
line = self._joinLine(self.currentId, s)
fd = file(self.filename, 'r+')
try:
fd.seek(0, 2) # End.
fd.write(line)
return self.currentId
finally:
self._incrementCurrentId(fd)
fd.close()
def get(self, id):
strId = self._canonicalId(id)
try:
fd = file(self.filename)
fd.readline() # First line, nextId.
for line in fd:
(lineId, s) = self._splitLine(line)
if lineId == strId:
return s
raise NoRecordError, id
finally:
fd.close()
# XXX This assumes it's not been given out. We should make sure that our
# maximum id remains accurate if this is some value we've never given
# out -- i.e., self.maxid = max(self.maxid, id) or something.
def set(self, id, s):
strLine = self._joinLine(id, s)
try:
fd = file(self.filename, 'r+')
self.remove(id, fd)
fd.seek(0, 2) # End.
fd.write(strLine)
finally:
fd.close()
def remove(self, id, fd=None):
fdWasNone = fd is None
strId = self._canonicalId(id)
try:
if fdWasNone:
fd = file(self.filename, 'r+')
fd.seek(0)
fd.readline() # First line, nextId
pos = fd.tell()
line = fd.readline()
while line:
(lineId, _) = self._splitLine(line)
if lineId == strId:
fd.seek(pos)
fd.write(self._canonicalId(None))
fd.seek(pos)
fd.readline() # Same line we just rewrote the id for.
pos = fd.tell()
line = fd.readline()
# We should be at the end.
finally:
if fdWasNone:
fd.close()
def __iter__(self):
fd = file(self.filename)
fd.readline() # First line, nextId.
for line in fd:
(id, s) = self._splitLine(line)
if not id.startswith('-'):
yield (int(id), s)
fd.close()
def vacuum(self):
infd = file(self.filename)
outfd = utils.transactionalFile(self.filename,
makeBackupIfSmaller=False)
outfd.write(infd.readline()) # First line, nextId.
for line in infd:
if not line.startswith('-'):
outfd.write(line)
infd.close()
outfd.close()
def flush(self):
pass # No-op, we maintain no open files.
def close(self):
self.vacuum() # Should we do this? It should be fine.
class CdbMapping(MappingInterface):
def __init__(self, filename, **kwargs):
self.filename = filename
self._openCdb() # So it can be overridden later.
if 'nextId' not in self.db:
self.db['nextId'] = '1'
def _openCdb(self, *args, **kwargs):
self.db = cdb.open(filename, 'c', **kwargs)
def _getNextId(self):
i = int(self.db['nextId'])
self.db['nextId'] = str(i+1)
return i
def get(self, id):
try:
return self.db[str(id)]
except KeyError:
raise NoRecordError, id
# XXX Same as above.
def set(self, id, s):
self.db[str(id)] = s
def add(self, s):
id = self._getNextId()
self.set(id, s)
return id
def remove(self, id):
del self.db[str(id)]
def __iter__(self):
for (id, s) in self.db.iteritems():
if id != 'nextId':
yield (int(id), s)
def flush(self):
self.db.flush()
def close(self):
self.db.close()
class DB(object):
Mapping = 'flat' # This is a good, sane default.
Record = None
def __init__(self, filename, Mapping=None, Record=None):
if Record is not None:
self.Record = Record
if Mapping is not None:
self.Mapping = Mapping
if isinstance(self.Mapping, basestring):
self.Mapping = Mappings[self.Mapping]
self.map = self.Mapping(filename)
def _newRecord(self, id, s):
record = self.Record(id=id)
record.deserialize(s)
return record
def get(self, id):
s = self.map.get(id)
return self._newRecord(id, s)
def set(self, id, record):
s = record.serialize()
self.map.set(id, s)
def add(self, record):
s = record.serialize()
id = self.map.add(s)
record.id = id
return id
def remove(self, id):
self.map.remove(id)
def __iter__(self):
for (id, s) in self.map:
# We don't need to yield the id because it's in the record.
yield self._newRecord(id, s)
def select(self, p):
for record in self:
if p(record):
yield record
def random(self):
try:
return self._newRecord(*random.choice(self.map))
except IndexError:
return None
def size(self):
return ilen(self.map)
def flush(self):
self.map.flush()
def vacuum(self):
self.map.vacuum()
def close(self):
self.map.close()
Mappings = {
'cdb': CdbMapping,
'flat': FlatfileMapping,
}
class Record(object):
def __init__(self, id=None, **kwargs):
if id is not None:
assert isinstance(id, int), 'id must be an integer.'
self.id = id
self.fields = []
self.defaults = {}
self.converters = {}
for name in self.__fields__:
if isinstance(name, tuple):
(name, spec) = name
else:
spec = utils.safeEval
assert name != 'id'
self.fields.append(name)
if isinstance(spec, tuple):
(converter, default) = spec
else:
converter = spec
default = None
self.defaults[name] = default
self.converters[name] = converter
seen = sets.Set()
for (name, value) in kwargs.iteritems():
assert name in self.fields, 'name must be a record value.'
seen.add(name)
setattr(self, name, value)
for name in self.fields:
if name not in seen:
default = self.defaults[name]
if callable(default):
default = default()
setattr(self, name, default)
def serialize(self):
return csv.join([repr(getattr(self, name)) for name in self.fields])
def deserialize(self, s):
unseenRecords = sets.Set(self.fields)
for (name, strValue) in zip(self.fields, csv.split(s)):
setattr(self, name, self.converters[name](strValue))
unseenRecords.remove(name)
for name in unseenRecords:
setattr(self, name, self.defaults[name])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

218
src/drivers/Socket.py Normal file
View File

@ -0,0 +1,218 @@
###
# 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.
###
"""
Contains simple socket drivers. Asyncore bugged (haha, pun!) me.
"""
from __future__ import division
import supybot.fix as fix
import time
import select
import socket
from itertools import imap
import supybot.log as log
import supybot.conf as conf
import supybot.utils as utils
import supybot.world as world
import supybot.drivers as drivers
import supybot.schedule as schedule
# XXX Shouldn't the reconnect wait (at least the last one) be configurable?
reconnectWaits = [0, 60, 300]
class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
def __init__(self, irc):
self.irc = irc
self.__parent = super(SocketDriver, self)
self.__parent.__init__(irc)
self.conn = None
self.servers = ()
self.eagains = 0
self.inbuffer = ''
self.outbuffer = ''
self.zombie = False
self.scheduled = None
self.connected = False
self.reconnectWaitsIndex = 0
self.reconnectWaits = reconnectWaits
self.connect()
def _getNextServer(self):
oldServer = getattr(self, 'currentServer', None)
server = self.__parent._getNextServer()
if self.currentServer != oldServer:
self.reconnectWaitsIndex = 0
return server
def _handleSocketError(self, e):
# (11, 'Resource temporarily unavailable') raised if connect
# hasn't finished yet. We'll keep track of how many we get.
if e.args[0] != 11 and self.eagains > 120:
drivers.log.disconnect(self.currentServer, e)
self.reconnect(wait=True)
else:
log.debug('Got EAGAIN, current count: %s.', self.eagains)
self.eagains += 1
def _sendIfMsgs(self):
if not self.zombie:
msgs = [self.irc.takeMsg()]
while msgs[-1] is not None:
msgs.append(self.irc.takeMsg())
del msgs[-1]
self.outbuffer += ''.join(imap(str, msgs))
if self.outbuffer:
try:
sent = self.conn.send(self.outbuffer)
self.outbuffer = self.outbuffer[sent:]
self.eagains = 0
except socket.error, e:
self._handleSocketError(e)
if self.zombie and not self.outbuffer:
self._reallyDie()
def run(self):
if not self.connected:
# We sleep here because otherwise, if we're the only driver, we'll
# spin at 100% CPU while we're disconnected.
time.sleep(conf.supybot.drivers.poll())
return
self._sendIfMsgs()
try:
self.inbuffer += self.conn.recv(1024)
self.eagains = 0
lines = self.inbuffer.split('\n')
self.inbuffer = lines.pop()
for line in lines:
msg = drivers.parseMsg(line)
if msg is not None:
self.irc.feedMsg(msg)
except socket.timeout:
pass
except socket.error, e:
self._handleSocketError(e)
return
if not self.irc.zombie:
self._sendIfMsgs()
def connect(self, **kwargs):
self.reconnect(reset=False, **kwargs)
def reconnect(self, wait=False, reset=True):
self.scheduled = False
if self.connected:
drivers.log.reconnect(self.irc.network)
self.conn.close()
self.connected = False
if reset:
drivers.log.debug('Resetting %s.', self.irc)
self.irc.reset()
else:
drivers.log.debug('Not resetting %s.', self.irc)
if wait:
self._scheduleReconnect()
return
server = self._getNextServer()
drivers.log.connect(self.currentServer)
try:
self.conn = utils.getSocket(server[0])
vhost = conf.supybot.protocols.irc.vhost()
self.conn.bind((vhost, 0))
except socket.error, e:
drivers.log.connectError(self.currentServer, e)
if self.reconnectWaitsIndex < len(self.reconnectWaits)-1:
self.reconnectWaitsIndex += 1
self.reconnect(wait=True)
return
# We allow more time for the connect here, since it might take longer.
# At least 10 seconds.
self.conn.settimeout(max(10, conf.supybot.drivers.poll()*10))
if self.reconnectWaitsIndex < len(self.reconnectWaits)-1:
self.reconnectWaitsIndex += 1
try:
self.conn.connect(server)
self.conn.settimeout(conf.supybot.drivers.poll())
except socket.error, e:
if e.args[0] == 115:
now = time.time()
when = now + 60
whenS = log.timestamp(when)
drivers.log.debug('Connection in progress, scheduling '
'connectedness check for %s', whenS)
schedule.addEvent(self._checkAndWriteOrReconnect, when)
else:
drivers.log.connectError(self.currentServer, e)
self.reconnect(wait=True)
return
self.connected = True
self.reconnectWaitPeriodsIndex = 0
def _checkAndWriteOrReconnect(self):
drivers.log.debug('Checking whether we are connected.')
(_, w, _) = select.select([], [self.conn], [], 0)
if w:
drivers.log.debug('Socket is writable, it might be connected.')
self.connected = True
self.reconnectWaitPeriodsIndex = 0
else:
drivers.log.connectError(self.currentServer, 'Timed out')
self.reconnect()
def _scheduleReconnect(self):
when = time.time() + self.reconnectWaits[self.reconnectWaitsIndex]
if not world.dying:
drivers.log.reconnect(self.irc.network, when)
self.scheduled = schedule.addEvent(self.reconnect, when)
def die(self):
self.zombie = True
if self.scheduled:
schedule.removeEvent(self.scheduled)
drivers.log.die(self.irc)
def _reallyDie(self):
if self.conn is not None:
self.conn.close()
drivers.IrcDriver.die(self)
# self.irc.die() Kill off the ircs yourself, jerk!
def name(self):
return '%s(%s)' % (self.__class__.__name__, self.irc)
Driver = SocketDriver
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

141
src/drivers/Twisted.py Normal file
View File

@ -0,0 +1,141 @@
###
# 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 supybot.fix as fix
import time
import supybot.log as log
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.drivers as drivers
import supybot.ircmsgs as ircmsgs
from twisted.internet import reactor, error
from twisted.protocols.basic import LineReceiver
from twisted.internet.protocol import ReconnectingClientFactory
class TwistedRunnerDriver(drivers.IrcDriver):
def name(self):
return self.__class__.__name__
def run(self):
try:
reactor.iterate(conf.supybot.drivers.poll())
except:
drivers.log.exception('Uncaught exception outside reactor:')
class SupyIrcProtocol(LineReceiver):
delimiter = '\n'
MAX_LENGTH = 1024
def __init__(self):
self.mostRecentCall = reactor.callLater(1, self.checkIrcForMsgs)
def lineReceived(self, line):
start = time.time()
msg = drivers.parseMsg(line)
if msg is not None:
self.irc.feedMsg(msg)
def checkIrcForMsgs(self):
if self.connected:
msg = self.irc.takeMsg()
if msg:
self.transport.write(str(msg))
self.mostRecentCall = reactor.callLater(1, self.checkIrcForMsgs)
def connectionLost(self, r):
self.mostRecentCall.cancel()
if r.check(error.ConnectionDone):
drivers.log.disconnect(self.factory.currentServer)
else:
drivers.log.disconnect(self.factory.currentServer, errorMsg(r))
if self.irc.zombie:
self.factory.continueTrying = False
while self.irc.takeMsg():
continue
else:
self.irc.reset()
def connectionMade(self):
self.factory.resetDelay()
self.irc.driver = self
def die(self):
drivers.log.die(self.irc)
self.factory.continueTrying = False
self.transport.loseConnection()
def reconnect(self, wait=None):
# We ignore wait here, because we handled our own waiting.
drivers.log.reconnect(self.irc.network)
self.transport.loseConnection()
def errorMsg(reason):
return reason.getErrorMessage()
class SupyReconnectingFactory(ReconnectingClientFactory, drivers.ServersMixin):
# XXX Shouldn't the maxDelay be configurable?
maxDelay = 300
protocol = SupyIrcProtocol
def __init__(self, irc):
self.irc = irc
drivers.ServersMixin.__init__(self, irc)
(server, port) = self._getNextServer()
vhost = conf.supybot.protocols.irc.vhost()
reactor.connectTCP(server, port, self, bindAddress=(vhost, 0))
def clientConnectionFailed(self, connector, r):
drivers.log.connectError(self.currentServer, errorMsg(r))
(connector.host, connector.port) = self._getNextServer()
if not r.check(error.TimeoutError):
ReconnectingClientFactory.clientConnectionFailed(self, connector,r)
def clientConnectionLost(self, connector, r):
(connector.host, connector.port) = self._getNextServer()
ReconnectingClientFactory.clientConnectionLost(self, connector, r)
def startedConnecting(self, connector):
drivers.log.connect(self.currentServer)
def buildProtocol(self, addr):
protocol = ReconnectingClientFactory.buildProtocol(self, addr)
protocol.irc = self.irc
return protocol
Driver = SupyReconnectingFactory
try:
ignore(poller)
except NameError:
poller = TwistedRunnerDriver()
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

220
src/drivers/__init__.py Normal file
View File

@ -0,0 +1,220 @@
###
# 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.
###
"""
Contains various drivers (network, file, and otherwise) for using IRC objects.
"""
import supybot.fix as fix
import sys
import time
import socket
import supybot.log as supylog
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
_drivers = {}
_deadDrivers = []
_newDrivers = []
class IrcDriver(object):
"""Base class for drivers."""
def __init__(self, *args, **kwargs):
add(self.name(), self)
super(IrcDriver, self).__init__(*args, **kwargs)
def run(self):
raise NotImplementedError
def die(self):
# The end of any overrided die method should be
# "super(Class, self).die()", in order to make
# sure this (and anything else later added) is done.
remove(self.name())
def reconnect(self, wait=False):
raise NotImplementedError
def name(self):
return repr(self)
class ServersMixin(object):
def __init__(self, irc, servers=()):
self.networkGroup = conf.supybot.networks.get(irc.network)
self.servers = servers
super(ServersMixin, self).__init__(irc)
def _getServers(self):
# We do this, rather than itertools.cycle the servers in __init__,
# because otherwise registry updates given as setValues or sets
# wouldn't be visible until a restart.
return self.networkGroup.servers()[:] # Be sure to copy!
def _getNextServer(self):
if not self.servers:
self.servers = self._getServers()
assert self.servers, 'Servers value for %s is empty.' % \
self.networkGroup._name
server = self.servers.pop(0)
self.currentServer = '%s:%s' % server
return server
def empty():
"""Returns whether or not the driver loop is empty."""
return (len(_drivers) + len(_newDrivers)) == 0
def add(name, driver):
"""Adds a given driver the loop with the given name."""
_newDrivers.append((name, driver))
def remove(name):
"""Removes the driver with the given name from the loop."""
_deadDrivers.append(name)
def run():
"""Runs the whole driver loop."""
for (name, driver) in _drivers.iteritems():
try:
if name not in _deadDrivers:
driver.run()
except:
log.exception('Uncaught exception in in drivers.run:')
_deadDrivers.append(name)
for name in _deadDrivers:
try:
driver = _drivers[name]
if hasattr(driver, 'irc') and driver.irc is not None:
# The Schedule driver has no irc object, or it's None.
driver.irc.driver = None
driver.irc = None
log.info('Removing driver %s.', name)
del _drivers[name]
except KeyError:
pass
while _newDrivers:
(name, driver) = _newDrivers.pop()
log.debug('Adding new driver %s.', name)
if name in _drivers:
log.warning('Driver %s already added, killing it.', name)
_drivers[name].die()
del _drivers[name]
_drivers[name] = driver
class Log(object):
"""This is used to have a nice, consistent interface for drivers to use."""
def connect(self, server):
self.info('Connecting to %s.', server)
def connectError(self, server, e):
if isinstance(e, Exception):
if isinstance(e, socket.gaierror):
e = e.args[1]
else:
e = utils.exnToString(e)
self.warning('Error connecting to %s: %s', server, e)
def disconnect(self, server, e=None):
if e:
if isinstance(e, Exception):
e = utils.exnToString(e)
else:
e = str(e)
if not e.endswith('.'):
e += '.'
self.warning('Disconnect from %s: %s', server, e)
else:
self.info('Disconnect from %s.', server)
def reconnect(self, network, when=None):
s = 'Reconnecting to %s' % network
if when is not None:
if not isinstance(when, basestring):
when = self.timestamp(when)
s += ' at %s.' % when
else:
s += '.'
self.info(s)
def die(self, irc):
self.info('Driver for %s dying.', irc)
debug = staticmethod(supylog.debug)
info = staticmethod(supylog.info)
warning = staticmethod(supylog.warning)
error = staticmethod(supylog.warning)
critical = staticmethod(supylog.critical)
timestamp = staticmethod(supylog.timestamp)
exception = staticmethod(supylog.exception)
stat = staticmethod(supylog.stat)
log = Log()
def newDriver(irc, moduleName=None):
"""Returns a new driver for the given server using the irc given and using
conf.supybot.driverModule to determine what driver to pick."""
# XXX Eventually this should be made to load the drivers from a
# configurable directory in addition to the installed one.
if moduleName is None:
moduleName = conf.supybot.drivers.module()
if moduleName == 'default':
try:
import supybot.drivers.Twisted
moduleName = 'supybot.drivers.Twisted'
except ImportError:
# We formerly used 'del' here, but 2.4 fixes the bug that we added
# the 'del' for, so we need to make sure we don't complain if the
# module is cleaned up already.
sys.modules.pop('supybot.drivers.Twisted', None)
moduleName = 'supybot.drivers.Socket'
elif not moduleName.startswith('supybot.drivers.'):
moduleName = 'supybot.drivers.' + moduleName
driverModule = __import__(moduleName, {}, {}, ['not empty'])
log.debug('Creating new driver (%s) for %s.', moduleName, irc)
driver = driverModule.Driver(irc)
irc.driver = driver
return driver
def parseMsg(s):
start = time.time()
s = s.strip()
if s:
msg = ircmsgs.IrcMsg(s)
log.stat('Time to parse IrcMsg: %s', time.time()-start)
msg.tag('receivedAt', start)
return msg
else:
return None
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

239
src/fix.py Normal file
View File

@ -0,0 +1,239 @@
###
# 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.
###
"""
Fixes stuff that Python should have but doesn't.
"""
from __future__ import division
__all__ = []
exported = ['ignore', 'window', 'group', 'partition', 'set', 'frozenset',
'any', 'all', 'rsplit', 'dynamic']
import sys
import new
import atexit
import string
string.ascii = string.maketrans('', '')
import random
_choice = random.choice
def choice(iterable):
if isinstance(iterable, (list, tuple)):
return _choice(iterable)
else:
n = 1
m = new.module('') # Guaranteed unique value.
ret = m
for x in iterable:
if random.random() < 1/n:
ret = x
n += 1
if ret is m:
raise IndexError
return ret
random.choice = choice
def ignore(*args, **kwargs):
"""Simply ignore the arguments sent to it."""
pass
class DynamicScope(object):
def _getLocals(self, name):
f = sys._getframe().f_back.f_back # _getLocals <- __[gs]etattr__ <- ...
while f:
if name in f.f_locals:
return f.f_locals
f = f.f_back
raise NameError, name
def __getattr__(self, name):
try:
return self._getLocals(name)[name]
except (NameError, KeyError):
return None
def __setattr__(self, name, value):
self._getLocals(name)[name] = value
dynamic = DynamicScope()
if sys.version_info < (2, 4, 0):
def reversed(L):
"""Iterates through a sequence in reverse."""
for i in xrange(len(L) - 1, -1, -1):
yield L[i]
exported.append('reversed')
def window(L, size):
"""Returns a sliding 'window' through the list L of size size."""
assert not isinstance(L, int), 'Argument order swapped: window(L, size)'
if size < 1:
raise ValueError, 'size <= 0 disallowed.'
for i in xrange(len(L) - (size-1)):
yield L[i:i+size]
import itertools
def ilen(iterable):
"""Returns the length of an iterator."""
i = 0
for _ in iterable:
i += 1
return i
def trueCycle(iterable):
while 1:
yielded = False
for x in iterable:
yield x
yielded = True
if not yielded:
raise StopIteration
itertools.trueCycle = trueCycle
itertools.ilen = ilen
def groupby(key, iterable):
if key is None:
key = lambda x: x
it = iter(iterable)
value = it.next() # If there are no items, this takes an early exit
oldkey = key(value)
group = [value]
for value in it:
newkey = key(value)
if newkey != oldkey:
yield group
group = []
oldkey = newkey
group.append(value)
yield group
itertools.groupby = groupby
def group(seq, groupSize, noneFill=True):
"""Groups a given sequence into sublists of length groupSize."""
ret = []
L = []
i = groupSize
for elt in seq:
if i > 0:
L.append(elt)
else:
ret.append(L)
i = groupSize
L = []
L.append(elt)
i -= 1
if L:
if noneFill:
while len(L) < groupSize:
L.append(None)
ret.append(L)
return ret
def partition(p, L):
"""Partitions a list L based on a predicate p. Returns a (yes,no) tuple"""
no = []
yes = []
for elt in L:
if p(elt):
yes.append(elt)
else:
no.append(elt)
return (yes, no)
def any(p, seq):
"""Returns true if any element in seq satisfies predicate p."""
for elt in itertools.ifilter(p, seq):
return True
else:
return False
def all(p, seq):
"""Returns true if all elements in seq satisfy predicate p."""
for elt in itertools.ifilterfalse(p, seq):
return False
else:
return True
def rsplit(s, sep=None, maxsplit=-1):
"""Equivalent to str.split, except splitting from the right."""
if sys.version_info < (2, 4, 0):
if sep is not None:
sep = sep[::-1]
L = s[::-1].split(sep, maxsplit)
L.reverse()
return [s[::-1] for s in L]
else:
return s.rsplit(sep, maxsplit)
if sys.version_info < (2, 4, 0):
import operator
def itemgetter(i):
return lambda x: x[i]
def attrgetter(attr):
return lambda x: getattr(x, attr)
operator.itemgetter = itemgetter
operator.attrgetter = attrgetter
import csv
import cStringIO as StringIO
def join(L):
fd = StringIO.StringIO()
writer = csv.writer(fd)
writer.writerow(L)
return fd.getvalue().rstrip('\r\n')
def split(s):
fd = StringIO.StringIO(s)
reader = csv.reader(fd)
return reader.next()
csv.join = join
csv.split = split
import sets
set = sets.Set
frozenset = sets.ImmutableSet
import socket
# Some socket modules don't have sslerror, so we'll just make it an error.
if not hasattr(socket, 'sslerror'):
socket.sslerror = socket.error
g = globals()
for name in exported:
__builtins__[name] = g[name]
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

1105
src/ircdb.py Normal file

File diff suppressed because it is too large Load Diff

991
src/irclib.py Normal file
View File

@ -0,0 +1,991 @@
###
# 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 supybot.fix as fix
import copy
import sets
import time
import random
import operator
from itertools import imap, chain, cycle
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
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
from supybot.structures import queue, smallqueue, RingBuffer
###
# The base class for a callback to be registered with an Irc object. Shows
# the required interface for callbacks -- name(),
# inFilter(irc, msg), outFilter(irc, msg), and __call__(irc, msg) [used so
# functions can be used as callbacks conceivable, and so if refactoring ever
# changes the nature of the callbacks from classes to functions, syntactical
# changes elsewhere won't be required.
###
class IrcCommandDispatcher(object):
"""Base class for classes that must dispatch on a command."""
def dispatchCommand(self, command):
"""Given a string 'command', dispatches to doCommand."""
return getattr(self, 'do' + command.capitalize(), None)
class IrcCallback(IrcCommandDispatcher):
"""Base class for standard callbacks.
Callbacks derived from this class should have methods of the form
"doCommand" -- doPrivmsg, doNick, do433, etc. These will be called
on matching messages.
"""
callAfter = ()
callBefore = ()
__metaclass__ = log.MetaFirewall
__firewalled__ = {'die': None,
'reset': None,
'__call__': None,
'__lt__': lambda self: 0,
'inFilter': lambda self, irc, msg: msg,
'outFilter': lambda self, irc, msg: msg,
'name': lambda self: self.__class__.__name__,}
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.name())
def name(self):
"""Returns the name of the callback."""
return self.__class__.__name__
def callPrecedence(self, irc):
"""Returns a pair of (callbacks to call before me,
callbacks to call after me)"""
after = []
before = []
for name in self.callBefore:
cb = irc.getCallback(name)
if cb is not None:
after.append(cb)
for name in self.callAfter:
cb = irc.getCallback(name)
if cb is not None:
before.append(cb)
assert self not in after, '%s was in its own after.' % self.name()
assert self not in before, '%s was in its own before.' % self.name()
return (before, after)
def inFilter(self, irc, msg):
"""Used for filtering/modifying messages as they're entering.
ircmsgs.IrcMsg objects are immutable, so this method is expected to
return another ircmsgs.IrcMsg object. Obviously the same IrcMsg
can be returned.
"""
return msg
def outFilter(self, irc, msg):
"""Used for filtering/modifying messages as they're leaving.
As with inFilter, an IrcMsg is returned.
"""
return msg
def __call__(self, irc, msg):
"""Used for handling each message."""
method = self.dispatchCommand(msg.command)
if method is not None:
method(irc, msg)
def reset(self):
"""Resets the callback. Called when reconnecting to the server."""
pass
def die(self):
"""Makes the callback die. Called when the parent Irc object dies."""
pass
###
# Basic queue for IRC messages. It doesn't presently (but should at some
# later point) reorder messages based on priority or penalty calculations.
###
_high = sets.ImmutableSet(['MODE', 'KICK', 'PONG', 'NICK', 'PASS', 'CAPAB'])
_low = sets.ImmutableSet(['PRIVMSG', 'PING', 'WHO', 'NOTICE'])
class IrcMsgQueue(object):
"""Class for a queue of IrcMsgs. Eventually, it should be smart.
Probably smarter than it is now, though it's gotten quite a bit smarter
than it originally was. A method to "score" methods, and a heapq to
maintain a priority queue of the messages would be the ideal way to do
intelligent queuing.
As it stands, however, we simple keep track of 'high priority' messages,
'low priority' messages, and normal messages, and just make sure to return
the 'high priority' ones before the normal ones before the 'low priority'
ones.
"""
__slots__ = ('msgs', 'highpriority', 'normal', 'lowpriority')
def __init__(self, iterable=()):
self.reset()
for msg in iterable:
self.enqueue(msg)
def reset(self):
"""Clears the queue."""
self.highpriority = smallqueue()
self.normal = smallqueue()
self.lowpriority = smallqueue()
self.msgs = sets.Set()
def enqueue(self, msg):
"""Enqueues a given message."""
if msg in self.msgs and \
not conf.supybot.protocols.irc.queueDuplicateMessages():
s = str(msg).strip()
log.info('Not adding message %s to queue, already added.',
utils.quoted(s))
return False
else:
self.msgs.add(msg)
if msg.command in _high:
self.highpriority.enqueue(msg)
elif msg.command in _low:
self.lowpriority.enqueue(msg)
else:
self.normal.enqueue(msg)
return True
def dequeue(self):
"""Dequeues a given message."""
msg = None
if self.highpriority:
msg = self.highpriority.dequeue()
elif self.normal:
msg = self.normal.dequeue()
elif self.lowpriority:
msg = self.lowpriority.dequeue()
if msg:
try:
self.msgs.remove(msg)
except KeyError:
s = 'Odd, dequeuing a message that\'s not in self.msgs.'
log.warning(s)
return msg
def __nonzero__(self):
return bool(self.highpriority or self.normal or self.lowpriority)
def __len__(self):
return sum(imap(len,[self.highpriority,self.lowpriority,self.normal]))
def __repr__(self):
name = self.__class__.__name__
return '%s(%r)' % (name, list(chain(self.highpriority,
self.normal,
self.lowpriority)))
__str__ = __repr__
###
# Maintains the state of IRC connection -- the most recent messages, the
# status of various modes (especially ops/halfops/voices) in channels, etc.
###
class ChannelState(object):
__slots__ = ('users', 'ops', 'halfops', 'bans',
'voices', 'topic', 'modes', 'created')
def __init__(self):
self.topic = ''
self.created = 0
self.ops = ircutils.IrcSet()
self.bans = ircutils.IrcSet()
self.users = ircutils.IrcSet()
self.voices = ircutils.IrcSet()
self.halfops = ircutils.IrcSet()
self.modes = ircutils.IrcDict()
def isOp(self, nick):
return nick in self.ops
def isVoice(self, nick):
return nick in self.voices
def isHalfop(self, nick):
return nick in self.halfops
def addUser(self, user):
"Adds a given user to the ChannelState. Power prefixes are handled."
nick = user.lstrip('@%+')
if not nick:
return
while user and user[0] in '@%+':
(marker, user) = (user[0], user[1:])
assert user, 'Looks like my caller is passing chars, not nicks.'
if marker == '@':
self.ops.add(nick)
elif marker == '%':
self.halfops.add(nick)
elif marker == '+':
self.voices.add(nick)
self.users.add(nick)
def replaceUser(self, oldNick, newNick):
"""Changes the user oldNick to newNick; used for NICK changes."""
# Note that this doesn't have to have the sigil (@%+) that users
# have to have for addUser; it just changes the name of the user
# without changing any of his categories.
for s in (self.users, self.ops, self.halfops, self.voices):
if oldNick in s:
s.remove(oldNick)
s.add(newNick)
def removeUser(self, user):
"""Removes a given user from the channel."""
self.users.discard(user)
self.ops.discard(user)
self.halfops.discard(user)
self.voices.discard(user)
def setMode(self, mode, value=None):
assert mode not in 'ovhbeq'
self.modes[mode] = value
def unsetMode(self, mode):
assert mode not in 'ovhbeq'
if mode in self.modes:
del self.modes[mode]
def doMode(self, msg):
def getSet(c):
if c == 'o':
set = self.ops
elif c == 'v':
set = self.voices
elif c == 'h':
set = self.halfops
elif c == 'b':
set = self.bans
else: # We don't care yet, so we'll just return an empty set.
set = sets.Set()
return set
for (mode, value) in ircutils.separateModes(msg.args[1:]):
(action, modeChar) = mode
if modeChar in 'ovhbeq': # We don't handle e or q yet.
set = getSet(modeChar)
if action == '-':
set.discard(value)
elif action == '+':
set.add(value)
else:
if action == '+':
self.setMode(modeChar, value)
else:
assert action == '-'
self.unsetMode(modeChar)
def __getstate__(self):
return [getattr(self, name) for name in self.__slots__]
def __setstate__(self, t):
for (name, value) in zip(self.__slots__, t):
setattr(self, name, value)
def __eq__(self, other):
ret = True
for name in self.__slots__:
ret = ret and getattr(self, name) == getattr(other, name)
return ret
def __ne__(self, other):
# This shouldn't even be necessary, grr...
return not self == other
class IrcState(IrcCommandDispatcher):
"""Maintains state of the Irc connection. Should also become smarter.
"""
__metaclass__ = log.MetaFirewall
__firewalled__ = {'addMsg': None}
def __init__(self, history=None, supported=None,
nicksToHostmasks=None, channels=None):
if history is None:
history = RingBuffer(conf.supybot.protocols.irc.maxHistoryLength())
if supported is None:
supported = utils.InsensitivePreservingDict()
if nicksToHostmasks is None:
nicksToHostmasks = ircutils.IrcDict()
if channels is None:
channels = ircutils.IrcDict()
self.supported = supported
self.history = history
self.channels = channels
self.nicksToHostmasks = nicksToHostmasks
def reset(self):
"""Resets the state to normal, unconnected state."""
self.history.reset()
self.channels.clear()
self.supported.clear()
self.nicksToHostmasks.clear()
self.history.resize(conf.supybot.protocols.irc.maxHistoryLength())
def __reduce__(self):
return (self.__class__, (self.history, self.supported,
self.nicksToHostmasks, self.channels))
def __eq__(self, other):
return self.history == other.history and \
self.channels == other.channels and \
self.supported == other.supported and \
self.nicksToHostmasks == other.nicksToHostmasks
def __ne__(self, other):
return not self == other
def copy(self):
ret = self.__class__()
ret.history = copy.deepcopy(self.history)
ret.nicksToHostmasks = copy.deepcopy(self.nicksToHostmasks)
ret.channels = copy.deepcopy(self.channels)
return ret
def addMsg(self, irc, msg):
"""Updates the state based on the irc object and the message."""
self.history.append(msg)
if ircutils.isUserHostmask(msg.prefix) and not msg.command == 'NICK':
self.nicksToHostmasks[msg.nick] = msg.prefix
method = self.dispatchCommand(msg.command)
if method is not None:
method(irc, msg)
def getTopic(self, channel):
"""Returns the topic for a given channel."""
return self.channels[channel].topic
def nickToHostmask(self, nick):
"""Returns the hostmask for a given nick."""
return self.nicksToHostmasks[nick]
_005converters = utils.InsensitivePreservingDict({
'modes': int,
'keylen': int,
'maxbans': int,
'nicklen': int,
'userlen': int,
'hostlen': int,
'kicklen': int,
'awaylen': int,
'silence': int,
'topiclen': int,
'channellen': int,
'maxtargets': int,
'maxnicklen': int,
'maxchannels': int,
'watch': int, # DynastyNet, EnterTheGame
})
def _prefixParser(s):
if ')' in s:
(left, right) = s.split(')')
assert left[0] == '(', 'Odd PREFIX in 005: %s' % s
left = left[1:]
assert len(left) == len(right), 'Odd PREFIX in 005: %s' % s
return dict(zip(left, right))
else:
return dict(zip('ovh', s))
_005converters['prefix'] = _prefixParser
del _prefixParser
def do005(self, irc, msg):
for arg in msg.args[1:-1]: # 0 is nick, -1 is "are supported"
if '=' in arg:
(name, value) = arg.split('=', 1)
converter = self._005converters.get(name, lambda x: x)
try:
self.supported[name] = converter(value)
except Exception, e:
log.exception('Uncaught exception in 005 converter:')
log.error('Name: %s, Converter: %s', name, converter)
else:
self.supported[arg] = None
def do352(self, irc, msg):
# WHO reply.
(nick, user, host) = (msg.args[5], msg.args[2], msg.args[3])
hostmask = '%s!%s@%s' % (nick, user, host)
self.nicksToHostmasks[nick] = hostmask
def do353(self, irc, msg):
# NAMES reply.
(_, type, channel, names) = msg.args
if channel not in self.channels:
self.channels[channel] = ChannelState()
c = self.channels[channel]
for name in names.split():
c.addUser(name)
if type == '@':
c.modes['s'] = None
def doJoin(self, irc, msg):
for channel in msg.args[0].split(','):
if channel in self.channels:
self.channels[channel].addUser(msg.nick)
elif msg.nick: # It must be us.
chan = ChannelState()
chan.addUser(msg.nick)
self.channels[channel] = chan
# I don't know why this assert was here.
#assert msg.nick == irc.nick, msg
def doMode(self, irc, msg):
channel = msg.args[0]
if ircutils.isChannel(channel): # There can be user modes, as well.
try:
chan = self.channels[channel]
except KeyError:
chan = ChannelState()
self.channels[channel] = chan
chan.doMode(msg)
def do324(self, irc, msg):
channel = msg.args[1]
chan = self.channels[channel]
for (mode, value) in ircutils.separateModes(msg.args[2:]):
modeChar = mode[1]
if mode[0] == '+' and mode[1] not in 'ovh':
chan.setMode(modeChar, value)
elif mode[0] == '-' and mode[1] not in 'ovh':
chan.unsetMode(modeChar)
def do329(self, irc, msg):
# This is the last part of an empty mode.
channel = msg.args[1]
chan = self.channels[channel]
chan.created = int(msg.args[2])
def doPart(self, irc, msg):
for channel in msg.args[0].split(','):
try:
chan = self.channels[channel]
except KeyError:
continue
if ircutils.strEqual(msg.nick, irc.nick):
del self.channels[channel]
else:
chan.removeUser(msg.nick)
def doKick(self, irc, msg):
(channel, users) = msg.args[:2]
chan = self.channels[channel]
for user in users.split(','):
chan.removeUser(user)
def doQuit(self, irc, msg):
for channel in self.channels.itervalues():
channel.removeUser(msg.nick)
if msg.nick in self.nicksToHostmasks:
# If we're quitting, it may not be.
del self.nicksToHostmasks[msg.nick]
def doTopic(self, irc, msg):
if len(msg.args) == 1:
return # Empty TOPIC for information. Does not affect state.
try:
chan = self.channels[msg.args[0]]
chan.topic = msg.args[1]
except KeyError:
pass # We don't have to be in a channel to send a TOPIC.
def do332(self, irc, msg):
chan = self.channels[msg.args[1]]
chan.topic = msg.args[2]
def doNick(self, irc, msg):
newNick = msg.args[0]
oldNick = msg.nick
try:
if msg.user and msg.host:
# Nick messages being handed out from the bot itself won't
# have the necessary prefix to make a hostmask.
newHostmask = ircutils.joinHostmask(newNick,msg.user,msg.host)
self.nicksToHostmasks[newNick] = newHostmask
del self.nicksToHostmasks[oldNick]
except KeyError:
pass
for channel in self.channels.itervalues():
channel.replaceUser(oldNick, newNick)
###
# The basic class for handling a connection to an IRC server. Accepts
# callbacks of the IrcCallback interface. Public attributes include 'driver',
# 'queue', and 'state', in addition to the standard nick/user/ident attributes.
###
_callbacks = []
class Irc(IrcCommandDispatcher):
"""The base class for an IRC connection.
Handles PING commands already.
"""
__metaclass__ = log.MetaFirewall
__firewalled__ = {'die': None,
'feedMsg': None,
'takeMsg': None,}
_nickSetters = sets.Set(['001', '002', '003', '004', '250', '251', '252',
'254', '255', '265', '266', '372', '375', '376',
'333', '353', '332', '366', '005'])
# We specifically want these callbacks to be common between all Ircs,
# that's why we don't do the normal None default with a check.
def __init__(self, network, callbacks=_callbacks):
self.zombie = False
world.ircs.append(self)
self.network = network
self.callbacks = callbacks
self.state = IrcState()
self.queue = IrcMsgQueue()
self.fastqueue = smallqueue()
self.driver = None # The driver should set this later.
self._setNonResettingVariables()
self._queueConnectMessages()
self.startedSync = ircutils.IrcDict()
def isChannel(self, s):
"""Helper function to check whether a given string is a channel on
the network this Irc object is connected to."""
kw = {}
if 'chantypes' in self.state.supported:
kw['chantypes'] = self.state.supported['chantypes']
if 'channellen' in self.state.supported:
kw['channellen'] = self.state.supported['channellen']
return ircutils.isChannel(s, **kw)
def isNick(self, s):
kw = {}
if 'nicklen' in self.state.supported:
kw['nicklen'] = self.state.supported['nicklen']
return ircutils.isNick(s, **kw)
# This *isn't* threadsafe!
def addCallback(self, callback):
"""Adds a callback to the callbacks list."""
self.callbacks.append(callback)
# This is the new list we're building, which will be tsorted.
cbs = []
# The vertices are self.callbacks itself. Now we make the edges.
edges = sets.Set()
for cb in self.callbacks:
(before, after) = cb.callPrecedence(self)
assert cb not in after, 'cb was in its own after.'
assert cb not in before, 'cb was in its own before.'
for otherCb in before:
edges.add((otherCb, cb))
for otherCb in after:
edges.add((cb, otherCb))
def getFirsts():
firsts = sets.Set(self.callbacks) - sets.Set(cbs)
for (before, after) in edges:
firsts.discard(after)
return firsts
firsts = getFirsts()
while firsts:
# Then we add these to our list of cbs, and remove all edges that
# originate with these cbs.
for cb in firsts:
cbs.append(cb)
edgesToRemove = []
for edge in edges:
if edge[0] is cb:
edgesToRemove.append(edge)
for edge in edgesToRemove:
edges.remove(edge)
firsts = getFirsts()
assert len(cbs) == len(self.callbacks), \
'cbs: %s, self.callbacks: %s' % (cbs, self.callbacks)
self.callbacks[:] = cbs
def getCallback(self, name):
"""Gets a given callback by name."""
name = name.lower()
for callback in self.callbacks:
if callback.name().lower() == name:
return callback
else:
return None
def removeCallback(self, name):
"""Removes a callback from the callback list."""
name = name.lower()
def nameMatches(cb):
return cb.name().lower() == name
(bad, good) = partition(nameMatches, self.callbacks)
self.callbacks[:] = good
return bad
def queueMsg(self, msg):
"""Queues a message to be sent to the server."""
if not self.zombie:
return self.queue.enqueue(msg)
else:
log.warning('Refusing to queue %r; %s is a zombie.', msg, self)
return False
def sendMsg(self, msg):
"""Queues a message to be sent to the server *immediately*"""
if not self.zombie:
self.fastqueue.enqueue(msg)
else:
log.warning('Refusing to send %r; %s is a zombie.', msg, self)
def takeMsg(self):
"""Called by the IrcDriver; takes a message to be sent."""
if not self.callbacks:
log.critical('No callbacks in %s.', self)
now = time.time()
msg = None
if self.fastqueue:
msg = self.fastqueue.dequeue()
elif self.queue:
if now-self.lastTake <= conf.supybot.protocols.irc.throttleTime():
log.debug('Irc.takeMsg throttling.')
else:
self.lastTake = now
msg = self.queue.dequeue()
elif self.afterConnect and \
conf.supybot.protocols.irc.ping() and \
now > self.lastping + conf.supybot.protocols.irc.ping.interval():
if self.outstandingPing:
s = 'Ping sent at %s not replied to.' % \
log.timestamp(self.lastping)
log.warning(s)
# Let's notify the plugins that we're reconnecting.
self.feedMsg(ircmsgs.error(s))
self.driver.reconnect()
elif not self.zombie:
self.lastping = now
now = str(int(now))
self.outstandingPing = True
self.queueMsg(ircmsgs.ping(now))
if msg:
for callback in reversed(self.callbacks):
msg = callback.outFilter(self, msg)
if msg is None:
log.debug('%s.outFilter returned None.' % callback.name())
return self.takeMsg()
world.debugFlush()
if len(str(msg)) > 512:
# Yes, this violates the contract, but at this point it doesn't
# matter. That's why we gotta go munging in private attributes
#
# I'm changing this to a log.debug to fix a possible loop in
# the LogToIrc plugin. Since users can't do anything about
# this issue, there's no fundamental reason to make it a
# warning.
log.debug('Truncating %r, message is too long.', msg)
msg._str = msg._str[:500] + '\r\n'
msg._len = len(str(msg))
# I don't think we should do this. Why should it matter? If it's
# something important, then the server will send it back to us,
# and if it's just a privmsg/notice/etc., we don't care.
# On second thought, we need this for testing.
if world.testing:
self.state.addMsg(self, msg)
log.debug('Outgoing message: ' + str(msg).rstrip('\r\n'))
if msg.command == 'JOIN':
channels = msg.args[0].split(',')
for channel in channels:
# Let's make this more accurate.
self.startedSync[channel] = time.time()
return msg
elif self.zombie:
# We kill the driver here so it doesn't continue to try to
# take messages from us.
self.driver.die()
self._reallyDie()
else:
return None
def feedMsg(self, msg):
"""Called by the IrcDriver; feeds a message received."""
msg.tag('receivedBy', self)
msg.tag('receivedOn', self.network)
if msg.args and self.isChannel(msg.args[0]):
channel = msg.args[0]
else:
channel = None
log.debug('Incoming message: ' + str(msg).rstrip('\r\n'))
# Yeah, so this is odd. Some networks (oftc) seem to give us certain
# messages with our nick instead of our prefix. We'll fix that here.
if msg.prefix == self.nick:
log.debug('Got one of those odd nick-instead-of-prefix msgs.')
msg = ircmsgs.IrcMsg(prefix=self.prefix, msg=msg)
# This catches cases where we know our own nick (from sending it to the
# server) but we don't yet know our prefix.
if msg.nick == self.nick and self.prefix != msg.prefix:
self.prefix = msg.prefix
# This keeps our nick and server attributes updated.
if msg.command in self._nickSetters:
if msg.args[0] != self.nick:
self.nick = msg.args[0]
log.debug('Updating nick attribute to %s.', self.nick)
if msg.prefix != self.server:
self.server = msg.prefix
log.debug('Updating server attribute to %s.')
# Dispatch to specific handlers for commands.
method = self.dispatchCommand(msg.command)
if method is not None:
method(msg)
# Now update the IrcState object.
try:
self.state.addMsg(self, msg)
except:
log.exception('Exception in update of IrcState object:')
# Now call the callbacks.
world.debugFlush()
for callback in self.callbacks:
try:
m = callback.inFilter(self, msg)
if not m:
log.debug('%s.inFilter returned None' % callback.name())
return
msg = m
except:
log.exception('Uncaught exception in inFilter:')
world.debugFlush()
for callback in self.callbacks:
try:
if callback is not None:
callback(self, msg)
except:
log.exception('Uncaught exception in callback:')
world.debugFlush()
def die(self):
"""Makes the Irc object *promise* to die -- but it won't die (of its
own volition) until all its queues are clear. Isn't that cool?"""
self.zombie = True
if not self.afterConnect:
self._reallyDie()
# This is useless because it's in world.ircs, so it won't be deleted until
# the program exits. Just figured you might want to know.
#def __del__(self):
# self._reallyDie()
def reset(self):
"""Resets the Irc object. Called when the driver reconnects."""
self._setNonResettingVariables()
self.state.reset()
self.queue.reset()
self.fastqueue.reset()
self.startedSync.clear()
for callback in self.callbacks:
callback.reset()
self._queueConnectMessages()
def _setNonResettingVariables(self):
# Configuration stuff.
self.nick = conf.supybot.nick()
self.user = conf.supybot.user()
self.ident = conf.supybot.ident()
self.alternateNicks = conf.supybot.nick.alternates()[:]
self.password = conf.supybot.networks.get(self.network).password()
self.prefix = '%s!%s@%s' % (self.nick, self.ident, 'unset.domain')
# The rest.
self.lastTake = 0
self.server = 'unset'
self.afterConnect = False
self.lastping = time.time()
self.outstandingPing = False
def _queueConnectMessages(self):
if self.zombie:
self.driver.die()
self._reallyDie()
else:
if self.password:
log.info('Sending PASS command, not logging the password.')
self.queueMsg(ircmsgs.password(self.password))
log.debug('Queuing NICK command, nick is %s.', self.nick)
self.queueMsg(ircmsgs.nick(self.nick))
log.debug('Queuing USER command, ident is %s, user is %s.',
self.ident, self.user)
self.queueMsg(ircmsgs.user(self.ident, self.user))
def _getNextNick(self):
if self.alternateNicks:
nick = self.alternateNicks.pop(0)
if '%s' in nick:
nick %= conf.supybot.nick()
return nick
else:
nick = conf.supybot.nick()
ret = nick
L = list(nick)
while len(L) <= 3:
L.append('`')
while ircutils.strEqual(ret, nick):
L[random.randrange(len(L))] = random.choice('0123456789')
ret = ''.join(L)
return ret
def do002(self, msg):
"""Logs the ircd version."""
(beginning, version) = rsplit(msg.args[-1], maxsplit=1)
log.info('Server %s has version %s', self.server, version)
def doPing(self, msg):
"""Handles PING messages."""
self.sendMsg(ircmsgs.pong(msg.args[0]))
def doPong(self, msg):
"""Handles PONG messages."""
self.outstandingPing = False
def do376(self, msg):
log.info('Got end of MOTD from %s', self.server)
self.afterConnect = True
# Let's reset nicks in case we had to use a weird one.
self.alternateNicks = conf.supybot.nick.alternates()[:]
umodes = conf.supybot.protocols.irc.umodes()
if umodes:
if umodes[0] not in '+-':
umodes = '+' + umodes
log.info('Sending user modes to %s: %s', self.network, umodes)
self.sendMsg(ircmsgs.mode(self.nick, umodes))
do377 = do422 = do376
def do433(self, msg):
"""Handles 'nickname already in use' messages."""
if not self.afterConnect:
newNick = self._getNextNick()
assert newNick != self.nick
log.info('Got 433: %s is in use. Trying %s.', self.nick, newNick)
self.sendMsg(ircmsgs.nick(newNick))
do432 = do433 # 432: Erroneous nickname.
def doJoin(self, msg):
if msg.nick == self.nick:
channel = msg.args[0]
self.queueMsg(ircmsgs.who(channel)) # Ends with 315.
self.queueMsg(ircmsgs.mode(channel)) # Ends with 329.
self.startedSync[channel] = time.time()
def do315(self, msg):
channel = msg.args[1]
popped = False
if channel in self.startedSync:
now = time.time()
started = self.startedSync.pop(channel)
elapsed = now - started
log.info('Join to %s on %s synced in %.2f seconds.',
channel, self.network, elapsed)
popped = True
if popped and not self.startedSync:
log.info('All channels synced on %s.', self.network)
def doError(self, msg):
"""Handles ERROR messages."""
log.info('Error message from %s: %s', self.network, msg.args[0])
if not self.zombie:
if msg.args[0].startswith('Closing Link'):
self.driver.reconnect()
elif 'too fast' in msg.args[0]: # Connecting too fast.
self.driver.reconnect(wait=True)
def doNick(self, msg):
"""Handles NICK messages."""
if msg.nick == self.nick:
newNick = msg.args[0]
self.nick = newNick
(nick, user, domain) = ircutils.splitHostmask(msg.prefix)
self.prefix = ircutils.joinHostmask(self.nick, user, domain)
elif conf.supybot.followIdentificationThroughNickChanges():
# We use elif here because this means it's someone else's nick
# change, not our own.
try:
id = ircdb.users.getUserId(msg.prefix)
u = ircdb.users.getUser(id)
except KeyError:
return
if u.auth:
(_, user, host) = ircutils.splitHostmask(msg.prefix)
newhostmask = ircutils.joinHostmask(msg.args[0], user, host)
for (i, (when, authmask)) in enumerate(u.auth[:]):
if ircutils.strEqual(msg.prefix, authmask):
log.info('Following identification for %s: %s -> %s',
u.name, authmask, newhostmask)
u.auth[i] = (u.auth[i][0], newhostmask)
ircdb.users.setUser(u)
def _reallyDie(self):
"""Makes the Irc object die. Dead."""
log.info('Irc object for %s dying.' % self.network)
# XXX This hasattr should be removed, I'm just putting it here because
# we're so close to a release. After 0.80.0 we should remove this
# and fix whatever AttributeErrors arise in the drivers themselves.
if self.driver is not None and hasattr(self.driver, 'die'):
self.driver.die()
if self in world.ircs:
world.ircs.remove(self)
# Only kill the callbacks if we're the last Irc.
if not world.ircs:
for cb in self.callbacks:
cb.die()
# If we shared our list of callbacks, this ensures that
# cb.die() is only called once for each callback. It's
# not really necessary since we already check to make sure
# we're the only Irc object, but a little robustitude never
# hurt anybody.
log.debug('Last Irc, clearing callbacks.')
self.callbacks[:] = []
else:
log.warning('Irc object killed twice: %s', utils.stackTrace())
def __hash__(self):
return id(self)
def __eq__(self, other):
return id(self) == id(other)
def __ne__(self, other):
return not (self == other)
def __str__(self):
return 'Irc object for %s' % self.network
def __repr__(self):
return '<irclib.Irc object for %s>' % self.network
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

767
src/ircmsgs.py Normal file
View File

@ -0,0 +1,767 @@
###
# 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.
###
"""
This module provides the basic IrcMsg object used throughout the bot to
represent the actual messages. It also provides several helper functions to
construct such messages in an easier way than the constructor for the IrcMsg
object (which, as you'll read later, is quite...full-featured :))
"""
import supybot.fix as fix
import re
import time
import string
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircutils as ircutils
###
# IrcMsg class -- used for representing IRC messages acquired from a network.
###
class MalformedIrcMsg(ValueError):
pass
class IrcMsg(object):
"""Class to represent an IRC message.
As usual, ignore attributes that begin with an underscore. They simply
don't exist. Instances of this class are *not* to be modified, since they
are hashable. Public attributes of this class are .prefix, .command,
.args, .nick, .user, and .host.
The constructor for this class is pretty intricate. It's designed to take
any of three major (sets of) arguments.
Called with no keyword arguments, it takes a single string that is a raw
IRC message (such as one taken straight from the network.
Called with keyword arguments, it *requires* a command parameter. Args is
optional, but with most commands will be necessary. Prefix is obviously
optional, since clients aren't allowed (well, technically, they are, but
only in a completely useless way) to send prefixes to the server.
Since this class isn't to be modified, the constructor also accepts a 'msg'
keyword argument representing a message from which to take all the
attributes not provided otherwise as keyword arguments. So, for instance,
if a programmer wanted to take a PRIVMSG he'd gotten and simply redirect it
to a different source, he could do this:
IrcMsg(prefix='', args=(newSource, otherMsg.args[1]), msg=otherMsg)
"""
# It's too useful to be able to tag IrcMsg objects with extra, unforeseen
# data. Goodbye, __slots__.
# On second thought, let's use methods for tagging.
__slots__ = ('args', 'command', 'host', 'nick', 'prefix', 'user',
'_hash', '_str', '_repr', '_len', 'tags')
def __init__(self, s='', command='', args=(), prefix='', msg=None):
assert not (msg and s), 'IrcMsg.__init__ cannot accept both s and msg'
if not s and not command and not msg:
raise MalformedIrcMsg, 'IRC messages require a command.'
self._str = None
self._repr = None
self._hash = None
self._len = None
self.tags = {}
if s:
originalString = s
try:
if not s.endswith('\n'):
s += '\n'
self._str = s
if s[0] == ':':
self.prefix, s = s[1:].split(None, 1)
else:
self.prefix = ''
if ' :' in s: # Note the space: IPV6 addresses are bad w/o it.
s, last = s.split(' :', 1)
self.args = s.split()
self.args.append(last.rstrip('\r\n'))
else:
self.args = s.split()
self.command = self.args.pop(0)
except (IndexError, ValueError):
raise MalformedIrcMsg, repr(originalString)
else:
if msg is not None:
if prefix:
self.prefix = prefix
else:
self.prefix = msg.prefix
if command:
self.command = command
else:
self.command = msg.command
if args:
self.args = args
else:
self.args = msg.args
self.tags = msg.tags.copy()
else:
self.prefix = prefix
self.command = command
assert all(ircutils.isValidArgument, args)
self.args = args
self.args = tuple(self.args)
if isUserHostmask(self.prefix):
(self.nick,self.user,self.host)=ircutils.splitHostmask(self.prefix)
else:
(self.nick, self.user, self.host) = (self.prefix,)*3
def __str__(self):
if self._str is not None:
return self._str
if self.prefix:
if len(self.args) > 1:
self._str = ':%s %s %s :%s\r\n' % \
(self.prefix, self.command,
' '.join(self.args[:-1]), self.args[-1])
else:
if self.args:
self._str = ':%s %s :%s\r\n' % \
(self.prefix, self.command, self.args[0])
else:
self._str = ':%s %s\r\n' % (self.prefix, self.command)
else:
if len(self.args) > 1:
self._str = '%s %s :%s\r\n' % \
(self.command,
' '.join(self.args[:-1]), self.args[-1])
else:
if self.args:
self._str = '%s :%s\r\n' % (self.command, self.args[0])
else:
self._str = '%s\r\n' % self.command
return self._str
def __len__(self):
return len(str(self))
def __eq__(self, other):
return isinstance(other, self.__class__) and \
hash(self) == hash(other) and \
self.command == other.command and \
self.prefix == other.prefix and \
self.args == other.args
__req__ = __eq__ # I don't know exactly what this does, but it can't hurt.
def __ne__(self, other):
return not (self == other)
__rne__ = __ne__ # Likewise as above.
def __hash__(self):
if self._hash is not None:
return self._hash
self._hash = hash(self.command) ^ \
hash(self.prefix) ^ \
hash(self.args)
return self._hash
def __repr__(self):
if self._repr is not None:
return self._repr
self._repr = 'IrcMsg(prefix=%s, command=%s, args=%r)' % \
(utils.quoted(self.prefix), utils.quoted(self.command),
self.args)
return self._repr
def __reduce__(self):
return (self.__class__, (str(self),))
def tag(self, tag, value=True):
self.tags[tag] = value
def tagged(self, tag):
return self.tags.get(tag) # Returns None if it's not there.
def __getattr__(self, attr):
return self.tagged(attr)
def isCtcp(msg):
"""Returns whether or not msg is a CTCP message."""
return msg.command in ('PRIVMSG', 'NOTICE') and \
msg.args[1].startswith('\x01') and \
msg.args[1].endswith('\x01') and \
len(msg.args[1]) >= 2
def isAction(msg):
"""A predicate returning true if the PRIVMSG in question is an ACTION"""
if isCtcp(msg):
s = msg.args[1]
payload = s[1:-1] # Chop off \x01.
command = payload.split(None, 1)[0]
return command == 'ACTION'
else:
return False
_unactionre = re.compile(r'^\x01ACTION\s+(.*)\x01$')
def unAction(msg):
"""Returns the payload (i.e., non-ACTION text) of an ACTION msg."""
assert isAction(msg)
return _unactionre.match(msg.args[1]).group(1)
def _escape(s):
s = s.replace('&', '&amp;')
s = s.replace('"', '&quot;')
s = s.replace('<', '&lt;')
s = s.replace('>', '&gt;')
return s
def toXml(msg, pretty=True, includeTime=True):
assert msg.command == _escape(msg.command)
L = []
L.append('<msg command="%s" prefix="%s"'%(msg.command,_escape(msg.prefix)))
if includeTime:
L.append(' time="%s"' % time.time())
L.append('>')
if pretty:
L.append('\n')
for arg in msg.args:
if pretty:
L.append(' ')
L.append('<arg>%s</arg>' % _escape(arg))
if pretty:
L.append('\n')
L.append('</msg>\n')
return ''.join(L)
def prettyPrint(msg, addRecipients=False, timestampFormat=None, showNick=True):
"""Provides a client-friendly string form for messages.
IIRC, I copied BitchX's (or was it XChat's?) format for messages.
"""
def nickorprefix():
return msg.nick or msg.prefix
def nick():
if addRecipients:
return '%s/%s' % (msg.nick, msg.args[0])
else:
return msg.nick
if msg.command == 'PRIVMSG':
m = _unactionre.match(msg.args[1])
if m:
s = '* %s %s' % (nick(), m.group(1))
else:
if not showNick:
s = '%s' % msg.args[1]
else:
s = '<%s> %s' % (nick(), msg.args[1])
elif msg.command == 'NOTICE':
if not showNick:
s = '%s' % msg.args[1]
else:
s = '-%s- %s' % (nick(), msg.args[1])
elif msg.command == 'JOIN':
s = '*** %s has joined %s' % (msg.nick, msg.args[0])
elif msg.command == 'PART':
if len(msg.args) > 1:
partmsg = ' (%s)' % msg.args[1]
else:
partmsg = ''
s = '*** %s has parted %s%s' % (msg.nick, msg.args[0], partmsg)
elif msg.command == 'KICK':
if len(msg.args) > 2:
kickmsg = ' (%s)' % msg.args[1]
else:
kickmsg = ''
s = '*** %s was kicked by %s%s' % (msg.args[1], msg.nick, kickmsg)
elif msg.command == 'MODE':
s = '*** %s sets mode: %s' % (nickorprefix(), ' '.join(msg.args))
elif msg.command == 'QUIT':
if msg.args:
quitmsg = ' (%s)' % msg.args[0]
else:
quitmsg = ''
s = '*** %s has quit IRC%s' % (msg.nick, quitmsg)
elif msg.command == 'TOPIC':
s = '*** %s changes topic to %s' % (nickorprefix(), msg.args[1])
elif msg.command == 'NICK':
s = '*** %s is now known as %s' % (msg.nick, msg.args[0])
at = getattr(msg, 'receivedAt', None)
if timestampFormat and at:
s = '%s %s' % (time.strftime(timestampFormat, time.localtime(at)), s)
return s
###
# Various IrcMsg functions
###
isNick = ircutils.isNick
isChannel = ircutils.isChannel
isUserHostmask = ircutils.isUserHostmask
def pong(payload, prefix='', msg=None):
"""Takes a payload and returns the proper PONG IrcMsg."""
if conf.supybot.protocols.irc.strictRfc():
assert payload, 'PONG requires a payload'
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='PONG', args=(payload,), msg=msg)
def ping(payload, prefix='', msg=None):
"""Takes a payload and returns the proper PING IrcMsg."""
if conf.supybot.protocols.irc.strictRfc():
assert payload, 'PING requires a payload'
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='PING', args=(payload,), msg=msg)
def op(channel, nick, prefix='', msg=None):
"""Returns a MODE to op nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '+o', nick), msg=msg)
def ops(channel, nicks, prefix='', msg=None):
"""Returns a MODE to op each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert nicks, 'Nicks must not be empty.'
assert all(isNick, nicks), nicks
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '+' + ('o'*len(nicks))) + tuple(nicks),
msg=msg)
def deop(channel, nick, prefix='', msg=None):
"""Returns a MODE to deop nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '-o', nick), msg=msg)
def deops(channel, nicks, prefix='', msg=None):
"""Returns a MODE to deop each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert nicks, 'Nicks must not be empty.'
assert all(isNick, nicks), nicks
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE', msg=msg,
args=(channel, '-' + ('o'*len(nicks))) + tuple(nicks))
def halfop(channel, nick, prefix='', msg=None):
"""Returns a MODE to halfop nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '+h', nick), msg=msg)
def halfops(channel, nicks, prefix='', msg=None):
"""Returns a MODE to halfop each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert nicks, 'Nicks must not be empty.'
assert all(isNick, nicks), nicks
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE', msg=msg,
args=(channel, '+' + ('h'*len(nicks))) + tuple(nicks))
def dehalfop(channel, nick, prefix='', msg=None):
"""Returns a MODE to dehalfop nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '-h', nick), msg=msg)
def dehalfops(channel, nicks, prefix='', msg=None):
"""Returns a MODE to dehalfop each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert nicks, 'Nicks must not be empty.'
assert all(isNick, nicks), nicks
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE', msg=msg,
args=(channel, '-' + ('h'*len(nicks))) + tuple(nicks))
def voice(channel, nick, prefix='', msg=None):
"""Returns a MODE to voice nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '+v', nick), msg=msg)
def voices(channel, nicks, prefix='', msg=None):
"""Returns a MODE to voice each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert nicks, 'Nicks must not be empty.'
assert all(isNick, nicks)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE', msg=msg,
args=(channel, '+' + ('v'*len(nicks))) + tuple(nicks))
def devoice(channel, nick, prefix='', msg=None):
"""Returns a MODE to devoice nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '-v', nick), msg=msg)
def devoices(channel, nicks, prefix='', msg=None):
"""Returns a MODE to devoice each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert nicks, 'Nicks must not be empty.'
assert all(isNick, nicks), nicks
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE', msg=msg,
args=(channel, '-' + ('v'*len(nicks))) + tuple(nicks))
def ban(channel, hostmask, exception='', prefix='', msg=None):
"""Returns a MODE to ban nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isUserHostmask(hostmask), repr(hostmask)
modes = [('+b', hostmask)]
if exception:
modes.append(('+e', exception))
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=[channel] + ircutils.joinModes(modes), msg=msg)
def bans(channel, hostmasks, exceptions=(), prefix='', msg=None):
"""Returns a MODE to ban each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert all(isUserHostmask, hostmasks), hostmasks
modes = [('+b', s) for s in hostmasks] + [('+e', s) for s in exceptions]
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=[channel] + ircutils.joinModes(modes), msg=msg)
def unban(channel, hostmask, prefix='', msg=None):
"""Returns a MODE to unban nick on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isUserHostmask(hostmask), repr(hostmask)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE',
args=(channel, '-b', hostmask), msg=msg)
def unbans(channel, hostmasks, prefix='', msg=None):
"""Returns a MODE to unban each of nicks on channel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert all(isUserHostmask, hostmasks), hostmasks
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='MODE', msg=msg,
args=(channel, '-' + ('b'*len(hostmasks)), hostmasks))
def kick(channel, nick, s='', prefix='', msg=None):
"""Returns a KICK to kick nick from channel with the message msg."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
if s:
return IrcMsg(prefix=prefix, command='KICK',
args=(channel, nick, s), msg=msg)
else:
return IrcMsg(prefix=prefix, command='KICK',
args=(channel, nick), msg=msg)
def kicks(channel, nicks, s='', prefix='', msg=None):
"""Returns a KICK to kick each of nicks from channel with the message msg.
"""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
assert all(isNick, nicks), nicks
if msg and not prefix:
prefix = msg.prefix
if s:
return IrcMsg(prefix=prefix, command='KICK',
args=(channel, ','.join(nicks), s), msg=msg)
else:
return IrcMsg(prefix=prefix, command='KICK',
args=(channel, ','.join(nicks)), msg=msg)
def privmsg(recipient, s, prefix='', msg=None):
"""Returns a PRIVMSG to recipient with the message msg."""
if conf.supybot.protocols.irc.strictRfc():
assert (isChannel(recipient) or isNick(recipient)), repr(recipient)
assert s, 's must not be empty.'
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='PRIVMSG',
args=(recipient, s), msg=msg)
def dcc(recipient, kind, *args, **kwargs):
# Stupid Python won't allow (recipient, kind, *args, prefix=''), so we have
# to use the **kwargs form. Blech.
assert isNick(recipient), 'Can\'t DCC a channel.'
kind = kind.upper()
assert kind in ('SEND', 'CHAT', 'RESUME', 'ACCEPT'), 'Invalid DCC command.'
args = (kind,) + args
return IrcMsg(prefix=kwargs.get('prefix', ''), command='PRIVMSG',
args=(recipient, ' '.join(args)))
def action(recipient, s, prefix='', msg=None):
"""Returns a PRIVMSG ACTION to recipient with the message msg."""
if conf.supybot.protocols.irc.strictRfc():
assert (isChannel(recipient) or isNick(recipient)), repr(recipient)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='PRIVMSG',
args=(recipient, '\x01ACTION %s\x01' % s), msg=msg)
def notice(recipient, s, prefix='', msg=None):
"""Returns a NOTICE to recipient with the message msg."""
if conf.supybot.protocols.irc.strictRfc():
assert (isChannel(recipient) or isNick(recipient)), repr(recipient)
assert s, 'msg must not be empty.'
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='NOTICE', args=(recipient, s), msg=msg)
def join(channel, key=None, prefix='', msg=None):
"""Returns a JOIN to a channel"""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
if msg and not prefix:
prefix = msg.prefix
if key is None:
return IrcMsg(prefix=prefix, command='JOIN', args=(channel,), msg=msg)
else:
if conf.supybot.protocols.irc.strictRfc():
assert key.translate(string.ascii, string.ascii[128:]) == key and \
'\x00' not in key and \
'\r' not in key and \
'\n' not in key and \
'\f' not in key and \
'\t' not in key and \
'\v' not in key and \
' ' not in key
return IrcMsg(prefix=prefix, command='JOIN',
args=(channel, key), msg=msg)
def joins(channels, keys=None, prefix='', msg=None):
"""Returns a JOIN to each of channels."""
if conf.supybot.protocols.irc.strictRfc():
assert all(isChannel, channels), channels
if msg and not prefix:
prefix = msg.prefix
if keys is None:
keys = []
assert len(keys) <= len(channels), 'Got more keys than channels.'
if not keys:
return IrcMsg(prefix=prefix,
command='JOIN',
args=(','.join(channels),), msg=msg)
else:
for key in keys:
if conf.supybot.protocols.irc.strictRfc():
assert key.translate(string.ascii,string.ascii[128:])==key and\
'\x00' not in key and \
'\r' not in key and \
'\n' not in key and \
'\f' not in key and \
'\t' not in key and \
'\v' not in key and \
' ' not in key
return IrcMsg(prefix=prefix,
command='JOIN',
args=(','.join(channels), ','.join(keys)), msg=msg)
def part(channel, s='', prefix='', msg=None):
"""Returns a PART from channel with the message msg."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
if msg and not prefix:
prefix = msg.prefix
if s:
return IrcMsg(prefix=prefix, command='PART',
args=(channel, s), msg=msg)
else:
return IrcMsg(prefix=prefix, command='PART',
args=(channel,), msg=msg)
def parts(channels, s='', prefix='', msg=None):
"""Returns a PART from each of channels with the message msg."""
if conf.supybot.protocols.irc.strictRfc():
assert all(isChannel, channels), channels
if msg and not prefix:
prefix = msg.prefix
if s:
return IrcMsg(prefix=prefix, command='PART',
args=(','.join(channels), s), msg=msg)
else:
return IrcMsg(prefix=prefix, command='PART',
args=(','.join(channels),), msg=msg)
def quit(s='', prefix='', msg=None):
"""Returns a QUIT with the message msg."""
if msg and not prefix:
prefix = msg.prefix
if s:
return IrcMsg(prefix=prefix, command='QUIT', args=(s,), msg=msg)
else:
return IrcMsg(prefix=prefix, command='QUIT', msg=msg)
def topic(channel, topic=None, prefix='', msg=None):
"""Returns a TOPIC for channel with the topic topic."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel), repr(channel)
if msg and not prefix:
prefix = msg.prefix
if topic is None:
return IrcMsg(prefix=prefix, command='TOPIC',
args=(channel,), msg=msg)
else:
return IrcMsg(prefix=prefix, command='TOPIC',
args=(channel, topic), msg=msg)
def nick(nick, prefix='', msg=None):
"""Returns a NICK with nick nick."""
if conf.supybot.protocols.irc.strictRfc():
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='NICK', args=(nick,), msg=msg)
def user(ident, user, prefix='', msg=None):
"""Returns a USER with ident ident and user user."""
if conf.supybot.protocols.irc.strictRfc():
assert '\x00' not in ident and \
'\r' not in ident and \
'\n' not in ident and \
' ' not in ident and \
'@' not in ident
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='USER',
args=(ident, '0', '*', user), msg=msg)
def who(hostmaskOrChannel, prefix='', msg=None):
"""Returns a WHO for the hostmask or channel hostmaskOrChannel."""
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(hostmaskOrChannel) or \
isUserHostmask(hostmaskOrChannel), repr(hostmaskOrChannel)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='WHO',
args=(hostmaskOrChannel,), msg=msg)
def whois(nick, mask='', prefix='', msg=None):
"""Returns a WHOIS for nick."""
if conf.supybot.protocols.irc.strictRfc():
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='WHOIS', args=(nick, mask), msg=msg)
def names(channel=None, prefix='', msg=None):
if conf.supybot.protocols.irc.strictRfc():
assert isChannel(channel)
if msg and not prefix:
prefix = msg.prefix
if channel is not None:
return IrcMsg(prefix=prefix, command='NAMES', args=(channel,), msg=msg)
else:
return IrcMsg(prefix=prefix, command='NAMES', msg=msg)
def mode(channel, args=(), prefix='', msg=None):
if msg and not prefix:
prefix = msg.prefix
if isinstance(args, basestring):
args = (args,)
else:
args = tuple(map(str, args))
return IrcMsg(prefix=prefix, command='MODE', args=(channel,)+args, msg=msg)
def limit(channel, limit, prefix='', msg=None):
return mode(channel, ['+l', limit], prefix=prefix, msg=msg)
def unlimit(channel, limit, prefix='', msg=None):
return mode(channel, ['-l', limit], prefix=prefix, msg=msg)
def invite(nick, channel, prefix='', msg=None):
"""Returns an INVITE for nick."""
if conf.supybot.protocols.irc.strictRfc():
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='INVITE',
args=(nick, channel), msg=msg)
def password(password, prefix='', msg=None):
"""Returns a PASS command for accessing a server."""
if conf.supybot.protocols.irc.strictRfc():
assert password, 'password must not be empty.'
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='PASS', args=(password,), msg=msg)
def ison(nick, prefix='', msg=None):
if conf.supybot.protocols.irc.strictRfc():
assert isNick(nick), repr(nick)
if msg and not prefix:
prefix = msg.prefix
return IrcMsg(prefix=prefix, command='ISON', args=(nick,), msg=msg)
def error(s, msg=None):
return IrcMsg(command='ERROR', args=(s,), msg=msg)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

671
src/ircutils.py Normal file
View File

@ -0,0 +1,671 @@
###
# 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.
###
"""
Provides a great number of useful utility functions IRC. Things to muck around
with hostmasks, set bold or color on strings, IRC-case-insensitive dicts, a
nick class to handle nicks (so comparisons and hashing and whatnot work in an
IRC-case-insensitive fashion), and numerous other things.
"""
import supybot.fix as fix
import re
import time
import random
import string
import textwrap
from itertools import imap, ilen
from cStringIO import StringIO as sio
import supybot.utils as utils
import supybot.structures as structures
def debug(s, *args):
"""Prints a debug string. Most likely replaced by our logging debug."""
print '***', s % args
def isUserHostmask(s):
"""Returns whether or not the string s is a valid User hostmask."""
p1 = s.find('!')
p2 = s.find('@')
if p1 < p2-1 and p1 >= 1 and p2 >= 3 and len(s) > p2+1:
return True
else:
return False
def isServerHostmask(s):
"""s => bool
Returns True if s is a valid server hostmask."""
return not isUserHostmask(s)
def nickFromHostmask(hostmask):
"""hostmask => nick
Returns the nick from a user hostmask."""
assert isUserHostmask(hostmask)
return hostmask.split('!', 1)[0]
def userFromHostmask(hostmask):
"""hostmask => user
Returns the user from a user hostmask."""
assert isUserHostmask(hostmask)
return hostmask.split('!', 1)[1].split('@', 1)[0]
def hostFromHostmask(hostmask):
"""hostmask => host
Returns the host from a user hostmask."""
assert isUserHostmask(hostmask)
return hostmask.split('@', 1)[1]
def splitHostmask(hostmask):
"""hostmask => (nick, user, host)
Returns the nick, user, host of a user hostmask."""
assert isUserHostmask(hostmask)
nick, rest = hostmask.split('!', 1)
user, host = rest.split('@', 1)
return (nick, user, host)
def joinHostmask(nick, ident, host):
"""nick, user, host => hostmask
Joins the nick, ident, host into a user hostmask."""
assert nick and ident and host
return '%s!%s@%s' % (nick, ident, host)
_rfc1459trans = string.maketrans(string.ascii_uppercase + r'\[]~',
string.ascii_lowercase + r'|{}^')
def toLower(s, casemapping=None):
"""s => s
Returns the string s lowered according to IRC case rules."""
if casemapping is None or casemapping == 'rfc1459':
return s.translate(_rfc1459trans)
elif casemapping == 'ascii': # freenode
return s.lower()
else:
raise ValueError, 'Invalid casemapping: %s' % utils.quoted(casemapping)
def strEqual(nick1, nick2):
"""s1, s2 => bool
Returns True if nick1 == nick2 according to IRC case rules."""
assert isinstance(nick1, basestring)
assert isinstance(nick2, basestring)
return toLower(nick1) == toLower(nick2)
nickEqual = strEqual
_nickchars = r'_[]\`^{}|-'
nickRe = re.compile(r'^[A-Za-z%s][0-9A-Za-z%s]*$'
% (re.escape(_nickchars), re.escape(_nickchars)))
def isNick(s, strictRfc=True, nicklen=None):
"""s => bool
Returns True if s is a valid IRC nick."""
if strictRfc:
ret = bool(nickRe.match(s))
if ret and nicklen is not None:
ret = len(s) <= nicklen
return ret
else:
return not isChannel(s) and \
not isUserHostmask(s) and \
not ' ' in s and not '!' in s
def isChannel(s, chantypes='#&+!', channellen=50):
"""s => bool
Returns True if s is a valid IRC channel name."""
return s and \
',' not in s and \
'\x07' not in s and \
s[0] in chantypes and \
len(s) <= channellen and \
len(s.split(None, 1)) == 1
_patternCache = {}
def _hostmaskPatternEqual(pattern, hostmask):
try:
return _patternCache[pattern](hostmask) is not None
except KeyError:
# We make our own regexps, rather than use fnmatch, because fnmatch's
# case-insensitivity is not IRC's case-insensitity.
fd = sio()
for c in pattern:
if c == '*':
fd.write('.*')
elif c == '?':
fd.write('.')
elif c in '[{':
fd.write('[[{]')
elif c in '}]':
fd.write(r'[}\]]')
elif c in '|\\':
fd.write(r'[|\\]')
elif c in '^~':
fd.write('[~^]')
else:
fd.write(re.escape(c))
fd.write('$')
f = re.compile(fd.getvalue(), re.I).match
_patternCache[pattern] = f
return f(hostmask) is not None
_hostmaskPatternEqualCache = {}
def hostmaskPatternEqual(pattern, hostmask):
"""pattern, hostmask => bool
Returns True if hostmask matches the hostmask pattern pattern."""
try:
return _hostmaskPatternEqualCache[(pattern, hostmask)]
except KeyError:
b = _hostmaskPatternEqual(pattern, hostmask)
_hostmaskPatternEqualCache[(pattern, hostmask)] = b
return b
def banmask(hostmask):
"""Returns a properly generic banning hostmask for a hostmask.
>>> banmask('nick!user@host.domain.tld')
'*!*@*.domain.tld'
>>> banmask('nick!user@10.0.0.1')
'*!*@10.0.0.*'
"""
assert isUserHostmask(hostmask)
host = hostFromHostmask(hostmask)
if utils.isIP(host):
L = host.split('.')
L[-1] = '*'
return '*!*@' + '.'.join(L)
elif utils.isIPV6(host):
L = host.split(':')
L[-1] = '*'
return '*!*@' + ':'.join(L)
else:
if '.' in host:
return '*!*@*%s' % host[host.find('.'):]
else:
return '*!*@' + host
_plusRequireArguments = 'ovhblkqe'
_minusRequireArguments = 'ovhbkqe'
def separateModes(args):
"""Separates modelines into single mode change tuples. Basically, you
should give it the .args of a MODE IrcMsg.
Examples:
>>> separateModes(['+ooo', 'jemfinch', 'StoneTable', 'philmes'])
[('+o', 'jemfinch'), ('+o', 'StoneTable'), ('+o', 'philmes')]
>>> separateModes(['+o-o', 'jemfinch', 'PeterB'])
[('+o', 'jemfinch'), ('-o', 'PeterB')]
>>> separateModes(['+s-o', 'test'])
[('+s', None), ('-o', 'test')]
>>> separateModes(['+sntl', '100'])
[('+s', None), ('+n', None), ('+t', None), ('+l', 100)]
"""
if not args:
return []
modes = args[0]
assert modes[0] in '+-', 'Invalid args: %r' % args
args = list(args[1:])
ret = []
for c in modes:
if c in '+-':
last = c
else:
if last == '+':
requireArguments = _plusRequireArguments
else:
requireArguments = _minusRequireArguments
if c in requireArguments:
arg = args.pop(0)
try:
arg = int(arg)
except ValueError:
pass
ret.append((last + c, arg))
else:
ret.append((last + c, None))
return ret
def joinModes(modes):
"""[(mode, targetOrNone), ...] => args
Joins modes of the same form as returned by separateModes."""
args = []
modeChars = []
currentMode = '\x00'
for (mode, arg) in modes:
if arg is not None:
args.append(arg)
if not mode.startswith(currentMode):
currentMode = mode[0]
modeChars.append(mode[0])
modeChars.append(mode[1])
args.insert(0, ''.join(modeChars))
return args
def bold(s):
"""Returns the string s, bolded."""
return '\x02%s\x02' % s
def reverse(s):
"""Returns the string s, reverse-videoed."""
return '\x16%s\x16' % s
def underline(s):
"""Returns the string s, underlined."""
return '\x1F%s\x1F' % s
# Definition of mircColors dictionary moved below because it became an IrcDict.
def mircColor(s, fg=None, bg=None):
"""Returns s with the appropriate mIRC color codes applied."""
if fg is None and bg is None:
return s
elif bg is None:
fg = mircColors[str(fg)]
return '\x03%s%s\x03' % (fg.zfill(2), s)
elif fg is None:
bg = mircColors[str(bg)]
return '\x03,%s%s\x03' % (bg.zfill(2), s)
else:
fg = mircColors[str(fg)]
bg = mircColors[str(bg)]
# No need to zfill fg because the comma delimits.
return '\x03%s,%s%s\x03' % (fg, bg.zfill(2), s)
def canonicalColor(s, bg=False, shift=0):
"""Assigns an (fg, bg) canonical color pair to a string based on its hash
value. This means it might change between Python versions. This pair can
be used as a *parameter to mircColor. The shift parameter is how much to
right-shift the hash value initially.
"""
h = hash(s) >> shift
fg = h % 14 + 2 # The + 2 is to rule out black and white.
if bg:
bg = (h >> 4) & 3 # The 5th, 6th, and 7th least significant bits.
if fg < 8:
bg += 8
else:
bg += 2
return (fg, bg)
else:
return (fg, None)
def stripBold(s):
"""Returns the string s, with bold removed."""
return s.replace('\x02', '')
_stripColorRe = re.compile(r'\x03(?:\d{1,2},\d{1,2}|\d{1,2}|,\d{1,2}|)')
def stripColor(s):
"""Returns the string s, with color removed."""
return _stripColorRe.sub('', s)
def stripReverse(s):
"""Returns the string s, with reverse-video removed."""
return s.replace('\x16', '')
def stripUnderline(s):
"""Returns the string s, with underlining removed."""
return s.replace('\x1f', '').replace('\x1F', '')
def stripFormatting(s):
"""Returns the string s, with all formatting removed."""
# stripColor has to go first because of some strings, check the tests.
s = stripColor(s)
s = stripBold(s)
s = stripReverse(s)
s = stripUnderline(s)
return s.replace('\x0f', '').replace('\x0F', '')
class FormatContext(object):
def __init__(self):
self.reset()
def reset(self):
self.fg = None
self.bg = None
self.bold = False
self.reverse = False
self.underline = False
def start(self, s):
"""Given a string, starts all the formatters in this context."""
if self.bold:
s = '\x02' + s
if self.reverse:
s = '\x16' + s
if self.underline:
s = '\x1f' + s
if self.fg is not None or self.bg is not None:
s = mircColor(s, fg=self.fg, bg=self.bg)[:-1] # Remove \x03.
return s
def end(self, s):
"""Given a string, ends all the formatters in this context."""
if self.bold or self.reverse or \
self.fg or self.bg or self.underline:
# Should we individually end formatters?
s += '\x0f'
return s
class FormatParser(object):
def __init__(self, s):
self.fd = sio(s)
self.last = None
def getChar(self):
if self.last is not None:
c = self.last
self.last = None
return c
else:
return self.fd.read(1)
def ungetChar(self, c):
self.last = c
def parse(self):
context = FormatContext()
c = self.getChar()
while c:
if c == '\x02':
context.bold = not context.bold
elif c == '\x16':
context.reverse = not context.reverse
elif c == '\x1f':
context.underline = not context.underline
elif c == '\x0f':
context.reset()
elif c == '\x03':
self.getColor(context)
c = self.getChar()
return context
def getInt(self):
i = 0
setI = False
c = self.getChar()
while c.isdigit() and i < 100:
setI = True
i *= 10
i += int(c)
c = self.getChar()
self.ungetChar(c)
if setI:
return i
else:
return None
def getColor(self, context):
context.fg = self.getInt()
c = self.getChar()
if c == ',':
context.bg = self.getInt()
def wrap(s, length):
processed = []
chunks = textwrap.wrap(s, length)
context = None
for chunk in chunks:
if context is not None:
chunk = context.start(chunk)
context = FormatParser(chunk).parse()
processed.append(context.end(chunk))
return processed
def isValidArgument(s):
"""Returns whether s is strictly a valid argument for an IRC message."""
return '\r' not in s and '\n' not in s and '\x00' not in s
def safeArgument(s):
"""If s is unsafe for IRC, returns a safe version."""
if isinstance(s, unicode):
s = s.encode('utf-8')
elif not isinstance(s, basestring):
debug('Got a non-string in safeArgument: %s', utils.quoted(s))
s = str(s)
if isValidArgument(s):
return s
else:
return repr(s)
def replyTo(msg):
"""Returns the appropriate target to send responses to msg."""
if isChannel(msg.args[0]):
return msg.args[0]
else:
return msg.nick
def dccIP(ip):
"""Returns in IP in the proper for DCC."""
assert utils.isIP(ip), \
'argument must be a string ip in xxx.yyy.zzz.www format.'
i = 0
x = 256**3
for quad in ip.split('.'):
i += int(quad)*x
x /= 256
return i
def unDccIP(i):
"""Takes an integer DCC IP and return a normal string IP."""
assert isinstance(i, (int, long)), '%r is not an number.' % i
L = []
while len(L) < 4:
L.append(i % 256)
i /= 256
L.reverse()
return '.'.join(imap(str, L))
class IrcString(str):
"""This class does case-insensitive comparison and hashing of nicks."""
def __new__(cls, s=''):
x = super(IrcString, cls).__new__(cls, s)
x.lowered = toLower(x)
return x
def __eq__(self, s):
try:
return toLower(s) == self.lowered
except:
return False
def __ne__(self, s):
return not (self == s)
def __hash__(self):
return hash(self.lowered)
class IrcDict(utils.InsensitivePreservingDict):
"""Subclass of dict to make key comparison IRC-case insensitive."""
def key(self, s):
if s is not None:
s = toLower(s)
return s
class IrcSet(utils.NormalizingSet):
"""A sets.Set using IrcStrings instead of regular strings."""
def normalize(self, s):
return IrcString(s)
def __reduce__(self):
return (self.__class__, (list(self),))
class FloodQueue(object):
timeout = 0
def __init__(self, timeout=None, queues=None):
if timeout is not None:
self.timeout = timeout
if queues is None:
queues = IrcDict()
self.queues = queues
def __repr__(self):
return 'FloodQueue(timeout=%r, queues=%s)' % (self.timeout,
repr(self.queues))
def key(self, msg):
return msg.user + '@' + msg.host
def getTimeout(self):
if callable(self.timeout):
return self.timeout()
else:
return self.timeout
def _getQueue(self, msg, insert=True):
key = self.key(msg)
try:
return self.queues[key]
except KeyError:
if insert:
# python--
# instancemethod.__repr__ calls the instance.__repr__, which
# means that our __repr__ calls self.queues.__repr__, which
# calls structures.TimeoutQueue.__repr__, which calls
# getTimeout.__repr__, which calls our __repr__, which calls...
getTimeout = lambda : self.getTimeout()
q = structures.TimeoutQueue(getTimeout)
self.queues[key] = q
return q
else:
return None
def enqueue(self, msg, what=None):
if what is None:
what = msg
q = self._getQueue(msg)
q.enqueue(what)
def len(self, msg):
q = self._getQueue(msg, insert=False)
if q is not None:
return len(q)
else:
return 0
def has(self, msg, what=None):
q = self._getQueue(msg, insert=False)
if q is not None:
if what is None:
what = msg
for elt in q:
if elt == what:
return True
return False
mircColors = IrcDict({
'white': '0',
'black': '1',
'blue': '2',
'green': '3',
'red': '4',
'brown': '5',
'purple': '6',
'orange': '7',
'yellow': '8',
'light green': '9',
'teal': '10',
'light blue': '11',
'dark blue': '12',
'pink': '13',
'dark grey': '14',
'light grey': '15',
'dark gray': '14',
'light gray': '15',
})
# We'll map integers to their string form so mircColor is simpler.
for (k, v) in mircColors.items():
if k is not None: # Ignore empty string for None.
sv = str(v)
mircColors[sv] = sv
def standardSubstitute(irc, msg, text, env=None):
"""Do the standard set of substitutions on text, and return it"""
if isChannel(msg.args[0]):
channel = msg.args[0]
else:
channel = 'somewhere'
def randInt():
return str(random.randint(-1000, 1000))
def randDate():
t = pow(2,30)*random.random()+time.time()/4.0
return time.ctime(t)
def randNick():
if channel != 'somewhere':
L = list(irc.state.channels[channel].users)
if len(L) > 1:
n = msg.nick
while n == msg.nick:
n = random.choice(L)
return n
else:
return msg.nick
else:
return 'someone'
ctime = time.ctime()
localtime = time.localtime()
vars = IrcDict({
'who': msg.nick,
'nick': msg.nick,
'user': msg.user,
'host': msg.host,
'channel': channel,
'botnick': irc.nick,
'now': ctime, 'ctime': ctime,
'randnick': randNick, 'randomnick': randNick,
'randdate': randDate, 'randomdate': randDate,
'rand': randInt, 'randint': randInt, 'randomint': randInt,
'today': time.strftime('%d %b %Y', localtime),
'year': localtime[0],
'month': localtime[1],
'monthname': time.strftime('%b', localtime),
'date': localtime[2],
'day': time.strftime('%A', localtime),
'h': localtime[3], 'hr': localtime[3], 'hour': localtime[3],
'm': localtime[4], 'min': localtime[4], 'minute': localtime[4],
's': localtime[5], 'sec': localtime[5], 'second': localtime[5],
'tz': time.tzname[time.daylight],
})
if env is not None:
vars.update(env)
return utils.perlVariableSubstitute(vars, text)
if __name__ == '__main__':
import sys, doctest
doctest.testmod(sys.modules['__main__'])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

405
src/log.py Normal file
View File

@ -0,0 +1,405 @@
###
# 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 supybot.fix as fix
import os
import sys
import time
import types
import atexit
import logging
import operator
import textwrap
import traceback
import supybot.ansi as ansi
import supybot.conf as conf
import supybot.utils as utils
import supybot.registry as registry
import supybot.ircutils as ircutils
deadlyExceptions = [KeyboardInterrupt, SystemExit]
###
# This is for testing, of course. Mostly is just disables the firewall code
# so exceptions can propagate.
###
testing = False
VERBOSE = 1
logging.addLevelName(VERBOSE, 'VERBOSE')
class Formatter(logging.Formatter):
_fmtConf = staticmethod(lambda : conf.supybot.log.format())
def formatTime(self, record, datefmt=None):
return timestamp(record.created)
def formatException(self, (E, e, tb)):
for exn in deadlyExceptions:
if issubclass(e.__class__, exn):
raise
return logging.Formatter.formatException(self, (E, e, tb))
def format(self, record):
self._fmt = self._fmtConf()
return logging.Formatter.format(self, record)
class PluginFormatter(Formatter):
_fmtConf = staticmethod(lambda : conf.supybot.log.plugins.format())
class Logger(logging.Logger):
def exception(self, *args):
(E, e, tb) = sys.exc_info()
tbinfo = traceback.extract_tb(tb)
path = '[%s]' % '|'.join(map(operator.itemgetter(2), tbinfo))
eStrId = '%s:%s' % (E, path)
eId = hex(hash(eStrId) & 0xFFFFF)
logging.Logger.exception(self, *args)
self.error('Exception id: %s', eId)
# The traceback should be sufficient if we want it.
# self.error('Exception string: %s', eStrId)
def verbose(self, *args, **kwargs):
self.log(VERBOSE, *args, **kwargs)
class StdoutStreamHandler(logging.StreamHandler):
def disable(self):
self.setLevel(sys.maxint) # Just in case.
_logger.removeHandler(self)
logging._acquireLock()
try:
del logging._handlers[self]
finally:
logging._releaseLock()
def format(self, record):
s = logging.StreamHandler.format(self, record)
if record.levelname != 'ERROR' and conf.supybot.log.stdout.wrap():
# We check for ERROR there because otherwise, tracebacks (which are
# already wrapped by Python itself) wrap oddly.
if not isinstance(record.levelname, basestring):
print record
print record.levelname
print utils.stackTrace()
prefixLen = len(record.levelname) + 1 # ' '
s = textwrap.fill(s, width=78, subsequent_indent=' '*prefixLen)
s.rstrip('\r\n')
return s
def emit(self, record):
if conf.supybot.log.stdout() and not conf.daemonized:
try:
logging.StreamHandler.emit(self, record)
except ValueError, e: # Raised if sys.stdout is closed.
self.disable()
error('Error logging to stdout. Removing stdout handler.')
exception('Uncaught exception in StdoutStreamHandler:')
class BetterFileHandler(logging.FileHandler):
def emit(self, record):
msg = self.format(record)
if not hasattr(types, "UnicodeType"): #if no unicode support...
self.stream.write(msg)
self.stream.write(os.linesep)
else:
try:
self.stream.write(msg)
self.stream.write(os.linesep)
except UnicodeError:
self.stream.write(msg.encode("utf8"))
self.stream.write(os.linesep)
self.flush()
class ColorizedFormatter(Formatter):
# This was necessary because these variables aren't defined until later.
# The staticmethod is necessary because they get treated like methods.
_fmtConf = staticmethod(lambda : conf.supybot.log.stdout.format())
def formatException(self, (E, e, tb)):
if conf.supybot.log.stdout.colorized():
return ''.join([ansi.RED,
Formatter.formatException(self, (E, e, tb)),
ansi.RESET])
else:
return Formatter.formatException(self, (E, e, tb))
def format(self, record, *args, **kwargs):
if conf.supybot.log.stdout.colorized():
color = ''
if record.levelno == logging.CRITICAL:
color = ansi.WHITE + ansi.BOLD
elif record.levelno == logging.ERROR:
color = ansi.RED
elif record.levelno == logging.WARNING:
color = ansi.YELLOW
if color:
return ''.join([color,
Formatter.format(self, record, *args, **kwargs),
ansi.RESET])
else:
return Formatter.format(self, record, *args, **kwargs)
else:
return Formatter.format(self, record, *args, **kwargs)
# These are public.
formatter = Formatter('NEVER SEEN; IF YOU SEE THIS, FILE A BUG!')
pluginFormatter = PluginFormatter('NEVER SEEN; IF YOU SEE THIS, FILE A BUG!')
# These are not.
logging.setLoggerClass(Logger)
_logger = logging.getLogger('supybot')
class ValidLogLevel(registry.String):
"""Invalid log level."""
minimumLevel = -1
def set(self, s):
s = s.upper()
try:
level = logging._levelNames[s]
except KeyError:
try:
level = int(s)
except ValueError:
self.error()
if level < self.minimumLevel:
self.error()
self.setValue(level)
def __str__(self):
# The str() is necessary here; apparently getLevelName returns an
# integer on occasion. logging--
level = str(logging.getLevelName(self.value))
if level.startswith('Level'):
level = level.split()[-1]
return level
class LogLevel(ValidLogLevel):
"""Invalid log level. Value must be either VERBOSE, DEBUG, INFO, WARNING,
ERROR, or CRITICAL."""
def setValue(self, v):
ValidLogLevel.setValue(self, v)
_logger.setLevel(self.value) # _logger defined later.
conf.registerGlobalValue(conf.supybot.directories, 'log',
conf.Directory('logs', """Determines what directory the bot will store its
logfiles in."""))
conf.registerGroup(conf.supybot, 'log')
conf.registerGlobalValue(conf.supybot.log, 'format',
registry.String('%(levelname)s %(asctime)s %(name)s %(message)s',
"""Determines what the bot's logging format will be. The relevant
documentation on the available formattings is Python's documentation on
its logging module."""))
conf.registerGlobalValue(conf.supybot.log, 'level',
LogLevel(logging.INFO, """Determines what the minimum priority level logged
will be. Valid values are VERBOSE, DEBUG, INFO, WARNING, ERROR,
and CRITICAL, in order of increasing priority."""))
conf.registerGlobalValue(conf.supybot.log, 'statistics',
ValidLogLevel(-1, """Determines what level statistics reporting
is to be logged at. Mostly, this just includes, for instance, the time it
took to parse a message, process a command, etc. You probably don't care
about this."""))
conf.registerGlobalValue(conf.supybot.log, 'timestampFormat',
registry.String('%Y-%m-%dT%H:%M:%S', """Determines the format string for
timestamps in logfiles. Refer to the Python documentation for the time
module to see what formats are accepted. If you set this variable to the
empty string, times will be logged in a simple seconds-since-epoch
format."""))
class BooleanRequiredFalseOnWindows(registry.Boolean):
def set(self, s):
registry.Boolean.set(self, s)
if self.value and os.name == 'nt':
raise InvalidRegistryValue, 'Value cannot be true on Windows.'
conf.registerGlobalValue(conf.supybot.log, 'stdout',
registry.Boolean(True, """Determines whether the bot will log to
stdout."""))
conf.registerGlobalValue(conf.supybot.log.stdout, 'colorized',
BooleanRequiredFalseOnWindows(False, """Determines whether the bot's logs
to stdout (if enabled) will be colorized with ANSI color."""))
conf.registerGlobalValue(conf.supybot.log.stdout, 'wrap',
registry.Boolean(True, """Determines whether the bot will wrap its logs
when they're output to stdout."""))
conf.registerGlobalValue(conf.supybot.log.stdout, 'format',
registry.String('%(levelname)s %(asctime)s %(message)s',
"""Determines what the bot's logging format will be. The relevant
documentation on the available formattings is Python's documentation on
its logging module."""))
conf.registerGroup(conf.supybot.log, 'plugins')
conf.registerGlobalValue(conf.supybot.log.plugins, 'individualLogfiles',
registry.Boolean(False, """Determines whether the bot will separate plugin
logs into their own individual logfiles."""))
conf.registerGlobalValue(conf.supybot.log.plugins, 'format',
registry.String('%(levelname)s %(asctime)s %(message)s',
"""Determines what the bot's logging format will be. The relevant
documentation on the available formattings is Python's documentation on
its logging module."""))
# These just make things easier.
debug = _logger.debug
verbose = _logger.verbose
info = _logger.info
warning = _logger.warning
error = _logger.error
critical = _logger.critical
exception = _logger.exception
# These were just begging to be replaced.
registry.error = error
registry.exception = exception
def stat(*args):
level = conf.supybot.log.statistics()
_logger.log(level, *args)
setLevel = _logger.setLevel
atexit.register(logging.shutdown)
# ircutils will work without this, but it's useful.
ircutils.debug = debug
def getPluginLogger(name):
if not conf.supybot.log.plugins.individualLogfiles():
return _logger
log = logging.getLogger('supybot.plugins.%s' % name)
if not log.handlers:
filename = os.path.join(pluginLogDir, '%s.log' % name)
handler = BetterFileHandler(filename)
handler.setLevel(-1)
handler.setFormatter(pluginFormatter)
log.addHandler(handler)
if name in sys.modules:
log.info('Starting log for %s.', name)
return log
def timestamp(when=None):
if when is None:
when = time.time()
format = conf.supybot.log.timestampFormat()
t = time.localtime(when)
if format:
return time.strftime(format, t)
else:
return str(int(time.mktime(t)))
def firewall(f, errorHandler=None):
def logException(self, s=None):
if s is None:
s = 'Uncaught exception'
if hasattr(self, 'log'):
self.log.exception('%s:', s)
else:
exception('%s in %s.%s:', s, self.__class__.__name__, f.func_name)
def m(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except Exception, e:
if testing:
raise
logException(self)
if errorHandler is not None:
try:
errorHandler(self, *args, **kwargs)
except Exception, e:
logException(self, 'Uncaught exception in errorHandler')
m = utils.changeFunctionName(m, f.func_name, f.__doc__)
return m
class MetaFirewall(type):
def __new__(cls, name, bases, dict):
firewalled = {}
for base in bases:
if hasattr(base, '__firewalled__'):
firewalled.update(base.__firewalled__)
if '__firewalled__' in dict:
firewalled.update(dict['__firewalled__'])
for attr in firewalled:
if attr in dict:
try:
errorHandler = firewalled[attr]
except:
errorHandler = None
dict[attr] = firewall(dict[attr], errorHandler)
return super(MetaFirewall, cls).__new__(cls, name, bases, dict)
#return type.__new__(cls, name, bases, dict)
_logDir = conf.supybot.directories.log()
if not os.path.exists(_logDir):
os.mkdir(_logDir, 0755)
pluginLogDir = os.path.join(_logDir, 'plugins')
if not os.path.exists(pluginLogDir):
os.mkdir(pluginLogDir, 0755)
try:
miscLogFilename = os.path.join(_logDir, 'misc.log')
_handler = BetterFileHandler(miscLogFilename)
except EnvironmentError, e:
raise SystemExit, \
'Error opening miscellaneous logfile (%s). ' \
'Generally, this is because you are running Supybot in a directory ' \
'you don\'t have permissions to add files in, or you\'re running ' \
'Supybot as a different user than you normal do. The original ' \
'error was: %s' % (miscLogFilename, utils.exnToString(e))
_handler.setFormatter(formatter)
_handler.setLevel(-1)
class PluginLogFilter(logging.Filter):
def filter(self, record):
if conf.supybot.log.plugins.individualLogfiles():
if record.name.startswith('supybot.plugins'):
return False
return True
_handler.addFilter(PluginLogFilter())
_logger.addHandler(_handler)
_logger.setLevel(conf.supybot.log.level())
if not conf.daemonized:
_stdoutHandler = StdoutStreamHandler(sys.stdout)
_stdoutFormatter = ColorizedFormatter('IF YOU SEE THIS, FILE A BUG!')
_stdoutHandler.setFormatter(_stdoutFormatter)
_stdoutHandler.setLevel(-1)
_logger.addHandler(_stdoutHandler)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

138
src/privmsgs.py Normal file
View File

@ -0,0 +1,138 @@
###
# 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.
###
"""
Includes various accessories for callbacks.Privmsg based callbacks.
"""
import supybot.fix as fix
import time
import types
import threading
import supybot.conf as conf
import supybot.utils as utils
import supybot.world as world
import supybot.ircdb as ircdb
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
import supybot.structures as structures
# XXX Deprecated, will be removed in 0.90.0.
def getChannel(msg, args=(), raiseError=True):
"""Returns the channel the msg came over or the channel given in args.
If the channel was given in args, args is modified (the channel is
removed).
"""
if args and ircutils.isChannel(args[0]):
if conf.supybot.reply.requireChannelCommandsToBeSentInChannel():
if args[0] != msg.args[0]:
s = 'Channel commands must be sent in the channel to which ' \
'they apply; if this is not the behavior you desire, ' \
'ask the bot\'s administrator to change the registry ' \
'variable ' \
'supybot.reply.requireChannelCommandsToBeSentInChannel ' \
'to False.'
raise callbacks.Error, s
return args.pop(0)
elif ircutils.isChannel(msg.args[0]):
return msg.args[0]
else:
if raiseError:
raise callbacks.Error, 'Command must be sent in a channel or ' \
'include a channel in its arguments.'
else:
return None
# XXX Deprecated, will be removed in 0.90.0.
def getArgs(args, required=1, optional=0):
"""Take the required/optional arguments from args.
Always returns a list of size required + optional, filling it with however
many empty strings is necessary to fill the tuple to the right size. If
there is only one argument, a string containing that argument is returned.
If there aren't enough args even to satisfy required, raise an error and
let the caller handle sending the help message.
"""
assert not isinstance(args, str), 'args should be a list.'
assert not isinstance(args, ircmsgs.IrcMsg), 'args should be a list.'
if len(args) < required:
raise callbacks.ArgumentError
if len(args) < required + optional:
ret = list(args) + ([''] * (required + optional - len(args)))
elif len(args) >= required + optional:
ret = list(args[:required + optional - 1])
ret.append(' '.join(args[required + optional - 1:]))
if len(ret) == 1:
return ret[0]
else:
return ret
# XXX Deprecated, will be removed in 0.90.0.
def checkCapability(f, capability):
"""Makes sure a user has a certain capability before a command will run.
capability can be either a string or a callable object which will be called
in order to produce a string for ircdb.checkCapability."""
def newf(self, irc, msg, args):
cap = capability
if callable(cap):
cap = cap()
if ircdb.checkCapability(msg.prefix, cap):
f(self, irc, msg, args)
else:
self.log.info('%s attempted %s without %s.',
msg.prefix, f.func_name, cap)
irc.errorNoCapability(cap)
return utils.changeFunctionName(newf, f.func_name, f.__doc__)
class CapabilityCheckingPrivmsg(callbacks.Privmsg):
"""A small subclass of callbacks.Privmsg that checks self.capability
before allowing any command to be called.
"""
capability = '' # To satisfy PyChecker
def __init__(self, *args, **kwargs):
self.__parent = super(CapabilityCheckingPrivmsg, self)
self.__parent.__init__(*args, **kwargs)
def callCommand(self, name, irc, msg, args, *L, **kwargs):
if ircdb.checkCapability(msg.prefix, self.capability):
self.__parent.callCommand(name, irc, msg, args, *L, **kwargs)
else:
self.log.warning('%s tried to call %s without %s.',
msg.prefix, name, self.capability)
irc.errorNoCapability(self.capability)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

140
src/questions.py Normal file
View File

@ -0,0 +1,140 @@
###
# 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.
###
"""Handles interactive questions; useful for wizards and whatnot."""
import sys
import textwrap
from getpass import getpass as getPass
import supybot.ansi as ansi
import supybot.utils as utils
useBold = False
def output(s, unformatted=True, fd=sys.stdout):
if unformatted:
s = textwrap.fill(utils.normalizeWhitespace(s), width=65)
print >>fd, s
print >>fd
def expect(prompt, possibilities, recursed=False, default=None,
acceptEmpty=False, fd=sys.stdout):
"""Prompt the user with prompt, allow them to choose from possibilities.
If possibilities is empty, allow anything.
"""
prompt = utils.normalizeWhitespace(prompt)
originalPrompt = prompt
if recursed:
output('Sorry, that response was not an option.')
if useBold:
choices = '[%s%%s%s]' % (ansi.RESET, ansi.BOLD)
else:
choices = '[%s]'
if possibilities:
prompt = '%s %s' % (originalPrompt, choices % '/'.join(possibilities))
if len(prompt) > 70:
prompt = '%s %s' % (originalPrompt, choices % '/ '.join(possibilities))
if default is not None:
prompt = '%s (default: %s)' % (prompt, default)
prompt = textwrap.fill(prompt)
prompt = prompt.replace('/ ', '/')
prompt = prompt.strip() + ' '
if useBold:
print >>fd, ansi.BOLD,
s = raw_input(prompt)
if useBold:
print >>fd, ansi.RESET
s = s.strip()
print >>fd
if possibilities:
if s in possibilities:
return s
elif not s and default is not None:
return default
elif not s and acceptEmpty:
return s
else:
return expect(originalPrompt, possibilities, recursed=True,
default=default)
else:
if not s and default is not None:
return default
return s.strip()
def anything(prompt):
"""Allow anything from the user."""
return expect(prompt, [])
def something(prompt, default=None):
"""Allow anything *except* nothing from the user."""
s = expect(prompt, [], default=default)
while not s:
output('Sorry, you must enter a value.')
s = expect(prompt, [], default=default)
return s
def yn(prompt, default=None):
"""Allow only 'y' or 'n' from the user."""
if default is not None:
if default:
default = 'y'
else:
default = 'n'
s = expect(prompt, ['y', 'n'], default=default)
if s is 'y':
return True
else:
return False
def getpass(prompt='Enter password: ', secondPrompt='Re-enter password: '):
"""Prompt the user for a password."""
password = ''
secondPassword = ' ' # Note that this should be different than password.
assert prompt
if not prompt[-1].isspace():
prompt += ' '
while True:
if useBold:
sys.stdout.write(ansi.BOLD)
password = getPass(prompt)
secondPassword = getPass(secondPrompt)
if useBold:
print ansi.RESET
if password != secondPassword:
output('Passwords don\'t match.')
else:
break
return password
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

623
src/registry.py Normal file
View File

@ -0,0 +1,623 @@
###
# Copyright (c) 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 os
import sets
import time
import string
import textwrap
import supybot.fix as fix
import supybot.utils as utils
def error(s):
"""Replace me with something better from another module!"""
print '***', s
def exception(s):
"""Ditto!"""
print '***', s, 'A bad exception.'
class RegistryException(Exception):
pass
class InvalidRegistryFile(RegistryException):
pass
class InvalidRegistryName(RegistryException):
pass
class InvalidRegistryValue(RegistryException):
pass
class NonExistentRegistryEntry(RegistryException):
pass
_cache = utils.InsensitivePreservingDict()
_lastModified = 0
def open(filename, clear=False):
"""Initializes the module by loading the registry file into memory."""
global _lastModified
if clear:
_cache.clear()
_fd = file(filename)
fd = utils.nonCommentNonEmptyLines(_fd)
acc = ''
for line in fd:
line = line.rstrip('\r\n')
# XXX There should be some way to determine whether or not we're
# starting a new variable or not. As it is, if there's a backslash
# at the end of every line in a variable, it won't be read, and
# worse, the error will pass silently.
if line.endswith('\\'):
acc += line[:-1]
continue
else:
acc += line
try:
(key, value) = re.split(r'(?<!\\):', acc, 1)
key = key.strip()
value = value.strip()
acc = ''
except ValueError:
raise InvalidRegistryFile, 'Error unpacking line %r' % acc
_cache[key] = value
_lastModified = time.time()
_fd.close()
def close(registry, filename, private=True):
first = True
fd = utils.transactionalFile(filename)
for (name, value) in registry.getValues(getChildren=True):
help = value.help()
if help:
lines = textwrap.wrap(value._help)
for (i, line) in enumerate(lines):
lines[i] = '# %s\n' % line
lines.insert(0, '###\n')
if first:
first = False
else:
lines.insert(0, '\n')
if hasattr(value, 'value'):
if value._showDefault:
lines.append('#\n')
try:
x = value.__class__(value._default, value._help)
except Exception, e:
exception('Exception instantiating default for %s:',
value._name)
try:
lines.append('# Default value: %s\n' % x)
except Exception, e:
exception('Exception printing default value of %s:',
value._name)
lines.append('###\n')
fd.writelines(lines)
if hasattr(value, 'value'): # This lets us print help for non-valued.
try:
if private or not value._private:
s = value.serialize()
else:
s = 'CENSORED'
fd.write('%s: %s\n' % (name, s))
except Exception, e:
exception('Exception printing value:')
fd.close()
def isValidRegistryName(name):
# Now we can have . and : in names. I'm still gonna call shenanigans on
# anyone who tries to have spaces (though technically I can't see any
# reason why it wouldn't work). We also reserve all names starting with
# underscores for internal use.
return len(name.split()) == 1 and not name.startswith('_')
def escape(name):
name = name.replace('\\', '\\\\')
name = name.replace(':', '\\:')
name = name.replace('.', '\\.')
return name
def unescape(name):
name = name.replace('\\.', '.')
name = name.replace('\\:', ':')
name = name.replace('\\\\', '\\')
return name
_splitRe = re.compile(r'(?<!\\)\.')
def split(name):
return map(unescape, _splitRe.split(name))
def join(names):
return '.'.join(map(escape, names))
class Group(object):
"""A group; it doesn't hold a value unless handled by a subclass."""
def __init__(self, help='', supplyDefault=False,
orderAlphabetically=False, private=False):
self._help = help
self._name = 'unset'
self._added = []
self._children = utils.InsensitivePreservingDict()
self._lastModified = 0
self._private = private
self._supplyDefault = supplyDefault
self._orderAlphabetically = orderAlphabetically
OriginalClass = self.__class__
class X(OriginalClass):
"""This class exists to differentiate those values that have
been changed from their default from those that haven't."""
def set(self, *args):
self.__class__ = OriginalClass
self.set(*args)
def setValue(self, *args):
self.__class__ = OriginalClass
self.setValue(*args)
self.X = X
def __call__(self):
raise ValueError, 'Groups have no value.'
def __nonExistentEntry(self, attr):
s = '%s is not a valid entry in %s' % (attr, self._name)
raise NonExistentRegistryEntry, s
def __makeChild(self, attr, s):
v = self.__class__(self._default, self._help)
v.set(s)
v.__class__ = self.X
v._supplyDefault = False
v._help = '' # Clear this so it doesn't print a bazillion times.
self.register(attr, v)
return v
def __getattr__(self, attr):
if attr in self._children:
return self._children[attr]
elif self._supplyDefault:
return self.__makeChild(attr, str(self))
else:
self.__nonExistentEntry(attr)
def help(self):
return self._help
def get(self, attr):
# Not getattr(self, attr) because some nodes might have groups that
# are named the same as their methods.
return self.__getattr__(attr)
def setName(self, name):
#print '***', name
self._name = name
if name in _cache and self._lastModified < _lastModified:
#print '***>', _cache[name]
self.set(_cache[name])
if self._supplyDefault:
for (k, v) in _cache.iteritems():
if k.startswith(self._name):
rest = k[len(self._name)+1:] # +1 is for .
parts = split(rest)
if len(parts) == 1 and parts[0] == name:
try:
self.__makeChild(group, v)
except InvalidRegistryValue:
# It's probably supposed to be registered later.
pass
def register(self, name, node=None):
if not isValidRegistryName(name):
raise InvalidRegistryName, name
if node is None:
node = Group()
# We tried in any number of horrible ways to make it so that
# re-registering something would work. It doesn't, plain and simple.
# For the longest time, we had an "Is this right?" comment here, but
# from experience, we now know that it most definitely *is* right.
if name not in self._children:
self._children[name] = node
self._added.append(name)
names = split(self._name)
names.append(name)
fullname = join(names)
node.setName(fullname)
else:
# We do this so the return value from here is at least useful;
# otherwise, we're just returning a useless, unattached node
# that's simply a waste of space.
node = self._children[name]
return node
def unregister(self, name):
try:
node = self._children[name]
del self._children[name]
# We do this because we need to remove case-insensitively.
name = name.lower()
for elt in reversed(self._added):
if elt.lower() == name:
self._added.remove(elt)
if node._name in _cache:
del _cache[node._name]
return node
except KeyError:
self.__nonExistentEntry(name)
def rename(self, old, new):
node = self.unregister(old)
self.register(new, node)
def getValues(self, getChildren=False, fullNames=True):
L = []
if self._orderAlphabetically:
self._added.sort()
for name in self._added:
node = self._children[name]
if hasattr(node, 'value') or hasattr(node, 'help'):
if node.__class__ is not self.X:
L.append((node._name, node))
if getChildren:
L.extend(node.getValues(getChildren, fullNames))
if not fullNames:
L = [(split(s)[-1], node) for (s, node) in L]
return L
class Value(Group):
"""Invalid registry value. If you're getting this message, report it,
because we forgot to put a proper help string here."""
def __init__(self, default, help, setDefault=True,
showDefault=True, **kwargs):
self.__parent = super(Value, self)
self.__parent.__init__(help, **kwargs)
self._default = default
self._showDefault = showDefault
self._help = utils.normalizeWhitespace(help.strip())
if setDefault:
self.setValue(default)
def error(self):
if self.__doc__:
s = self.__doc__
else:
s = """%s has no docstring. If you're getting this message,
report it, because we forgot to put a proper help string here."""
e = InvalidRegistryValue(utils.normalizeWhitespace(s % self._name))
e.value = self
raise e
def setName(self, *args):
if self._name == 'unset':
self._lastModified = 0
self.__parent.setName(*args)
self._lastModified = time.time()
def set(self, s):
"""Override this with a function to convert a string to whatever type
you want, and call self.setValue to set the value."""
self.setValue(s)
def setValue(self, v):
"""Check conditions on the actual value type here. I.e., if you're a
IntegerLessThanOneHundred (all your values must be integers less than
100) convert to an integer in set() and check that the integer is less
than 100 in this method. You *must* call this parent method in your
own setValue."""
self._lastModified = time.time()
self.value = v
if self._supplyDefault:
for (name, v) in self._children.items():
if v.__class__ is self.X:
self.unregister(name)
def __str__(self):
return repr(self())
def serialize(self):
return str(self)
# We tried many, *many* different syntactic methods here, and this one was
# simply the best -- not very intrusive, easily overridden by subclasses,
# etc.
def __call__(self):
if _lastModified > self._lastModified:
if self._name in _cache:
self.set(_cache[self._name])
return self.value
class Boolean(Value):
"""Value must be either True or False (or On or Off)."""
def set(self, s):
try:
v = utils.toBool(s)
except ValueError:
if s.strip().lower() == 'toggle':
v = not self.value
else:
self.error()
self.setValue(v)
def setValue(self, v):
super(Boolean, self).setValue(bool(v))
class Integer(Value):
"""Value must be an integer."""
def set(self, s):
try:
self.setValue(int(s))
except ValueError:
self.error()
class NonNegativeInteger(Integer):
"""Value must be a non-negative integer."""
def setValue(self, v):
if v < 0:
self.error()
super(NonNegativeInteger, self).setValue(v)
class PositiveInteger(NonNegativeInteger):
"""Value must be positive (non-zero) integer."""
def setValue(self, v):
if not v:
self.error()
super(PositiveInteger, self).setValue(v)
class Float(Value):
"""Value must be a floating-point number."""
def set(self, s):
try:
self.setValue(float(s))
except ValueError:
self.error()
def setValue(self, v):
try:
super(Float, self).setValue(float(v))
except ValueError:
self.error()
class PositiveFloat(Float):
"""Value must be a floating-point number greater than zero."""
def setValue(self, v):
if v <= 0:
self.error()
else:
super(PositiveFloat, self).setValue(v)
class Probability(Float):
"""Value must be a floating point number in the range [0, 1]."""
def __init__(self, *args, **kwargs):
self.__parent = super(Probability, self)
self.__parent.__init__(*args, **kwargs)
def setValue(self, v):
if 0 <= v <= 1:
self.__parent.setValue(v)
else:
self.error()
class String(Value):
"""Value is not a valid Python string."""
def set(self, s):
if not s:
s = '""'
elif s[0] != s[-1] or s[0] not in '\'"':
s = repr(s)
try:
v = utils.safeEval(s)
if not isinstance(v, basestring):
raise ValueError
self.setValue(v)
except ValueError: # This catches utils.safeEval(s) errors too.
self.error()
_printable = string.printable[:-4]
def _needsQuoting(self, s):
return s.translate(string.ascii, self._printable) and s.strip() != s
def __str__(self):
s = self.value
if self._needsQuoting(s):
s = repr(s)
return s
class OnlySomeStrings(String):
validStrings = ()
def __init__(self, *args, **kwargs):
assert self.validStrings, 'There must be some valid strings. ' \
'This is a bug.'
self.__parent = super(OnlySomeStrings, self)
self.__parent.__init__(*args, **kwargs)
self.__doc__ = 'Valid values include %s.' % \
utils.commaAndify(map(repr, self.validStrings))
def help(self):
strings = [s for s in self.validStrings if s]
return '%s Valid strings: %s.' % \
(self._help, utils.commaAndify(strings))
def normalize(self, s):
lowered = s.lower()
L = list(map(str.lower, self.validStrings))
try:
i = L.index(lowered)
except ValueError:
return s # This is handled in setValue.
return self.validStrings[i]
def setValue(self, s):
s = self.normalize(s)
if s in self.validStrings:
self.__parent.setValue(s)
else:
self.error()
class NormalizedString(String):
def __init__(self, default, *args, **kwargs):
default = self.normalize(default)
self.__parent = super(NormalizedString, self)
self.__parent.__init__(default, *args, **kwargs)
self._showDefault = False
def normalize(self, s):
return utils.normalizeWhitespace(s.strip())
def set(self, s):
s = self.normalize(s)
self.__parent.set(s)
def setValue(self, s):
s = self.normalize(s)
self.__parent.setValue(s)
def serialize(self):
s = str(self)
prefixLen = len(self._name) + 2
lines = textwrap.wrap(s, width=76-prefixLen)
last = len(lines)-1
for (i, line) in enumerate(lines):
if i != 0:
line = ' '*prefixLen + line
if i != last:
line += '\\'
lines[i] = line
ret = os.linesep.join(lines)
return ret
class StringSurroundedBySpaces(String):
def setValue(self, v):
if v.lstrip() == v:
v= ' ' + v
if v.rstrip() == v:
v += ' '
super(StringSurroundedBySpaces, self).setValue(v)
class StringWithSpaceOnRight(String):
def setValue(self, v):
if v.rstrip() == v:
v += ' '
super(StringWithSpaceOnRight, self).setValue(v)
class Regexp(Value):
"""Value must be a valid regular expression."""
def __init__(self, *args, **kwargs):
kwargs['setDefault'] = False
self.sr = ''
self.value = None
self.__parent = super(Regexp, self)
self.__parent.__init__(*args, **kwargs)
def error(self, e):
self.__parent.error('Value must be a regexp of the form %s' % e)
def set(self, s):
try:
if s:
self.setValue(utils.perlReToPythonRe(s), sr=s)
else:
self.setValue(None)
except ValueError, e:
self.error(e)
def setValue(self, v, sr=None):
parent = super(Regexp, self)
if v is None:
self.sr = ''
parent.setValue(None)
elif sr is not None:
self.sr = sr
parent.setValue(v)
else:
raise InvalidRegistryValue, \
'Can\'t setValue a regexp, there would be an inconsistency '\
'between the regexp and the recorded string value.'
def __str__(self):
self() # Gotta update if we've been reloaded.
return self.sr
class SeparatedListOf(Value):
List = list
Value = Value
sorted = False
def splitter(self, s):
"""Override this with a function that takes a string and returns a list
of strings."""
raise NotImplementedError
def joiner(self, L):
"""Override this to join the internal list for output."""
raise NotImplementedError
def set(self, s):
L = self.splitter(s)
for (i, s) in enumerate(L):
v = self.Value(s, '')
L[i] = v()
self.setValue(L)
def setValue(self, v):
super(SeparatedListOf, self).setValue(self.List(v))
def __str__(self):
values = self()
if self.sorted:
values = sorted(values)
if values:
return self.joiner(values)
else:
# We must return *something* here, otherwise down along the road we
# can run into issues showing users the value if they've disabled
# nick prefixes in any of the numerous ways possible. Since the
# config parser doesn't care about this space, we'll use it :)
return ' '
class SpaceSeparatedListOf(SeparatedListOf):
def splitter(self, s):
return s.split()
joiner = ' '.join
class SpaceSeparatedListOfStrings(SpaceSeparatedListOf):
Value = String
class SpaceSeparatedSetOfStrings(SpaceSeparatedListOfStrings):
List = sets.Set
class CommaSeparatedListOfStrings(SeparatedListOf):
Value = String
def splitter(self, s):
return re.split(r'\s*,\s*', s)
joiner = ', '.join
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

150
src/schedule.py Normal file
View File

@ -0,0 +1,150 @@
###
# 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.
###
"""
Schedule plugin with a subclass of drivers.IrcDriver in order to be run as a
Supybot driver.
"""
import supybot.fix as fix
import time
import heapq
import supybot.log as log
import supybot.world as world
import supybot.drivers as drivers
class mytuple(tuple):
def __cmp__(self, other):
return cmp(self[0], other[0])
def __le__(self, other):
return self[0] <= other[0]
def __lt__(self, other):
return self[0] < other[0]
def __gt__(self, other):
return self[0] > other[0]
def __ge__(self, other):
return self[0] >= other[0]
class Schedule(drivers.IrcDriver):
"""An IrcDriver to handling scheduling of events.
Events, in this case, are functions accepting no arguments.
"""
def __init__(self):
drivers.IrcDriver.__init__(self)
self.schedule = []
self.events = {}
self.counter = 0
def reset(self):
self.events.clear()
self.schedule[:] = []
# We don't reset the counter here because if someone has held an id of
# one of the nuked events, we don't want him removing new events with
# his old id.
def name(self):
return 'Schedule'
def addEvent(self, f, t, name=None):
"""Schedules an event f to run at time t.
name must be hashable and not an int.
"""
if name is None:
name = self.counter
self.counter += 1
assert name not in self.events
self.events[name] = f
heapq.heappush(self.schedule, mytuple((t, name)))
return name
def removeEvent(self, name):
"""Removes the event with the given name from the schedule."""
f = self.events.pop(name)
self.schedule = [(t, n) for (t, n) in self.schedule if n != name]
# We must heapify here because the heap property may not be preserved
# by the above list comprehension. We could, conceivably, just mark
# the elements of the heap as removed and ignore them when we heappop,
# but that would only save a constant factor (we're already linear for
# the listcomp) so I'm not worried about it right now.
heapq.heapify(self.schedule)
return f
def rescheduleEvent(self, name, t):
f = self.removeEvent(name)
self.addEvent(f, t, name=name)
def addPeriodicEvent(self, f, t, name=None, now=True):
"""Adds a periodic event that is called every t seconds."""
def wrapper():
try:
f()
finally:
# Even if it raises an exception, let's schedule it.
return self.addEvent(wrapper, time.time() + t, name)
if now:
return wrapper()
else:
return self.addEvent(wrapper, time.time() + t, name)
removePeriodicEvent = removeEvent
def run(self):
if len(drivers._drivers) == 1 and not world.testing:
log.error('Schedule is the only remaining driver, '
'why do we continue to live?')
time.sleep(1) # We're the only driver; let's pause to think.
while self.schedule and self.schedule[0][0] < time.time():
(t, name) = heapq.heappop(self.schedule)
f = self.events[name]
del self.events[name]
try:
f()
except Exception, e:
log.exception('Uncaught exception in scheduled function:')
try:
ignore(schedule)
except NameError:
schedule = Schedule()
addEvent = schedule.addEvent
removeEvent = schedule.removeEvent
rescheduleEvent = schedule.rescheduleEvent
addPeriodicEvent = schedule.addPeriodicEvent
removePeriodicEvent = removeEvent
run = schedule.run
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

419
src/structures.py Normal file
View File

@ -0,0 +1,419 @@
###
# 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.
###
"""
Data structures for Python.
"""
import supybot.fix as fix
import time
import types
from itertools import imap, ilen
class RingBuffer(object):
"""Class to represent a fixed-size ring buffer."""
__slots__ = ('L', 'i', 'full', 'maxSize')
def __init__(self, maxSize, seq=()):
if maxSize <= 0:
raise ValueError, 'maxSize must be > 0.'
self.maxSize = maxSize
self.reset()
for elt in seq:
self.append(elt)
def reset(self):
self.full = False
self.L = []
self.i = 0
def resize(self, i):
if self.full:
L = list(self)
self.reset()
self.L = L
self.maxSize = i
def __len__(self):
return len(self.L)
def __eq__(self, other):
if self.__class__ == other.__class__ and \
self.maxSize == other.maxSize and len(self) == len(other):
iterator = iter(other)
for elt in self:
otherelt = iterator.next()
if not elt == otherelt:
return False
return True
return False
def __nonzero__(self):
return len(self) > 0
def __contains__(self, elt):
return elt in self.L
def append(self, elt):
if self.full:
self.L[self.i] = elt
self.i += 1
self.i %= len(self.L)
elif len(self) == self.maxSize:
self.full = True
self.append(elt)
else:
self.L.append(elt)
def extend(self, seq):
for elt in seq:
self.append(elt)
def __getitem__(self, idx):
if self.full:
oidx = idx
if type(oidx) == types.SliceType:
L = []
for i in xrange(*slice.indices(oidx, len(self))):
L.append(self[i])
return L
else:
(m, idx) = divmod(oidx, len(self.L))
if m and m != -1:
raise IndexError, oidx
idx = (idx + self.i) % len(self.L)
return self.L[idx]
else:
if type(idx) == types.SliceType:
L = []
for i in xrange(*slice.indices(idx, len(self))):
L.append(self[i])
return L
else:
return self.L[idx]
def __setitem__(self, idx, elt):
if self.full:
oidx = idx
if type(oidx) == types.SliceType:
range = xrange(*slice.indices(oidx, len(self)))
if len(range) != len(elt):
raise ValueError, 'seq must be the same length as slice.'
else:
for (i, x) in zip(range, elt):
self[i] = x
else:
(m, idx) = divmod(oidx, len(self.L))
if m and m != -1:
raise IndexError, oidx
idx = (idx + self.i) % len(self.L)
self.L[idx] = elt
else:
if type(idx) == types.SliceType:
range = xrange(*slice.indices(idx, len(self)))
if len(range) != len(elt):
raise ValueError, 'seq must be the same length as slice.'
else:
for (i, x) in zip(range, elt):
self[i] = x
else:
self.L[idx] = elt
def __repr__(self):
return 'RingBuffer(%r, %r)' % (self.maxSize, list(self))
def __getstate__(self):
return (self.maxSize, self.full, self.i, self.L)
def __setstate__(self, (maxSize, full, i, L)):
self.maxSize = maxSize
self.full = full
self.i = i
self.L = L
class queue(object):
"""Queue class for handling large queues. Queues smaller than 1,000 or so
elements are probably better served by the smallqueue class.
"""
__slots__ = ('front', 'back')
def __init__(self, seq=()):
self.back = []
self.front = []
for elt in seq:
self.enqueue(elt)
def reset(self):
self.back[:] = []
self.front[:] = []
def enqueue(self, elt):
self.back.append(elt)
def dequeue(self):
try:
return self.front.pop()
except IndexError:
self.back.reverse()
self.front = self.back
self.back = []
return self.front.pop()
def peek(self):
if self.front:
return self.front[-1]
else:
return self.back[0]
def __len__(self):
return len(self.front) + len(self.back)
def __nonzero__(self):
return bool(self.back or self.front)
def __contains__(self, elt):
return elt in self.front or elt in self.back
def __iter__(self):
for elt in reversed(self.front):
yield elt
for elt in self.back:
yield elt
def __eq__(self, other):
if len(self) == len(other):
otheriter = iter(other)
for elt in self:
otherelt = otheriter.next()
if not (elt == otherelt):
return False
return True
else:
return False
def __repr__(self):
return 'queue([%s])' % ', '.join(imap(repr, self))
def __getitem__(self, oidx):
if len(self) == 0:
raise IndexError, 'queue index out of range'
if type(oidx) == types.SliceType:
L = []
for i in xrange(*slice.indices(oidx, len(self))):
L.append(self[i])
return L
else:
(m, idx) = divmod(oidx, len(self))
if m and m != -1:
raise IndexError, oidx
if len(self.front) > idx:
return self.front[-(idx+1)]
else:
return self.back[(idx-len(self.front))]
def __setitem__(self, oidx, value):
if len(self) == 0:
raise IndexError, 'queue index out of range'
if type(oidx) == types.SliceType:
range = xrange(*slice.indices(oidx, len(self)))
if len(range) != len(value):
raise ValueError, 'seq must be the same length as slice.'
else:
for i in range:
(m, idx) = divmod(oidx, len(self))
if m and m != -1:
raise IndexError, oidx
for (i, x) in zip(range, value):
self[i] = x
else:
(m, idx) = divmod(oidx, len(self))
if m and m != -1:
raise IndexError, oidx
if len(self.front) > idx:
self.front[-(idx+1)] = value
else:
self.back[idx-len(self.front)] = value
def __delitem__(self, oidx):
if type(oidx) == types.SliceType:
range = xrange(*slice.indices(oidx, len(self)))
for i in range:
del self[i]
else:
(m, idx) = divmod(oidx, len(self))
if m and m != -1:
raise IndexError, oidx
if len(self.front) > idx:
del self.front[-(idx+1)]
else:
del self.back[idx-len(self.front)]
def __getstate__(self):
return (list(self),)
def __setstate__(self, (L,)):
L.reverse()
self.front = L
self.back = []
class smallqueue(list):
__slots__ = ()
def enqueue(self, elt):
self.append(elt)
def dequeue(self):
return self.pop(0)
def peek(self):
return self[0]
def __repr__(self):
return 'smallqueue([%s])' % ', '.join(imap(repr, self))
def reset(self):
self[:] = []
class TimeoutQueue(object):
def __init__(self, timeout, queue=None):
if queue is None:
queue = smallqueue()
self.queue = queue
self.timeout = timeout
def __repr__(self):
return '%s(timeout=%r, queue=%r)' % (self.__class__.__name__,
self.timeout, self.queue)
def _getTimeout(self):
if callable(self.timeout):
return self.timeout()
else:
return self.timeout
def _clearOldElements(self):
now = time.time()
while now - self.queue.peek()[0] > self._getTimeout():
self.queue.dequeue()
def setTimeout(self, i):
self.timeout = i
def enqueue(self, elt, at=None):
if at is None:
at = time.time()
self.queue.enqueue((at, elt))
def dequeue(self):
self._clearOldElements()
return self.queue.dequeue()[1]
def __iter__(self):
# We could _clearOldElements here, but what happens if someone stores
# the resulting generator and elements that should've timed out are
# yielded? Hmm? What happens then, smarty-pants?
for (t, elt) in self.queue:
if time.time() - t < self._getTimeout():
yield elt
def __len__(self):
return ilen(self)
class MaxLengthQueue(queue):
__slots__ = ('length',)
def __init__(self, length, seq=()):
self.length = length
queue.__init__(self, seq)
def __getstate__(self):
return (self.length, queue.__getstate__(self))
def __setstate__(self, (length, q)):
self.length = length
queue.__setstate__(self, q)
def enqueue(self, elt):
queue.enqueue(self, elt)
if len(self) > self.length:
self.dequeue()
class TwoWayDictionary(dict):
__slots__ = ()
def __init__(self, seq=(), **kwargs):
if hasattr(seq, 'iteritems'):
seq = seq.iteritems()
elif hasattr(seq, 'items'):
seq = seq.items()
for (key, value) in seq:
self[key] = value
self[value] = key
for (key, value) in kwargs.iteritems():
self[key] = value
self[value] = key
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
dict.__setitem__(self, value, key)
def __delitem__(self, key):
value = self[key]
dict.__delitem__(self, key)
dict.__delitem__(self, value)
class MultiSet(object):
def __init__(self, seq=()):
self.d = {}
for elt in seq:
self.add(elt)
def add(self, elt):
try:
self.d[elt] += 1
except KeyError:
self.d[elt] = 1
def remove(self, elt):
self.d[elt] -= 1
if not self.d[elt]:
del self.d[elt]
def __getitem__(self, elt):
return self.d[elt]
def __contains__(self, elt):
return elt in self.d
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

77
src/unpreserve.py Normal file
View File

@ -0,0 +1,77 @@
###
# Copyright (c) 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.
###
class Reader(object):
def __init__(self, Creator, *args, **kwargs):
self.Creator = Creator
self.args = args
self.kwargs = kwargs
self.creator = None
self.modifiedCreator = False
self.indent = None
def normalizeCommand(self, s):
return s.lower()
def readFile(self, filename):
self.read(file(filename))
def read(self, fd):
lineno = 0
for line in fd:
lineno += 1
if not line.strip():
continue
line = line.rstrip('\r\n')
line = line.expandtabs()
s = line.lstrip(' ')
indent = len(line) - len(s)
if indent != self.indent:
# New indentation level.
if self.creator is not None:
self.creator.finish()
self.creator = self.Creator(*self.args, **self.kwargs)
self.modifiedCreator = False
self.indent = indent
(command, rest) = s.split(None, 1)
command = self.normalizeCommand(command)
self.modifiedCreator = True
if hasattr(self.creator, command):
command = getattr(self.creator, command)
command(rest, lineno)
else:
self.creator.badCommand(command, rest, lineno)
if self.modifiedCreator:
self.creator.finish()
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

875
src/utils.py Normal file
View File

@ -0,0 +1,875 @@
###
# 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.
###
"""
Simple utility functions.
"""
import supybot.fix as fix
import os
import re
import sys
import md5
import new
import sha
import sets
import time
import types
import random
import shutil
import socket
import string
import sgmllib
import compiler
import textwrap
import UserDict
import itertools
import traceback
import htmlentitydefs
from itertools import imap, ifilter
from supybot.structures import TwoWayDictionary
curry = new.instancemethod
def normalizeWhitespace(s):
"""Normalizes the whitespace in a string; \s+ becomes one space."""
return ' '.join(s.split())
class HtmlToText(sgmllib.SGMLParser):
"""Taken from some eff-bot code on c.l.p."""
entitydefs = htmlentitydefs.entitydefs.copy()
entitydefs['nbsp'] = ' '
def __init__(self, tagReplace=' '):
self.data = []
self.tagReplace = tagReplace
sgmllib.SGMLParser.__init__(self)
def unknown_starttag(self, tag, attr):
self.data.append(self.tagReplace)
def unknown_endtag(self, tag):
self.data.append(self.tagReplace)
def handle_data(self, data):
self.data.append(data)
def getText(self):
text = ''.join(self.data).strip()
return normalizeWhitespace(text)
def htmlToText(s, tagReplace=' '):
"""Turns HTML into text. tagReplace is a string to replace HTML tags with.
"""
x = HtmlToText(tagReplace)
x.feed(s)
return x.getText()
def eachSubstring(s):
"""Returns every substring starting at the first index until the last."""
for i in xrange(1, len(s)+1):
yield s[:i]
def abbrev(strings, d=None):
"""Returns a dictionary mapping unambiguous abbreviations to full forms."""
if len(strings) != len(set(strings)):
raise ValueError, \
'strings given to utils.abbrev have duplicates: %r' % strings
if d is None:
d = {}
for s in strings:
for abbreviation in eachSubstring(s):
if abbreviation not in d:
d[abbreviation] = s
else:
if abbreviation not in strings:
d[abbreviation] = None
removals = []
for key in d:
if d[key] is None:
removals.append(key)
for key in removals:
del d[key]
return d
def timeElapsed(elapsed, short=False, leadingZeroes=False, years=True,
weeks=True, days=True, hours=True, minutes=True, seconds=True):
"""Given <elapsed> seconds, returns a string with an English description of
how much time as passed. leadingZeroes determines whether 0 days, 0 hours,
etc. will be printed; the others determine what larger time periods should
be used.
"""
ret = []
def format(s, i):
if i or leadingZeroes or ret:
if short:
ret.append('%s%s' % (i, s[0]))
else:
ret.append(nItems(s, i))
elapsed = int(elapsed)
assert years or weeks or days or \
hours or minutes or seconds, 'One flag must be True'
if years:
(yrs, elapsed) = (elapsed // 31536000, elapsed % 31536000)
format('year', yrs)
if weeks:
(wks, elapsed) = (elapsed // 604800, elapsed % 604800)
format('week', wks)
if days:
(ds, elapsed) = (elapsed // 86400, elapsed % 86400)
format('day', ds)
if hours:
(hrs, elapsed) = (elapsed // 3600, elapsed % 3600)
format('hour', hrs)
if minutes or seconds:
(mins, secs) = (elapsed // 60, elapsed % 60)
if leadingZeroes or mins:
format('minute', mins)
if seconds:
leadingZeroes = True
format('second', secs)
if not ret:
raise ValueError, 'Time difference not great enough to be noted.'
if short:
return ' '.join(ret)
else:
return commaAndify(ret)
def distance(s, t):
"""Returns the levenshtein edit distance between two strings."""
n = len(s)
m = len(t)
if n == 0:
return m
elif m == 0:
return n
d = []
for i in range(n+1):
d.append([])
for j in range(m+1):
d[i].append(0)
d[0][j] = j
d[i][0] = i
for i in range(1, n+1):
cs = s[i-1]
for j in range(1, m+1):
ct = t[j-1]
cost = int(cs != ct)
d[i][j] = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+cost)
return d[n][m]
_soundextrans = string.maketrans(string.ascii_uppercase,
'01230120022455012623010202')
_notUpper = string.ascii.translate(string.ascii, string.ascii_uppercase)
def soundex(s, length=4):
"""Returns the soundex hash of a given string."""
s = s.upper() # Make everything uppercase.
s = s.translate(string.ascii, _notUpper) # Delete non-letters.
if not s:
raise ValueError, 'Invalid string for soundex: %s'
firstChar = s[0] # Save the first character.
s = s.translate(_soundextrans) # Convert to soundex numbers.
s = s.lstrip(s[0]) # Remove all repeated first characters.
L = [firstChar]
for c in s:
if c != L[-1]:
L.append(c)
L = [c for c in L if c != '0'] + (['0']*(length-1))
s = ''.join(L)
return length and s[:length] or s.rstrip('0')
def dqrepr(s):
"""Returns a repr() of s guaranteed to be in double quotes."""
# The wankers-that-be decided not to use double-quotes anymore in 2.3.
# return '"' + repr("'\x00" + s)[6:]
return '"%s"' % s.encode('string_escape').replace('"', '\\"')
def quoted(s):
"""Returns a quoted s."""
return '"%s"' % s
def _getSep(s):
if len(s) < 2:
raise ValueError, 'string given to _getSep is too short: %r' % s
if s.startswith('m') or s.startswith('s'):
separator = s[1]
else:
separator = s[0]
if separator.isalnum() or separator in '{}[]()<>':
raise ValueError, \
'Invalid separator: separator must not be alphanumeric or in ' \
'"{}[]()<>"'
return separator
def _getSplitterRe(s):
separator = _getSep(s)
return re.compile(r'(?<!\\)%s' % re.escape(separator))
def perlReToPythonRe(s):
"""Converts a string representation of a Perl regular expression (i.e.,
m/^foo$/i or /foo|bar/) to a Python regular expression.
"""
sep = _getSep(s)
splitter = _getSplitterRe(s)
try:
(kind, regexp, flags) = splitter.split(s)
except ValueError: # Unpack list of wrong size.
raise ValueError, 'Must be of the form m/.../ or /.../'
regexp = regexp.replace('\\'+sep, sep)
if kind not in ('', 'm'):
raise ValueError, 'Invalid kind: must be in ("", "m")'
flag = 0
try:
for c in flags.upper():
flag |= getattr(re, c)
except AttributeError:
raise ValueError, 'Invalid flag: %s' % c
try:
return re.compile(regexp, flag)
except re.error, e:
raise ValueError, str(e)
def perlReToReplacer(s):
"""Converts a string representation of a Perl regular expression (i.e.,
s/foo/bar/g or s/foo/bar/i) to a Python function doing the equivalent
replacement.
"""
sep = _getSep(s)
splitter = _getSplitterRe(s)
try:
(kind, regexp, replace, flags) = splitter.split(s)
except ValueError: # Unpack list of wrong size.
raise ValueError, 'Must be of the form s/.../.../'
regexp = regexp.replace('\x08', r'\b')
replace = replace.replace('\\'+sep, sep)
for i in xrange(10):
replace = replace.replace(chr(i), r'\%s' % i)
if kind != 's':
raise ValueError, 'Invalid kind: must be "s"'
g = False
if 'g' in flags:
g = True
flags = filter('g'.__ne__, flags)
r = perlReToPythonRe('/'.join(('', regexp, flags)))
if g:
return curry(r.sub, replace)
else:
return lambda s: r.sub(replace, s, 1)
_perlVarSubstituteRe = re.compile(r'\$\{([^}]+)\}|\$([a-zA-Z][a-zA-Z0-9]*)')
def perlVariableSubstitute(vars, text):
def replacer(m):
(braced, unbraced) = m.groups()
var = braced or unbraced
try:
x = vars[var]
if callable(x):
return x()
else:
return str(x)
except KeyError:
if braced:
return '${%s}' % braced
else:
return '$' + unbraced
return _perlVarSubstituteRe.sub(replacer, text)
def findBinaryInPath(s):
"""Return full path of a binary if it's in PATH, otherwise return None."""
cmdLine = None
for dir in os.getenv('PATH').split(':'):
filename = os.path.join(dir, s)
if os.path.exists(filename):
cmdLine = filename
break
return cmdLine
def commaAndify(seq, comma=',', And='and'):
"""Given a a sequence, returns an English clause for that sequence.
I.e., given [1, 2, 3], returns '1, 2, and 3'
"""
L = list(seq)
if len(L) == 0:
return ''
elif len(L) == 1:
return ''.join(L) # We need this because it raises TypeError.
elif len(L) == 2:
L.insert(1, And)
return ' '.join(L)
else:
L[-1] = '%s %s' % (And, L[-1])
sep = '%s ' % comma
return sep.join(L)
_unCommaTheRe = re.compile(r'(.*),\s*(the)$', re.I)
def unCommaThe(s):
"""Takes a string of the form 'foo, the' and turns it into 'the foo'."""
m = _unCommaTheRe.match(s)
if m is not None:
return '%s %s' % (m.group(2), m.group(1))
else:
return s
def wrapLines(s):
"""Word wraps several paragraphs in a string s."""
L = []
for line in s.splitlines():
L.append(textwrap.fill(line))
return '\n'.join(L)
def ellipsisify(s, n):
"""Returns a shortened version of s. Produces up to the first n chars at
the nearest word boundary.
"""
if len(s) <= n:
return s
else:
return (textwrap.wrap(s, n-3)[0] + '...')
plurals = TwoWayDictionary({})
def matchCase(s1, s2):
"""Matches the case of s1 in s2"""
if s1.isupper():
return s2.upper()
else:
L = list(s2)
for (i, char) in enumerate(s1[:len(s2)]):
if char.isupper():
L[i] = L[i].upper()
return ''.join(L)
consonants = 'bcdfghjklmnpqrstvwxz'
_pluralizeRegex = re.compile('[%s]y$' % consonants)
def pluralize(s, i=2):
"""Returns the plural of s based on its number i. Put any exceptions to
the general English rule of appending 's' in the plurals dictionary.
"""
if i == 1:
return s
else:
lowered = s.lower()
# Exception dictionary
if lowered in plurals:
return matchCase(s, plurals[lowered])
# Words ending with 'ch', 'sh' or 'ss' such as 'punch(es)', 'fish(es)
# and miss(es)
elif any(lowered.endswith, ['x', 'ch', 'sh', 'ss']):
return matchCase(s, s+'es')
# Words ending with a consonant followed by a 'y' such as
# 'try (tries)' or 'spy (spies)'
elif _pluralizeRegex.search(lowered):
return matchCase(s, s[:-1] + 'ies')
# In all other cases, we simply add an 's' to the base word
else:
return matchCase(s, s+'s')
_depluralizeRegex = re.compile('[%s]ies' % consonants)
def depluralize(s):
"""Returns the singular of s."""
lowered = s.lower()
if lowered in plurals:
return matchCase(s, plurals[lowered])
elif any(lowered.endswith, ['ches', 'shes', 'sses']):
return s[:-2]
elif re.search(_depluralizeRegex, lowered):
return s[:-3] + 'y'
else:
if lowered.endswith('s'):
return s[:-1] # Chop off 's'.
else:
return s # Don't know what to do.
def nItems(item, n, between=None):
"""Works like this:
>>> nItems('clock', 1)
'1 clock'
>>> nItems('clock', 10)
'10 clocks'
>>> nItems('clock', 10, between='grandfather')
'10 grandfather clocks'
"""
if between is None:
return '%s %s' % (n, pluralize(item, n))
else:
return '%s %s %s' % (n, between, pluralize(item, n))
def be(i):
"""Returns the form of the verb 'to be' based on the number i."""
if i == 1:
return 'is'
else:
return 'are'
def has(i):
"""Returns the form of the verb 'to have' based on the number i."""
if i == 1:
return 'has'
else:
return 'have'
def sortBy(f, L):
"""Uses the decorate-sort-undecorate pattern to sort L by function f."""
for (i, elt) in enumerate(L):
L[i] = (f(elt), i, elt)
L.sort()
for (i, elt) in enumerate(L):
L[i] = L[i][2]
if sys.version_info < (2, 4, 0):
def sorted(iterable, cmp=None, key=None, reversed=False):
L = list(iterable)
if key is not None:
assert cmp is None, 'Can\'t use both cmp and key.'
sortBy(key, L)
else:
L.sort(cmp)
if reversed:
L.reverse()
return L
__builtins__['sorted'] = sorted
def mktemp(suffix=''):
"""Gives a decent random string, suitable for a filename."""
r = random.Random()
m = md5.md5(suffix)
r.seed(time.time())
s = str(r.getstate())
for x in xrange(0, random.randrange(400), random.randrange(1, 5)):
m.update(str(x))
m.update(s)
m.update(str(time.time()))
s = m.hexdigest()
return sha.sha(s + str(time.time())).hexdigest() + suffix
def itersplit(isSeparator, iterable, maxsplit=-1, yieldEmpty=False):
"""itersplit(isSeparator, iterable, maxsplit=-1, yieldEmpty=False)
Splits an iterator based on a predicate isSeparator."""
if isinstance(isSeparator, basestring):
f = lambda s: s == isSeparator
else:
f = isSeparator
acc = []
for element in iterable:
if maxsplit == 0 or not f(element):
acc.append(element)
else:
maxsplit -= 1
if acc or yieldEmpty:
yield acc
acc = []
if acc or yieldEmpty:
yield acc
def flatten(seq, strings=False):
"""Flattens a list of lists into a single list. See the test for examples.
"""
for elt in seq:
if not strings and type(elt) == str or type(elt) == unicode:
yield elt
else:
try:
for x in flatten(elt):
yield x
except TypeError:
yield elt
def saltHash(password, salt=None, hash='sha'):
if salt is None:
salt = mktemp()[:8]
if hash == 'sha':
hasher = sha.sha
elif hash == 'md5':
hasher = md5.md5
return '|'.join([salt, hasher(salt + password).hexdigest()])
def safeEval(s, namespace={'True': True, 'False': False, 'None': None}):
"""Evaluates s, safely. Useful for turning strings into tuples/lists/etc.
without unsafely using eval()."""
try:
node = compiler.parse(s)
except SyntaxError, e:
raise ValueError, 'Invalid string: %s.' % e
nodes = compiler.parse(s).node.nodes
if not nodes:
if node.__class__ is compiler.ast.Module:
return node.doc
else:
raise ValueError, 'Unsafe string: %s' % quoted(s)
node = nodes[0]
if node.__class__ is not compiler.ast.Discard:
raise ValueError, 'Invalid expression: %s' % quoted(s)
node = node.getChildNodes()[0]
def checkNode(node):
if node.__class__ is compiler.ast.Const:
return True
if node.__class__ in (compiler.ast.List,
compiler.ast.Tuple,
compiler.ast.Dict):
return all(checkNode, node.getChildNodes())
if node.__class__ is compiler.ast.Name:
if node.name in namespace:
return True
else:
return False
else:
return False
if checkNode(node):
return eval(s, namespace, namespace)
else:
raise ValueError, 'Unsafe string: %s' % quoted(s)
def exnToString(e):
"""Turns a simple exception instance into a string (better than str(e))"""
strE = str(e)
if strE:
return '%s: %s' % (e.__class__.__name__, strE)
else:
return e.__class__.__name__
class IterableMap(object):
"""Define .iteritems() in a class and subclass this to get the other iters.
"""
def iteritems(self):
raise NotImplementedError
def iterkeys(self):
for (key, _) in self.iteritems():
yield key
__iter__ = iterkeys
def itervalues(self):
for (_, value) in self.iteritems():
yield value
def items(self):
return list(self.iteritems())
def keys(self):
return list(self.iterkeys())
def values(self):
return list(self.itervalues())
def __len__(self):
ret = 0
for _ in self.iteritems():
ret += 1
return ret
def __nonzero__(self):
for _ in self.iteritems():
return True
return False
def nonCommentLines(fd):
for line in fd:
if not line.startswith('#'):
yield line
def nonEmptyLines(fd):
## for line in fd:
## if line.strip():
## yield line
return ifilter(str.strip, fd)
def nonCommentNonEmptyLines(fd):
return nonEmptyLines(nonCommentLines(fd))
def changeFunctionName(f, name, doc=None):
if doc is None:
doc = f.__doc__
newf = types.FunctionType(f.func_code, f.func_globals, name,
f.func_defaults, f.func_closure)
newf.__doc__ = doc
return newf
def getSocket(host):
"""Returns a socket of the correct AF_INET type (v4 or v6) in order to
communicate with host.
"""
host = socket.gethostbyname(host)
if isIP(host):
return socket.socket(socket.AF_INET, socket.SOCK_STREAM)
elif isIPV6(host):
return socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
raise socket.error, 'Something wonky happened.'
def isIP(s):
"""Returns whether or not a given string is an IPV4 address.
>>> isIP('255.255.255.255')
1
>>> isIP('abc.abc.abc.abc')
0
"""
try:
return bool(socket.inet_aton(s))
except socket.error:
return False
def bruteIsIPV6(s):
if s.count('::') <= 1:
L = s.split(':')
if len(L) <= 8:
for x in L:
if x:
try:
int(x, 16)
except ValueError:
return False
return True
return False
def isIPV6(s):
"""Returns whether or not a given string is an IPV6 address."""
try:
if hasattr(socket, 'inet_pton'):
return bool(socket.inet_pton(socket.AF_INET6, s))
else:
return bruteIsIPV6(s)
except socket.error:
try:
socket.inet_pton(socket.AF_INET6, '::')
except socket.error:
# We gotta fake it.
return bruteIsIPV6(s)
return False
class InsensitivePreservingDict(UserDict.DictMixin, object):
def key(self, s):
"""Override this if you wish."""
if s is not None:
s = s.lower()
return s
def __init__(self, dict=None, key=None):
if key is not None:
self.key = key
self.data = {}
if dict is not None:
self.update(dict)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
super(InsensitivePreservingDict, self).__repr__())
def fromkeys(cls, keys, s=None, dict=None, key=None):
d = cls(dict=dict, key=key)
for key in keys:
d[key] = s
return d
fromkeys = classmethod(fromkeys)
def __getitem__(self, k):
return self.data[self.key(k)][1]
def __setitem__(self, k, v):
self.data[self.key(k)] = (k, v)
def __delitem__(self, k):
del self.data[self.key(k)]
def iteritems(self):
return self.data.itervalues()
def keys(self):
L = []
for (k, _) in self.iteritems():
L.append(k)
return L
def __reduce__(self):
return (self.__class__, (dict(self.data.values()),))
class NormalizingSet(sets.Set):
def __init__(self, iterable=()):
iterable = itertools.imap(self.normalize, iterable)
super(NormalizingSet, self).__init__(iterable)
def normalize(self, x):
return x
def add(self, x):
return super(NormalizingSet, self).add(self.normalize(x))
def remove(self, x):
return super(NormalizingSet, self).remove(self.normalize(x))
def discard(self, x):
return super(NormalizingSet, self).discard(self.normalize(x))
def __contains__(self, x):
return super(NormalizingSet, self).__contains__(self.normalize(x))
has_key = __contains__
def mungeEmailForWeb(s):
s = s.replace('@', ' AT ')
s = s.replace('.', ' DOT ')
return s
class AtomicFile(file):
"""Used for files that need to be atomically written -- i.e., if there's a
failure, the original file remains, unmodified. mode must be 'w' or 'wb'"""
def __init__(self, filename, mode='w', allowEmptyOverwrite=True,
makeBackupIfSmaller=True, tmpDir=None, backupDir=None):
if mode not in ('w', 'wb'):
raise ValueError, 'Invalid mode: %s' % quoted(mode)
self.rolledback = False
self.allowEmptyOverwrite = allowEmptyOverwrite
self.makeBackupIfSmaller = makeBackupIfSmaller
self.filename = filename
self.backupDir = backupDir
if tmpDir is None:
# If not given a tmpDir, we'll just put a random token on the end
# of our filename and put it in the same directory.
self.tempFilename = '%s.%s' % (self.filename, mktemp())
else:
# If given a tmpDir, we'll get the basename (just the filename, no
# directory), put our random token on the end, and put it in tmpDir
tempFilename = '%s.%s' % (os.path.basename(self.filename), mktemp())
self.tempFilename = os.path.join(tmpDir, tempFilename)
# This doesn't work because of the uncollectable garbage effect.
# self.__parent = super(AtomicFile, self)
super(AtomicFile, self).__init__(self.tempFilename, mode)
def rollback(self):
if not self.closed:
super(AtomicFile, self).close()
if os.path.exists(self.tempFilename):
os.remove(self.tempFilename)
self.rolledback = True
def close(self):
if not self.rolledback:
super(AtomicFile, self).close()
# We don't mind writing an empty file if the file we're overwriting
# doesn't exist.
newSize = os.path.getsize(self.tempFilename)
originalExists = os.path.exists(self.filename)
if newSize or self.allowEmptyOverwrite or not originalExists:
if originalExists:
oldSize = os.path.getsize(self.filename)
if self.makeBackupIfSmaller and newSize < oldSize:
now = int(time.time())
backupFilename = '%s.backup.%s' % (self.filename, now)
if self.backupDir is not None:
backupFilename = os.path.basename(backupFilename)
backupFilename = os.path.join(self.backupDir,
backupFilename)
shutil.copy(self.filename, backupFilename)
# We use shutil.move here instead of os.rename because
# the latter doesn't work on Windows when self.filename
# (the target) already exists. shutil.move handles those
# intricacies for us.
# This raises IOError if we can't write to the file. Since
# in *nix, it only takes write perms to the *directory* to
# rename a file (and shutil.move will use os.rename if
# possible), we first check if we have the write permission
# and only then do we write.
fd = file(self.filename, 'a')
fd.close()
shutil.move(self.tempFilename, self.filename)
else:
raise ValueError, 'AtomicFile.close called after rollback.'
def __del__(self):
# We rollback because if we're deleted without being explicitly closed,
# that's bad. We really should log this here, but as of yet we've got
# no logging facility in utils. I've got some ideas for this, though.
self.rollback()
def transactionalFile(*args, **kwargs):
# This exists so it can be replaced by a function that provides the tmpDir.
# We do that replacement in conf.py.
return AtomicFile(*args, **kwargs)
def stackTrace(frame=None, compact=True):
if frame is None:
frame = sys._getframe()
if compact:
L = []
while frame:
lineno = frame.f_lineno
funcname = frame.f_code.co_name
filename = os.path.basename(frame.f_code.co_filename)
L.append('[%s|%s|%s]' % (filename, funcname, lineno))
frame = frame.f_back
return textwrap.fill(' '.join(L))
else:
return traceback.format_stack(frame)
def callTracer(fd=None, basename=True):
if fd is None:
fd = sys.stdout
def tracer(frame, event, _):
if event == 'call':
code = frame.f_code
lineno = frame.f_lineno
funcname = code.co_name
filename = code.co_filename
if basename:
filename = os.path.basename(filename)
print >>fd, '%s: %s(%s)' % (filename, funcname, lineno)
return tracer
def toBool(s):
s = s.strip().lower()
if s in ('true', 'on', 'enable', 'enabled', '1'):
return True
elif s in ('false', 'off', 'disable', 'disabled', '0'):
return False
else:
raise ValueError, 'Invalid string for toBool: %s' % quoted(s)
def mapinto(f, L):
for (i, x) in enumerate(L):
L[i] = f(x)
if __name__ == '__main__':
import doctest
doctest.testmod(sys.modules['__main__'])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

129
src/webutils.py Normal file
View File

@ -0,0 +1,129 @@
###
# 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 supybot.fix as fix
import re
import socket
import urllib
import urllib2
import httplib
import urlparse
import supybot.conf as conf
Request = urllib2.Request
urlquote = urllib.quote
urlunquote = urllib.unquote
class WebError(Exception):
pass
# XXX We should tighten this up a bit.
urlRe = re.compile(r"(\w+://[^\])>\s]+)", re.I)
httpUrlRe = re.compile(r"(https?://[^\])>\s]+)", re.I)
REFUSED = 'Connection refused.'
TIMED_OUT = 'Connection timed out.'
UNKNOWN_HOST = 'Unknown host.'
RESET_BY_PEER = 'Connection reset by peer.'
FORBIDDEN = 'Client forbidden from accessing URL.'
def strError(e):
try:
n = e.args[0]
except Exception:
return str(e)
if n == 111:
return REFUSED
elif n in (110, 10060):
return TIMED_OUT
elif n == 104:
return RESET_BY_PEER
elif n in (8, 3, 2):
return UNKNOWN_HOST
elif n == 403:
return FORBIDDEN
else:
return str(e)
_headers = {
'User-agent': 'Mozilla/4.0 (compatible; Supybot %s)' % conf.version,
}
def getUrlFd(url, headers=None):
"""Gets a file-like object for a url."""
if headers is None:
headers = _headers
try:
if not isinstance(url, urllib2.Request):
if '#' in url:
url = url[:url.index('#')]
request = urllib2.Request(url, headers=headers)
else:
request = url
httpProxy = conf.supybot.protocols.http.proxy()
if httpProxy:
request.set_proxy(httpProxy, 'http')
fd = urllib2.urlopen(request)
return fd
except socket.timeout, e:
raise WebError, TIMED_OUT
except (socket.error, socket.sslerror), e:
raise WebError, strError(e)
except httplib.InvalidURL, e:
raise WebError, 'Invalid URL: %s' % e
except urllib2.HTTPError, e:
raise WebError, strError(e)
except urllib2.URLError, e:
raise WebError, strError(e.reason)
# Raised when urllib doesn't recognize the url type
except ValueError, e:
raise WebError, strError(e)
def getUrl(url, size=None, headers=None):
"""Gets a page. Returns a string that is the page gotten."""
fd = getUrlFd(url, headers=headers)
try:
if size is None:
text = fd.read()
else:
text = fd.read(size)
except socket.timeout, e:
raise WebError, TIMED_OUT
fd.close()
return text
def getDomain(url):
return urlparse.urlparse(url)[1]
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

202
src/world.py Normal file
View File

@ -0,0 +1,202 @@
###
# 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.
###
"""
Module for general worldly stuff, like global variables and whatnot.
"""
import supybot.fix as fix
import gc
import os
import sys
import sre
import time
import atexit
import threading
import supybot.log as log
import supybot.conf as conf
import supybot.drivers as drivers
import supybot.ircutils as ircutils
import supybot.registry as registry
startedAt = time.time() # Just in case it doesn't get set later.
starting = False
mainThread = threading.currentThread()
# ??? Should we do this? What do we gain?
# assert 'MainThread' in repr(mainThread)
def isMainThread():
return mainThread is threading.currentThread()
threadsSpawned = 1 # Starts at one for the initial "thread."
class SupyThread(threading.Thread):
def __init__(self, *args, **kwargs):
global threadsSpawned
threadsSpawned += 1
super(SupyThread, self).__init__(*args, **kwargs)
commandsProcessed = 0
ircs = [] # A list of all the IRCs.
def getIrc(network):
network = network.lower()
for irc in ircs:
if irc.network.lower() == network:
return irc
return None
def _flushUserData():
userdataFilename = os.path.join(conf.supybot.directories.conf(),
'userdata.conf')
registry.close(conf.users, userdataFilename)
flushers = [_flushUserData] # A periodic function will flush all these.
registryFilename = None
def flush():
"""Flushes all the registered flushers."""
for (i, f) in enumerate(flushers):
try:
f()
except Exception, e:
log.exception('Uncaught exception in flusher #%s (%s):', i, f)
def debugFlush(s=''):
if conf.supybot.debug.flushVeryOften():
if s:
log.debug(s)
flush()
def upkeep():
"""Does upkeep (like flushing, garbage collection, etc.)"""
sys.exc_clear() # Just in case, let's clear the exception info.
if os.name == 'nt':
try:
import msvcrt
msvcrt.heapmin()
except ImportError:
pass
except IOError: # Win98 sux0rs!
pass
if conf.daemonized:
# If we're daemonized, sys.stdout has been replaced with a StringIO
# object, so let's see if anything's been printed, and if so, let's
# log.warning it (things shouldn't be printed, and we're more likely
# to get bug reports if we make it a warning).
assert not type(sys.stdout) == file, 'Not a StringIO object!'
s = sys.stdout.getvalue()
if s:
log.warning('Printed to stdout after daemonization: %s', s)
sys.stdout.reset() # Seeks to 0.
sys.stdout.truncate() # Truncates to current offset.
assert not type(sys.stderr) == file, 'Not a StringIO object!'
s = sys.stderr.getvalue()
if s:
log.error('Printed to stderr after daemonization: %s', s)
sys.stderr.reset() # Seeks to 0.
sys.stderr.truncate() # Truncates to current offset.
doFlush = conf.supybot.flush() and not starting
if doFlush:
flush()
# This is so registry._cache gets filled.
# This seems dumb, so we'll try not doing it anymore.
#if registryFilename is not None:
# registry.open(registryFilename)
if not dying:
log.debug('Regexp cache size: %s', len(sre._cache))
log.debug('Pattern cache size: %s'%len(ircutils._patternCache))
log.debug('HostmaskPatternEqual cache size: %s' %
len(ircutils._hostmaskPatternEqualCache))
#timestamp = log.timestamp()
if doFlush:
log.info('Flushers flushed and garbage collected.')
else:
log.info('Garbage collected.')
collected = gc.collect()
if gc.garbage:
log.warning('Noncollectable garbage (file this as a bug on SF.net): %s',
gc.garbage)
return collected
def makeDriversDie():
"""Kills drivers."""
log.info('Killing Driver objects.')
for driver in drivers._drivers.itervalues():
driver.die()
def makeIrcsDie():
"""Kills Ircs."""
log.info('Killing Irc objects.')
for irc in ircs[:]:
if not irc.zombie:
irc.die()
else:
log.debug('Not killing %s, it\'s already a zombie.', irc)
def startDying():
"""Starts dying."""
log.info('Shutdown initiated.')
global dying
dying = True
def finished():
log.info('Shutdown complete.')
# These are in order; don't reorder them for cosmetic purposes. The order
# in which they're registered is the reverse order in which they will run.
atexit.register(finished)
atexit.register(upkeep)
atexit.register(makeIrcsDie)
atexit.register(makeDriversDie)
atexit.register(startDying)
##################################################
##################################################
##################################################
## Don't even *think* about messing with these. ##
##################################################
##################################################
##################################################
dying = False
testing = False
starting = False
profiling = False
documenting = False
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

570
test/test_callbacks.py Normal file
View File

@ -0,0 +1,570 @@
###
# 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 testsupport import *
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
import supybot.callbacks as callbacks
tokenize = callbacks.tokenize
class TokenizerTestCase(SupyTestCase):
def testEmpty(self):
self.assertEqual(tokenize(''), [])
def testNullCharacter(self):
self.assertEqual(tokenize(utils.dqrepr('\0')), ['\0'])
def testSingleDQInDQString(self):
self.assertEqual(tokenize('"\\""'), ['"'])
def testDQsWithBackslash(self):
self.assertEqual(tokenize('"\\\\"'), ["\\"])
def testDoubleQuotes(self):
self.assertEqual(tokenize('"\\"foo\\""'), ['"foo"'])
def testSingleWord(self):
self.assertEqual(tokenize('foo'), ['foo'])
def testMultipleSimpleWords(self):
words = 'one two three four five six seven eight'.split()
for i in range(len(words)):
self.assertEqual(tokenize(' '.join(words[:i])), words[:i])
def testSingleQuotesNotQuotes(self):
self.assertEqual(tokenize("it's"), ["it's"])
def testQuotedWords(self):
self.assertEqual(tokenize('"foo bar"'), ['foo bar'])
self.assertEqual(tokenize('""'), [''])
self.assertEqual(tokenize('foo "" bar'), ['foo', '', 'bar'])
self.assertEqual(tokenize('foo "bar baz" quux'),
['foo', 'bar baz', 'quux'])
def testNesting(self):
self.assertEqual(tokenize('[]'), [[]])
self.assertEqual(tokenize('[foo]'), [['foo']])
self.assertEqual(tokenize('[ foo ]'), [['foo']])
self.assertEqual(tokenize('foo [bar]'), ['foo', ['bar']])
self.assertEqual(tokenize('foo bar [baz quux]'),
['foo', 'bar', ['baz', 'quux']])
try:
orig = conf.supybot.commands.nested()
conf.supybot.commands.nested.setValue(False)
self.assertEqual(tokenize('[]'), ['[]'])
self.assertEqual(tokenize('[foo]'), ['[foo]'])
self.assertEqual(tokenize('foo [bar]'), ['foo', '[bar]'])
self.assertEqual(tokenize('foo bar [baz quux]'),
['foo', 'bar', '[baz', 'quux]'])
finally:
conf.supybot.commands.nested.setValue(orig)
def testError(self):
self.assertRaises(SyntaxError, tokenize, '[foo') #]
self.assertRaises(SyntaxError, tokenize, '"foo') #"
def testPipe(self):
try:
conf.supybot.commands.nested.pipeSyntax.setValue(True)
self.assertRaises(SyntaxError, tokenize, '| foo')
self.assertRaises(SyntaxError, tokenize, 'foo ||bar')
self.assertRaises(SyntaxError, tokenize, 'bar |')
self.assertEqual(tokenize('foo|bar'), ['bar', ['foo']])
self.assertEqual(tokenize('foo | bar'), ['bar', ['foo']])
self.assertEqual(tokenize('foo | bar | baz'),
['baz', ['bar',['foo']]])
self.assertEqual(tokenize('foo bar | baz'),
['baz', ['foo', 'bar']])
self.assertEqual(tokenize('foo | bar baz'),
['bar', 'baz', ['foo']])
self.assertEqual(tokenize('foo bar | baz quux'),
['baz', 'quux', ['foo', 'bar']])
finally:
conf.supybot.commands.nested.pipeSyntax.setValue(False)
self.assertEqual(tokenize('foo|bar'), ['foo|bar'])
self.assertEqual(tokenize('foo | bar'), ['foo', '|', 'bar'])
self.assertEqual(tokenize('foo | bar | baz'),
['foo', '|', 'bar', '|', 'baz'])
self.assertEqual(tokenize('foo bar | baz'),
['foo', 'bar', '|', 'baz'])
def testQuoteConfiguration(self):
f = callbacks.tokenize
self.assertEqual(f('[foo]'), [['foo']])
self.assertEqual(f('"[foo]"'), ['[foo]'])
try:
original = conf.supybot.commands.quotes()
conf.supybot.commands.quotes.setValue('`')
self.assertEqual(f('[foo]'), [['foo']])
self.assertEqual(f('`[foo]`'), ['[foo]'])
conf.supybot.commands.quotes.setValue('\'')
self.assertEqual(f('[foo]'), [['foo']])
self.assertEqual(f('\'[foo]\''), ['[foo]'])
conf.supybot.commands.quotes.setValue('`\'')
self.assertEqual(f('[foo]'), [['foo']])
self.assertEqual(f('`[foo]`'), ['[foo]'])
self.assertEqual(f('[foo]'), [['foo']])
self.assertEqual(f('\'[foo]\''), ['[foo]'])
finally:
conf.supybot.commands.quotes.setValue(original)
def testBold(self):
s = '\x02foo\x02'
self.assertEqual(tokenize(s), [s])
s = s[:-1] + '\x0f'
self.assertEqual(tokenize(s), [s])
def testColor(self):
s = '\x032,3foo\x03'
self.assertEqual(tokenize(s), [s])
s = s[:-1] + '\x0f'
self.assertEqual(tokenize(s), [s])
class FunctionsTestCase(SupyTestCase):
def testCanonicalName(self):
self.assertEqual('foo', callbacks.canonicalName('foo'))
self.assertEqual('foobar', callbacks.canonicalName('foo-bar'))
self.assertEqual('foobar', callbacks.canonicalName('foo_bar'))
self.assertEqual('foobar', callbacks.canonicalName('FOO-bar'))
self.assertEqual('foobar', callbacks.canonicalName('FOOBAR'))
self.assertEqual('foobar', callbacks.canonicalName('foo___bar'))
self.assertEqual('foobar', callbacks.canonicalName('_f_o_o-b_a_r'))
# The following seems to be a hack for the Karma plugin; I'm not
# entirely sure that it's completely necessary anymore.
self.assertEqual('foobar--', callbacks.canonicalName('foobar--'))
def testAddressed(self):
oldprefixchars = str(conf.supybot.reply.whenAddressedBy.chars)
nick = 'supybot'
conf.supybot.reply.whenAddressedBy.chars.set('~!@')
inChannel = ['~foo', '@foo', '!foo',
'%s: foo' % nick, '%s foo' % nick,
'%s: foo' % nick.capitalize(), '%s: foo' % nick.upper()]
inChannel = [ircmsgs.privmsg('#foo', s) for s in inChannel]
badmsg = ircmsgs.privmsg('#foo', '%s:foo' % nick)
self.failIf(callbacks.addressed(nick, badmsg))
badmsg = ircmsgs.privmsg('#foo', '%s^: foo' % nick)
self.failIf(callbacks.addressed(nick, badmsg))
for msg in inChannel:
self.assertEqual('foo', callbacks.addressed(nick, msg), msg)
msg = ircmsgs.privmsg(nick, 'foo')
self.assertEqual('foo', callbacks.addressed(nick, msg))
conf.supybot.reply.whenAddressedBy.chars.set(oldprefixchars)
msg = ircmsgs.privmsg('#foo', '%s::::: bar' % nick)
self.assertEqual('bar', callbacks.addressed(nick, msg))
msg = ircmsgs.privmsg('#foo', '%s: foo' % nick.upper())
self.assertEqual('foo', callbacks.addressed(nick, msg))
badmsg = ircmsgs.privmsg('#foo', '%s`: foo' % nick)
self.failIf(callbacks.addressed(nick, badmsg))
def testAddressedReplyWhenNotAddressed(self):
msg1 = ircmsgs.privmsg('#foo', '@bar')
msg2 = ircmsgs.privmsg('#foo', 'bar')
self.assertEqual(callbacks.addressed('blah', msg1), 'bar')
self.assertEqual(callbacks.addressed('blah', msg2), '')
try:
original = conf.supybot.reply.whenNotAddressed()
conf.supybot.reply.whenNotAddressed.setValue(True)
# need to recreate the msg objects since the old ones have already
# been tagged
msg1 = ircmsgs.privmsg('#foo', '@bar')
msg2 = ircmsgs.privmsg('#foo', 'bar')
self.assertEqual(callbacks.addressed('blah', msg1), 'bar')
self.assertEqual(callbacks.addressed('blah', msg2), 'bar')
finally:
conf.supybot.reply.whenNotAddressed.setValue(original)
def testAddressedWithMultipleNicks(self):
msg = ircmsgs.privmsg('#foo', 'bar: baz')
self.assertEqual(callbacks.addressed('bar', msg), 'baz')
# need to recreate the msg objects since the old ones have already
# been tagged
msg = ircmsgs.privmsg('#foo', 'bar: baz')
self.assertEqual(callbacks.addressed('biff', msg, nicks=['bar']),
'baz')
def testAddressedWithNickAtEnd(self):
msg = ircmsgs.privmsg('#foo', 'baz, bar')
self.assertEqual(callbacks.addressed('bar', msg,
whenAddressedByNickAtEnd=True),
'baz')
def testAddressedPrefixCharsTakePrecedenceOverNickAtEnd(self):
msg = ircmsgs.privmsg('#foo', '@echo foo')
self.assertEqual(callbacks.addressed('foo', msg,
whenAddressedByNickAtEnd=True,
prefixChars='@'),
'echo foo')
def testReply(self):
prefix = 'foo!bar@baz'
channelMsg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix)
nonChannelMsg = ircmsgs.privmsg('supybot', 'bar baz', prefix=prefix)
self.assertEqual(ircmsgs.privmsg(nonChannelMsg.nick, 'foo'),
callbacks.reply(channelMsg, 'foo', private=True))
self.assertEqual(ircmsgs.privmsg(nonChannelMsg.nick, 'foo'),
callbacks.reply(nonChannelMsg, 'foo'))
self.assertEqual(ircmsgs.privmsg(channelMsg.args[0],
'%s: foo' % channelMsg.nick),
callbacks.reply(channelMsg, 'foo'))
self.assertEqual(ircmsgs.privmsg(channelMsg.args[0],
'foo'),
callbacks.reply(channelMsg, 'foo', prefixName=False))
self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'),
callbacks.reply(channelMsg, 'foo',
notice=True, private=True))
def testReplyTo(self):
prefix = 'foo!bar@baz'
msg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix)
self.assertEqual(callbacks.reply(msg, 'blah', to='blah'),
ircmsgs.privmsg('#foo', 'blah: blah'))
self.assertEqual(callbacks.reply(msg, 'blah', to='blah', private=True),
ircmsgs.privmsg('blah', 'blah'))
def testGetCommands(self):
self.assertEqual(callbacks.getCommands(['foo']), ['foo'])
self.assertEqual(callbacks.getCommands(['foo', 'bar']), ['foo'])
self.assertEqual(callbacks.getCommands(['foo', ['bar', 'baz']]),
['foo', 'bar'])
self.assertEqual(callbacks.getCommands(['foo', 'bar', ['baz']]),
['foo', 'baz'])
self.assertEqual(callbacks.getCommands(['foo', ['bar'], ['baz']]),
['foo', 'bar', 'baz'])
def testTokenize(self):
self.assertEqual(callbacks.tokenize(''), [])
self.assertEqual(callbacks.tokenize('foo'), ['foo'])
self.assertEqual(callbacks.tokenize('foo'), ['foo'])
self.assertEqual(callbacks.tokenize('bar [baz]'), ['bar', ['baz']])
class PrivmsgTestCase(ChannelPluginTestCase):
plugins = ('Utilities', 'Misc', 'Http')
conf.allowEval = True
timeout = 2
def testEmptySquareBrackets(self):
self.assertError('echo []')
def testHelpNoNameError(self):
# This will raise a NameError if some dynamic scoping isn't working
self.assertHelp('extension')
def testMaximumNestingDepth(self):
original = conf.supybot.commands.nested.maximum()
try:
conf.supybot.commands.nested.maximum.setValue(3)
self.assertResponse('echo foo', 'foo')
self.assertResponse('echo [echo foo]', 'foo')
self.assertResponse('echo [echo [echo foo]]', 'foo')
self.assertResponse('echo [echo [echo [echo foo]]]', 'foo')
self.assertError('echo [echo [echo [echo [echo foo]]]]')
finally:
conf.supybot.commands.nested.maximum.setValue(original)
def testSimpleReply(self):
self.assertResponse("eval irc.reply('foo')", 'foo')
def testSimpleReplyAction(self):
self.assertResponse("eval irc.reply('foo', action=True)",
'\x01ACTION foo\x01')
def testReplyWithNickPrefix(self):
self.feedMsg('@strlen foo')
m = self.irc.takeMsg()
self.failUnless(m is not None, 'm: %r' % m)
self.failUnless(m.args[1].startswith(self.nick))
try:
original = conf.supybot.reply.withNickPrefix()
conf.supybot.reply.withNickPrefix.setValue(False)
self.feedMsg('@strlen foobar')
m = self.irc.takeMsg()
self.failUnless(m is not None)
self.failIf(m.args[1].startswith(self.nick))
finally:
conf.supybot.reply.withNickPrefix.setValue(original)
def testErrorPrivateKwarg(self):
try:
original = conf.supybot.reply.error.inPrivate()
conf.supybot.reply.error.inPrivate.setValue(False)
m = self.getMsg("eval irc.error('foo', private=True)")
self.failUnless(m, 'No message returned.')
self.failIf(ircutils.isChannel(m.args[0]))
finally:
conf.supybot.reply.error.inPrivate.setValue(original)
def testErrorNoArgumentIsArgumentError(self):
self.assertHelp('eval irc.error()')
def testErrorWithNotice(self):
try:
original = conf.supybot.reply.error.withNotice()
conf.supybot.reply.error.withNotice.setValue(True)
m = self.getMsg("eval irc.error('foo')")
self.failUnless(m, 'No message returned.')
self.failUnless(m.command == 'NOTICE')
finally:
conf.supybot.reply.error.withNotice.setValue(original)
def testErrorReplyPrivate(self):
try:
original = str(conf.supybot.reply.error.inPrivate)
conf.supybot.reply.error.inPrivate.set('False')
# If this doesn't raise an error, we've got a problem, so the next
# two assertions shouldn't run. So we first check that what we
# expect to error actually does so we don't go on a wild goose
# chase because our command never errored in the first place :)
s = 're s/foo/bar baz' # will error; should be "re s/foo/bar/ baz"
self.assertError(s)
m = self.getMsg(s)
self.failUnless(ircutils.isChannel(m.args[0]))
conf.supybot.reply.error.inPrivate.set('True')
m = self.getMsg(s)
self.failIf(ircutils.isChannel(m.args[0]))
finally:
conf.supybot.reply.error.inPrivate.set(original)
# Now for stuff not based on the plugins.
class First(callbacks.Privmsg):
def firstcmd(self, irc, msg, args):
"""First"""
irc.reply('foo')
class Second(callbacks.Privmsg):
def secondcmd(self, irc, msg, args):
"""Second"""
irc.reply('bar')
class FirstRepeat(callbacks.Privmsg):
def firstcmd(self, irc, msg, args):
"""FirstRepeat"""
irc.reply('baz')
class Third(callbacks.Privmsg):
def third(self, irc, msg, args):
"""Third"""
irc.reply(' '.join(args))
def tearDown(self):
if hasattr(self.First, 'first'):
del self.First.first
if hasattr(self.Second, 'second'):
del self.Second.second
if hasattr(self.FirstRepeat, 'firstrepeat'):
del self.FirstRepeat.firstrepeat
ChannelPluginTestCase.tearDown(self)
def testDispatching(self):
self.irc.addCallback(self.First())
self.irc.addCallback(self.Second())
self.assertResponse('firstcmd', 'foo')
self.assertResponse('secondcmd', 'bar')
self.assertResponse('first firstcmd', 'foo')
self.assertResponse('second secondcmd', 'bar')
def testAmbiguousError(self):
self.irc.addCallback(self.First())
self.assertNotError('firstcmd')
self.irc.addCallback(self.FirstRepeat())
self.assertError('firstcmd')
self.assertError('firstcmd [firstcmd]')
self.assertNotRegexp('firstcmd', '(foo.*baz|baz.*foo)')
self.assertResponse('first firstcmd', 'foo')
self.assertResponse('firstrepeat firstcmd', 'baz')
def testAmbiguousHelpError(self):
self.irc.addCallback(self.First())
self.irc.addCallback(self.FirstRepeat())
self.assertError('help first')
def testHelpDispatching(self):
self.irc.addCallback(self.First())
self.assertHelp('help firstcmd')
self.assertHelp('help first firstcmd')
self.irc.addCallback(self.FirstRepeat())
self.assertError('help firstcmd')
self.assertRegexp('help first firstcmd', 'First', 0) # no re.I flag.
self.assertRegexp('help firstrepeat firstcmd', 'FirstRepeat', 0)
class TwoRepliesFirstAction(callbacks.Privmsg):
def testactionreply(self, irc, msg, args):
irc.reply('foo', action=True)
irc.reply('bar') # We're going to check that this isn't an action.
def testNotActionSecondReply(self):
self.irc.addCallback(self.TwoRepliesFirstAction())
self.assertAction('testactionreply', 'foo')
m = self.getMsg(' ')
self.failIf(m.args[1].startswith('\x01ACTION'))
def testEmptyNest(self):
try:
conf.supybot.reply.whenNotCommand.set('True')
self.assertError('echo []')
conf.supybot.reply.whenNotCommand.set('False')
self.assertResponse('echo []', '[]')
finally:
conf.supybot.reply.whenNotCommand.set('False')
def testDispatcherHelp(self):
self.assertNotRegexp('help first', r'\(dispatcher')
self.assertNotRegexp('help first', r'%s')
def testDefaultCommand(self):
self.irc.addCallback(self.First())
self.irc.addCallback(self.Third())
self.assertError('first blah')
self.assertResponse('third foo bar baz', 'foo bar baz')
def testSyntaxErrorNotEscaping(self):
self.assertError('load [foo')
self.assertError('load foo]')
def testNoEscapingAttributeErrorFromTokenizeWithFirstElementList(self):
self.assertError('[plugin list] list')
class InvalidCommand(callbacks.Privmsg):
def invalidCommand(self, irc, msg, tokens):
irc.reply('foo')
def testInvalidCommandOneReplyOnly(self):
try:
original = str(conf.supybot.reply.whenNotCommand)
conf.supybot.reply.whenNotCommand.set('True')
self.assertRegexp('asdfjkl', 'not a valid command')
self.irc.addCallback(self.InvalidCommand())
self.assertResponse('asdfjkl', 'foo')
self.assertNoResponse(' ', 2)
finally:
conf.supybot.reply.whenNotCommand.set(original)
class BadInvalidCommand(callbacks.Privmsg):
def invalidCommand(self, irc, msg, tokens):
s = 'This shouldn\'t keep Misc.invalidCommand from being called'
raise Exception, s
def testBadInvalidCommandDoesNotKillAll(self):
try:
original = str(conf.supybot.reply.whenNotCommand)
conf.supybot.reply.whenNotCommand.set('True')
self.irc.addCallback(self.BadInvalidCommand())
self.assertRegexp('asdfjkl', 'not a valid command')
finally:
conf.supybot.reply.whenNotCommand.set(original)
class PrivmsgCommandAndRegexpTestCase(PluginTestCase):
plugins = ()
class PCAR(callbacks.PrivmsgCommandAndRegexp):
def test(self, irc, msg, args):
"<foo>"
raise callbacks.ArgumentError
def testNoEscapingArgumentError(self):
self.irc.addCallback(self.PCAR())
self.assertResponse('test', 'test <foo>')
class RichReplyMethodsTestCase(PluginTestCase):
plugins = ()
class NoCapability(callbacks.Privmsg):
def error(self, irc, msg, args):
irc.errorNoCapability('admin')
def testErrorNoCapability(self):
self.irc.addCallback(self.NoCapability())
self.assertRegexp('error', 'admin')
class WithPrivateNoticeTestCase(ChannelPluginTestCase):
plugins = ('Utilities',)
class WithPrivateNotice(callbacks.Privmsg):
def normal(self, irc, msg, args):
irc.reply('should be with private notice')
def explicit(self, irc, msg, args):
irc.reply('should not be with private notice',
private=False, notice=False)
def implicit(self, irc, msg, args):
irc.reply('should be with notice due to private',
private=True)
def test(self):
self.irc.addCallback(self.WithPrivateNotice())
# Check normal behavior.
m = self.assertNotError('normal')
self.failIf(m.command == 'NOTICE')
self.failUnless(ircutils.isChannel(m.args[0]))
m = self.assertNotError('explicit')
self.failIf(m.command == 'NOTICE')
self.failUnless(ircutils.isChannel(m.args[0]))
# Check abnormal behavior.
originalInPrivate = conf.supybot.reply.inPrivate()
originalWithNotice = conf.supybot.reply.withNotice()
try:
conf.supybot.reply.inPrivate.setValue(True)
conf.supybot.reply.withNotice.setValue(True)
m = self.assertNotError('normal')
self.failUnless(m.command == 'NOTICE')
self.failIf(ircutils.isChannel(m.args[0]))
m = self.assertNotError('explicit')
self.failIf(m.command == 'NOTICE')
self.failUnless(ircutils.isChannel(m.args[0]))
finally:
conf.supybot.reply.inPrivate.setValue(originalInPrivate)
conf.supybot.reply.withNotice.setValue(originalWithNotice)
orig = conf.supybot.reply.withNoticeWhenPrivate()
try:
conf.supybot.reply.withNoticeWhenPrivate.setValue(True)
m = self.assertNotError('implicit')
self.failUnless(m.command == 'NOTICE')
self.failIf(ircutils.isChannel(m.args[0]))
m = self.assertNotError('normal')
self.failIf(m.command == 'NOTICE')
self.failUnless(ircutils.isChannel(m.args[0]))
finally:
conf.supybot.reply.withNoticeWhenPrivate.setValue(orig)
def testWithNoticeWhenPrivateNotChannel(self):
original = conf.supybot.reply.withNoticeWhenPrivate()
try:
conf.supybot.reply.withNoticeWhenPrivate.setValue(True)
m = self.assertNotError("eval irc.reply('y',to='x',private=True)")
self.failUnless(m.command == 'NOTICE')
m = self.getMsg(' ')
m = self.assertNotError("eval irc.reply('y',to='#x',private=True)")
self.failIf(m.command == 'NOTICE')
finally:
conf.supybot.reply.withNoticeWhenPrivate.setValue(original)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

138
test/test_commands.py Normal file
View File

@ -0,0 +1,138 @@
###
# 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 testsupport import *
from supybot.commands import *
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.callbacks as callbacks
class CommandsTestCase(SupyTestCase):
def assertState(self, spec, given, expected, target='test', **kwargs):
msg = ircmsgs.privmsg(target, 'foo')
realIrc = getTestIrc()
realIrc.nick = 'test'
realIrc.state.supported['chantypes'] = '#'
irc = callbacks.SimpleProxy(realIrc, msg)
myspec = Spec(spec, **kwargs)
state = myspec(irc, msg, given)
self.assertEqual(state.args, expected,
'Expected %r, got %r' % (expected, state.args))
def testEmptySpec(self):
self.assertState([], [], [])
def testSpecInt(self):
self.assertState(['int'], ['1'], [1])
self.assertState(['int', 'int', 'int'], ['1', '2', '3'], [1, 2, 3])
def testSpecLong(self):
self.assertState(['long'], ['1'], [1L])
self.assertState(['long', 'long', 'long'], ['1', '2', '3'],
[1L, 2L, 3L])
def testRestHandling(self):
self.assertState([rest(None)], ['foo', 'bar', 'baz'], ['foo bar baz'])
def testRestRequiresArgs(self):
self.assertRaises(callbacks.ArgumentError,
self.assertState, [rest('something')], [], ['asdf'])
def testOptional(self):
spec = [optional('int', 999), None]
self.assertState(spec, ['12', 'foo'], [12, 'foo'])
self.assertState(spec, ['foo'], [999, 'foo'])
def testAdditional(self):
spec = [additional('int', 999)]
self.assertState(spec, ['12'], [12])
self.assertState(spec, [], [999])
self.assertRaises(callbacks.Error,
self.assertState, spec, ['foo'], ['asdf'])
def testReverse(self):
spec = [reverse('positiveInt'), 'float', 'text']
self.assertState(spec, ['-1.0', 'foo', '1'], [1, -1.0, 'foo'])
def testGetopts(self):
spec = ['int', getopts({'foo': None, 'bar': 'int'}), 'int']
self.assertState(spec,
['12', '--foo', 'baz', '--bar', '13', '15'],
[12, [('foo', 'baz'), ('bar', 13)], 15])
def testAny(self):
self.assertState([any('int')], ['1', '2', '3'], [[1, 2, 3]])
self.assertState([None, any('int')], ['1', '2', '3'], ['1', [2, 3]])
self.assertState([any('int')], [], [[]])
self.assertState([any('int', continueOnError=True), 'text'],
['1', '2', 'test'], [[1, 2], 'test'])
def testMany(self):
spec = [many('int')]
self.assertState(spec, ['1', '2', '3'], [[1, 2, 3]])
self.assertRaises(callbacks.Error,
self.assertState, spec, [], ['asdf'])
def testChannelRespectsNetwork(self):
spec = ['channel', 'text']
self.assertState(spec, ['#foo', '+s'], ['#foo', '+s'])
self.assertState(spec, ['+s'], ['#foo', '+s'], target='#foo')
def testGlob(self):
spec = ['glob']
self.assertState(spec, ['foo'], ['*foo*'])
self.assertState(spec, ['?foo'], ['?foo'])
self.assertState(spec, ['foo*'], ['foo*'])
def testGetId(self):
spec = ['id']
self.assertState(spec, ['#12'], [12])
def testCommaList(self):
spec = [commalist('int')]
self.assertState(spec, ['12'], [[12]])
self.assertState(spec, ['12,', '10'], [[12, 10]])
self.assertState(spec, ['12,11,10,', '9'], [[12, 11, 10, 9]])
spec.append('int')
self.assertState(spec, ['12,11,10', '9'], [[12, 11, 10], 9])
def testLiteral(self):
spec = [('literal', ['foo', 'bar', 'baz'])]
self.assertState(spec, ['foo'], ['foo'])
self.assertState(spec, ['fo'], ['foo'])
self.assertState(spec, ['f'], ['foo'])
self.assertState(spec, ['bar'], ['bar'])
self.assertState(spec, ['baz'], ['baz'])
self.assertRaises(callbacks.ArgumentError,
self.assertState, spec, ['ba'], ['baz'])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

127
test/test_fix.py Normal file
View File

@ -0,0 +1,127 @@
###
# 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 __future__ import generators
from testsupport import *
import random
import itertools
class FunctionsTest(SupyTestCase):
def testRandomChoice(self):
self.assertRaises(IndexError, random.choice, {})
def testReversed(self):
L = range(10)
revL = list(reversed(L))
L.reverse()
self.assertEqual(L, revL, 'reversed didn\'t return reversed list')
for _ in reversed([]):
self.fail('reversed caused iteration over empty sequence')
def testGroup(self):
s = '1. d4 d5 2. Nf3 Nc6 3. e3 Nf6 4. Nc3 e6 5. Bd3 a6'
self.assertEqual(group(s.split(), 3)[:3],
[['1.', 'd4', 'd5'],
['2.', 'Nf3', 'Nc6'],
['3.', 'e3', 'Nf6']])
def testWindow(self):
L = range(10)
def wwindow(*args):
return list(window(*args))
self.assertEqual(wwindow([], 1), [], 'Empty sequence, empty window')
self.assertEqual(wwindow([], 2), [], 'Empty sequence, empty window')
self.assertEqual(wwindow([], 5), [], 'Empty sequence, empty window')
self.assertEqual(wwindow([], 100), [], 'Empty sequence, empty window')
self.assertEqual(wwindow(L, 1), [[x] for x in L], 'Window length 1')
self.assertRaises(ValueError, wwindow, [], 0)
self.assertRaises(ValueError, wwindow, [], -1)
def testAny(self):
self.failUnless(any(lambda i: i == 0, range(10)))
self.failIf(any(None, range(1)))
self.failUnless(any(None, range(2)))
self.failIf(any(None, []))
def testAll(self):
self.failIf(all(lambda i: i == 0, range(10)))
self.failIf(all(lambda i: i % 2, range(2)))
self.failIf(all(lambda i: i % 2 == 0, [1, 3, 5]))
self.failUnless(all(lambda i: i % 2 == 0, [2, 4, 6]))
self.failUnless(all(None, ()))
def testPartition(self):
L = range(10)
def even(i):
return not(i % 2)
(yes, no) = partition(even, L)
self.assertEqual(yes, [0, 2, 4, 6, 8])
self.assertEqual(no, [1, 3, 5, 7, 9])
def testIlen(self):
self.assertEqual(itertools.ilen(iter(range(10))), 10)
def testRsplit(self):
self.assertEqual(rsplit('foo bar baz'), 'foo bar baz'.split())
self.assertEqual(rsplit('foo bar baz', maxsplit=1),
['foo bar', 'baz'])
self.assertEqual(rsplit('foo bar baz', maxsplit=1),
['foo bar', 'baz'])
self.assertEqual(rsplit('foobarbaz', 'bar'), ['foo', 'baz'])
class TestDynamic(SupyTestCase):
def test(self):
def f(x):
i = 2
return g(x)
def g(y):
j = 3
return h(y)
def h(z):
self.assertEqual(dynamic.z, z)
self.assertEqual(dynamic.j, 3)
self.assertEqual(dynamic.i, 2)
self.assertEqual(dynamic.y, z)
self.assertEqual(dynamic.x, z)
#self.assertRaises(NameError, getattr, dynamic, 'asdfqwerqewr')
self.assertEqual(dynamic.self, self)
return z
self.assertEqual(f(10), 10)
def testCommonUsage(self):
foo = 'bar'
def f():
foo = dynamic.foo
self.assertEqual(foo, 'bar')
f()
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

564
test/test_ircdb.py Normal file
View File

@ -0,0 +1,564 @@
###
# 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 testsupport import *
import os
import unittest
import supybot.conf as conf
import supybot.world as world
import supybot.ircdb as ircdb
import supybot.ircutils as ircutils
class IrcdbTestCase(SupyTestCase):
def setUp(self):
world.testing = False
SupyTestCase.setUp(self)
def tearDown(self):
world.testing = True
SupyTestCase.tearDown(self)
class FunctionsTestCase(IrcdbTestCase):
def testIsAntiCapability(self):
self.failIf(ircdb.isAntiCapability('foo'))
self.failIf(ircdb.isAntiCapability('#foo,bar'))
self.failUnless(ircdb.isAntiCapability('-foo'))
self.failUnless(ircdb.isAntiCapability('#foo,-bar'))
self.failUnless(ircdb.isAntiCapability('#foo.bar,-baz'))
def testIsChannelCapability(self):
self.failIf(ircdb.isChannelCapability('foo'))
self.failUnless(ircdb.isChannelCapability('#foo,bar'))
self.failUnless(ircdb.isChannelCapability('#foo.bar,baz'))
self.failUnless(ircdb.isChannelCapability('#foo,bar.baz'))
def testMakeAntiCapability(self):
self.assertEqual(ircdb.makeAntiCapability('foo'), '-foo')
self.assertEqual(ircdb.makeAntiCapability('#foo,bar'), '#foo,-bar')
def testMakeChannelCapability(self):
self.assertEqual(ircdb.makeChannelCapability('#f', 'b'), '#f,b')
self.assertEqual(ircdb.makeChannelCapability('#f', '-b'), '#f,-b')
def testFromChannelCapability(self):
self.assertEqual(ircdb.fromChannelCapability('#foo,bar'),
['#foo', 'bar'])
self.assertEqual(ircdb.fromChannelCapability('#foo.bar,baz'),
['#foo.bar', 'baz'])
self.assertEqual(ircdb.fromChannelCapability('#foo,bar.baz'),
['#foo', 'bar.baz'])
def testUnAntiCapability(self):
self.assertEqual(ircdb.unAntiCapability('-bar'), 'bar')
self.assertEqual(ircdb.unAntiCapability('#foo,-bar'), '#foo,bar')
self.assertEqual(ircdb.unAntiCapability('#foo.bar,-baz'),
'#foo.bar,baz')
def testInvertCapability(self):
self.assertEqual(ircdb.invertCapability('bar'), '-bar')
self.assertEqual(ircdb.invertCapability('-bar'), 'bar')
self.assertEqual(ircdb.invertCapability('#foo,bar'), '#foo,-bar')
self.assertEqual(ircdb.invertCapability('#foo,-bar'), '#foo,bar')
class CapabilitySetTestCase(SupyTestCase):
def testGeneral(self):
d = ircdb.CapabilitySet()
self.assertRaises(KeyError, d.check, 'foo')
d = ircdb.CapabilitySet(('foo',))
self.failUnless(d.check('foo'))
self.failIf(d.check('-foo'))
d.add('bar')
self.failUnless(d.check('bar'))
self.failIf(d.check('-bar'))
d.add('-baz')
self.failIf(d.check('baz'))
self.failUnless(d.check('-baz'))
d.add('-bar')
self.failIf(d.check('bar'))
self.failUnless(d.check('-bar'))
d.remove('-bar')
self.assertRaises(KeyError, d.check, '-bar')
self.assertRaises(KeyError, d.check, 'bar')
def testReprEval(self):
s = ircdb.UserCapabilitySet()
self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__))
s.add('foo')
self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__))
s.add('bar')
self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__))
def testContains(self):
s = ircdb.CapabilitySet()
self.failIf('foo' in s)
self.failIf('-foo' in s)
s.add('foo')
self.failUnless('foo' in s)
self.failUnless('-foo' in s)
s.remove('foo')
self.failIf('foo' in s)
self.failIf('-foo' in s)
s.add('-foo')
self.failUnless('foo' in s)
self.failUnless('-foo' in s)
def testCheck(self):
s = ircdb.CapabilitySet()
self.assertRaises(KeyError, s.check, 'foo')
self.assertRaises(KeyError, s.check, '-foo')
s.add('foo')
self.failUnless(s.check('foo'))
self.failIf(s.check('-foo'))
s.remove('foo')
self.assertRaises(KeyError, s.check, 'foo')
self.assertRaises(KeyError, s.check, '-foo')
s.add('-foo')
self.failIf(s.check('foo'))
self.failUnless(s.check('-foo'))
s.remove('-foo')
self.assertRaises(KeyError, s.check, 'foo')
self.assertRaises(KeyError, s.check, '-foo')
def testAdd(self):
s = ircdb.CapabilitySet()
s.add('foo')
s.add('-foo')
self.failIf(s.check('foo'))
self.failUnless(s.check('-foo'))
s.add('foo')
self.failUnless(s.check('foo'))
self.failIf(s.check('-foo'))
class UserCapabilitySetTestCase(SupyTestCase):
def testOwnerHasAll(self):
d = ircdb.UserCapabilitySet(('owner',))
self.failIf(d.check('-foo'))
self.failUnless(d.check('foo'))
def testOwnerIsAlwaysPresent(self):
d = ircdb.UserCapabilitySet()
self.failUnless('owner' in d)
self.failUnless('-owner' in d)
self.failIf(d.check('owner'))
d.add('owner')
self.failUnless(d.check('owner'))
def testReprEval(self):
s = ircdb.UserCapabilitySet()
self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__))
s.add('foo')
self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__))
s.add('bar')
self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__))
def testOwner(self):
s = ircdb.UserCapabilitySet()
s.add('owner')
self.failUnless('foo' in s)
self.failUnless('-foo' in s)
self.failUnless(s.check('owner'))
self.failIf(s.check('-owner'))
self.failIf(s.check('-foo'))
self.failUnless(s.check('foo'))
## def testWorksAfterReload(self):
## s = ircdb.UserCapabilitySet(['owner'])
## self.failUnless(s.check('owner'))
## import sets
## reload(sets)
## self.failUnless(s.check('owner'))
class IrcUserTestCase(IrcdbTestCase):
def testCapabilities(self):
u = ircdb.IrcUser()
u.addCapability('foo')
self.failUnless(u._checkCapability('foo'))
self.failIf(u._checkCapability('-foo'))
u.addCapability('-bar')
self.failUnless(u._checkCapability('-bar'))
self.failIf(u._checkCapability('bar'))
u.removeCapability('foo')
u.removeCapability('-bar')
self.assertRaises(KeyError, u._checkCapability, 'foo')
self.assertRaises(KeyError, u._checkCapability, '-bar')
def testAddhostmask(self):
u = ircdb.IrcUser()
self.assertRaises(ValueError, u.addHostmask, '*!*@*')
def testRemoveHostmask(self):
u = ircdb.IrcUser()
u.addHostmask('foo!bar@baz')
self.failUnless(u.checkHostmask('foo!bar@baz'))
u.addHostmask('foo!bar@baz')
u.removeHostmask('foo!bar@baz')
self.failIf(u.checkHostmask('foo!bar@baz'))
def testOwner(self):
u = ircdb.IrcUser()
u.addCapability('owner')
self.failUnless(u._checkCapability('foo'))
self.failIf(u._checkCapability('-foo'))
def testInitCapabilities(self):
u = ircdb.IrcUser(capabilities=['foo'])
self.failUnless(u._checkCapability('foo'))
def testPassword(self):
u = ircdb.IrcUser()
u.setPassword('foobar')
self.failUnless(u.checkPassword('foobar'))
self.failIf(u.checkPassword('somethingelse'))
def testTimeoutAuth(self):
orig = conf.supybot.databases.users.timeoutIdentification()
try:
conf.supybot.databases.users.timeoutIdentification.setValue(2)
u = ircdb.IrcUser()
u.addAuth('foo!bar@baz')
self.failUnless(u.checkHostmask('foo!bar@baz'))
time.sleep(2.1)
self.failIf(u.checkHostmask('foo!bar@baz'))
finally:
conf.supybot.databases.users.timeoutIdentification.setValue(orig)
def testMultipleAuth(self):
orig = conf.supybot.databases.users.timeoutIdentification()
try:
conf.supybot.databases.users.timeoutIdentification.setValue(2)
u = ircdb.IrcUser()
u.addAuth('foo!bar@baz')
self.failUnless(u.checkHostmask('foo!bar@baz'))
u.addAuth('boo!far@fizz')
self.failUnless(u.checkHostmask('boo!far@fizz'))
time.sleep(2.1)
self.failIf(u.checkHostmask('foo!bar@baz'))
self.failIf(u.checkHostmask('boo!far@fizz'))
finally:
conf.supybot.databases.users.timeoutIdentification.setValue(orig)
def testHashedPassword(self):
u = ircdb.IrcUser()
u.setPassword('foobar', hashed=True)
self.failUnless(u.checkPassword('foobar'))
self.failIf(u.checkPassword('somethingelse'))
self.assertNotEqual(u.password, 'foobar')
def testHostmasks(self):
prefix = 'foo12341234!bar@baz.domain.tld'
hostmasks = ['*!bar@baz.domain.tld', 'foo12341234!*@*']
u = ircdb.IrcUser()
self.failIf(u.checkHostmask(prefix))
for hostmask in hostmasks:
u.addHostmask(hostmask)
self.failUnless(u.checkHostmask(prefix))
def testAuth(self):
prefix = 'foo!bar@baz'
u = ircdb.IrcUser()
u.addAuth(prefix)
self.failUnless(u.auth)
u.clearAuth()
self.failIf(u.auth)
def testIgnore(self):
u = ircdb.IrcUser(ignore=True)
self.failIf(u._checkCapability('foo'))
self.failUnless(u._checkCapability('-foo'))
def testRemoveCapability(self):
u = ircdb.IrcUser(capabilities=('foo',))
self.assertRaises(KeyError, u.removeCapability, 'bar')
class IrcChannelTestCase(IrcdbTestCase):
def testInit(self):
c = ircdb.IrcChannel()
self.failIf(c._checkCapability('op'))
self.failIf(c._checkCapability('voice'))
self.failIf(c._checkCapability('halfop'))
self.failIf(c._checkCapability('protected'))
def testCapabilities(self):
c = ircdb.IrcChannel(defaultAllow=False)
self.failIf(c._checkCapability('foo'))
c.addCapability('foo')
self.failUnless(c._checkCapability('foo'))
c.removeCapability('foo')
self.failIf(c._checkCapability('foo'))
def testDefaultCapability(self):
c = ircdb.IrcChannel()
c.setDefaultCapability(False)
self.failIf(c._checkCapability('foo'))
self.failUnless(c._checkCapability('-foo'))
c.setDefaultCapability(True)
self.failUnless(c._checkCapability('foo'))
self.failIf(c._checkCapability('-foo'))
def testLobotomized(self):
c = ircdb.IrcChannel(lobotomized=True)
self.failUnless(c.checkIgnored('foo!bar@baz'))
def testIgnored(self):
prefix = 'foo!bar@baz'
banmask = ircutils.banmask(prefix)
c = ircdb.IrcChannel()
self.failIf(c.checkIgnored(prefix))
c.addIgnore(banmask)
self.failUnless(c.checkIgnored(prefix))
c.removeIgnore(banmask)
self.failIf(c.checkIgnored(prefix))
c.addBan(banmask)
self.failUnless(c.checkIgnored(prefix))
c.removeBan(banmask)
self.failIf(c.checkIgnored(prefix))
class UsersDictionaryTestCase(IrcdbTestCase):
filename = os.path.join(conf.supybot.directories.conf(),
'UsersDictionaryTestCase.conf')
def setUp(self):
try:
os.remove(self.filename)
except:
pass
self.users = ircdb.UsersDictionary()
IrcdbTestCase.setUp(self)
def testIterAndNumUsers(self):
self.assertEqual(self.users.numUsers(), 0)
u = self.users.newUser()
hostmask = 'foo!xyzzy@baz.domain.com'
banmask = ircutils.banmask(hostmask)
u.addHostmask(banmask)
u.name = 'foo'
self.users.setUser(u)
self.assertEqual(self.users.numUsers(), 1)
u = self.users.newUser()
hostmask = 'biff!fladksfj@blakjdsf'
banmask = ircutils.banmask(hostmask)
u.addHostmask(banmask)
u.name = 'biff'
self.users.setUser(u)
self.assertEqual(self.users.numUsers(), 2)
self.users.delUser(2)
self.assertEqual(self.users.numUsers(), 1)
self.users.delUser(1)
self.assertEqual(self.users.numUsers(), 0)
def testGetSetDelUser(self):
self.assertRaises(KeyError, self.users.getUser, 'foo')
self.assertRaises(KeyError,
self.users.getUser, 'foo!xyzzy@baz.domain.com')
u = self.users.newUser()
hostmask = 'foo!xyzzy@baz.domain.com'
banmask = ircutils.banmask(hostmask)
u.addHostmask(banmask)
u.addHostmask(hostmask)
u.name = 'foo'
self.users.setUser(u)
self.assertEqual(self.users.getUser('foo'), u)
self.assertEqual(self.users.getUser('FOO'), u)
self.assertEqual(self.users.getUser(hostmask), u)
self.assertEqual(self.users.getUser(banmask), u)
# The UsersDictionary shouldn't allow users to be added whose hostmasks
# match another user's already in the database.
u2 = self.users.newUser()
u2.addHostmask('*!xyzzy@baz.domain.c?m')
self.assertRaises(ValueError, self.users.setUser, u2)
class CheckCapabilityTestCase(IrcdbTestCase):
filename = os.path.join(conf.supybot.directories.conf(),
'CheckCapabilityTestCase.conf')
owner = 'owner!owner@owner'
nothing = 'nothing!nothing@nothing'
justfoo = 'justfoo!justfoo@justfoo'
antifoo = 'antifoo!antifoo@antifoo'
justchanfoo = 'justchanfoo!justchanfoo@justchanfoo'
antichanfoo = 'antichanfoo!antichanfoo@antichanfoo'
securefoo = 'securefoo!securefoo@securefoo'
channel = '#channel'
cap = 'foo'
anticap = ircdb.makeAntiCapability(cap)
chancap = ircdb.makeChannelCapability(channel, cap)
antichancap = ircdb.makeAntiCapability(chancap)
chanop = ircdb.makeChannelCapability(channel, 'op')
channelnothing = ircdb.IrcChannel()
channelcap = ircdb.IrcChannel()
channelcap.addCapability(cap)
channelanticap = ircdb.IrcChannel()
channelanticap.addCapability(anticap)
def setUp(self):
IrcdbTestCase.setUp(self)
try:
os.remove(self.filename)
except:
pass
self.users = ircdb.UsersDictionary()
#self.users.open(self.filename)
self.channels = ircdb.ChannelsDictionary()
#self.channels.open(self.filename)
owner = self.users.newUser()
owner.name = 'owner'
owner.addCapability('owner')
owner.addHostmask(self.owner)
self.users.setUser(owner)
nothing = self.users.newUser()
nothing.name = 'nothing'
nothing.addHostmask(self.nothing)
self.users.setUser(nothing)
justfoo = self.users.newUser()
justfoo.name = 'justfoo'
justfoo.addCapability(self.cap)
justfoo.addHostmask(self.justfoo)
self.users.setUser(justfoo)
antifoo = self.users.newUser()
antifoo.name = 'antifoo'
antifoo.addCapability(self.anticap)
antifoo.addHostmask(self.antifoo)
self.users.setUser(antifoo)
justchanfoo = self.users.newUser()
justchanfoo.name = 'justchanfoo'
justchanfoo.addCapability(self.chancap)
justchanfoo.addHostmask(self.justchanfoo)
self.users.setUser(justchanfoo)
antichanfoo = self.users.newUser()
antichanfoo.name = 'antichanfoo'
antichanfoo.addCapability(self.antichancap)
antichanfoo.addHostmask(self.antichanfoo)
self.users.setUser(antichanfoo)
securefoo = self.users.newUser()
securefoo.name = 'securefoo'
securefoo.addCapability(self.cap)
securefoo.secure = True
securefoo.addHostmask(self.securefoo)
self.users.setUser(securefoo)
channel = ircdb.IrcChannel()
self.channels.setChannel(self.channel, channel)
def checkCapability(self, hostmask, capability):
return ircdb.checkCapability(hostmask, capability,
self.users, self.channels)
def testOwner(self):
self.failUnless(self.checkCapability(self.owner, self.cap))
self.failIf(self.checkCapability(self.owner, self.anticap))
self.failUnless(self.checkCapability(self.owner, self.chancap))
self.failIf(self.checkCapability(self.owner, self.antichancap))
self.channels.setChannel(self.channel, self.channelanticap)
self.failUnless(self.checkCapability(self.owner, self.cap))
self.failIf(self.checkCapability(self.owner, self.anticap))
def testNothingAgainstChannel(self):
self.channels.setChannel(self.channel, self.channelnothing)
self.assertEqual(self.checkCapability(self.nothing, self.chancap),
self.channelnothing.defaultAllow)
self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow
self.channels.setChannel(self.channel, self.channelnothing)
self.assertEqual(self.checkCapability(self.nothing, self.chancap),
self.channelnothing.defaultAllow)
self.channels.setChannel(self.channel, self.channelcap)
self.failUnless(self.checkCapability(self.nothing, self.chancap))
self.failIf(self.checkCapability(self.nothing, self.antichancap))
self.channels.setChannel(self.channel, self.channelanticap)
self.failIf(self.checkCapability(self.nothing, self.chancap))
self.failUnless(self.checkCapability(self.nothing, self.antichancap))
def testNothing(self):
self.assertEqual(self.checkCapability(self.nothing, self.cap),
conf.supybot.capabilities.default())
self.assertEqual(self.checkCapability(self.nothing, self.anticap),
not conf.supybot.capabilities.default())
def testJustFoo(self):
self.failUnless(self.checkCapability(self.justfoo, self.cap))
self.failIf(self.checkCapability(self.justfoo, self.anticap))
def testAntiFoo(self):
self.failUnless(self.checkCapability(self.antifoo, self.anticap))
self.failIf(self.checkCapability(self.antifoo, self.cap))
def testJustChanFoo(self):
self.channels.setChannel(self.channel, self.channelnothing)
self.failUnless(self.checkCapability(self.justchanfoo, self.chancap))
self.failIf(self.checkCapability(self.justchanfoo, self.antichancap))
self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow
self.failUnless(self.checkCapability(self.justchanfoo, self.chancap))
self.failIf(self.checkCapability(self.justchanfoo, self.antichancap))
self.channels.setChannel(self.channel, self.channelanticap)
self.failUnless(self.checkCapability(self.justchanfoo, self.chancap))
self.failIf(self.checkCapability(self.justchanfoo, self.antichancap))
def testChanOpCountsAsEverything(self):
self.channels.setChannel(self.channel, self.channelanticap)
id = self.users.getUserId('nothing')
u = self.users.getUser(id)
u.addCapability(self.chanop)
self.users.setUser(u)
self.failUnless(self.checkCapability(self.nothing, self.chancap))
self.channels.setChannel(self.channel, self.channelnothing)
self.failUnless(self.checkCapability(self.nothing, self.chancap))
self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow
self.failUnless(self.checkCapability(self.nothing, self.chancap))
def testAntiChanFoo(self):
self.channels.setChannel(self.channel, self.channelnothing)
self.failIf(self.checkCapability(self.antichanfoo, self.chancap))
self.failUnless(self.checkCapability(self.antichanfoo,
self.antichancap))
def testSecurefoo(self):
self.failUnless(self.checkCapability(self.securefoo, self.cap))
id = self.users.getUserId(self.securefoo)
u = self.users.getUser(id)
u.addAuth(self.securefoo)
self.users.setUser(u)
try:
originalConfDefaultAllow = conf.supybot.capabilities.default()
conf.supybot.capabilities.default.set('False')
self.failIf(self.checkCapability('a' + self.securefoo, self.cap))
finally:
conf.supybot.capabilities.default.set(str(originalConfDefaultAllow))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

457
test/test_irclib.py Normal file
View File

@ -0,0 +1,457 @@
###
# 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 testsupport import *
import copy
import pickle
import supybot.conf as conf
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
class IrcMsgQueueTestCase(SupyTestCase):
mode = ircmsgs.op('#foo', 'jemfinch')
msg = ircmsgs.privmsg('#foo', 'hey, you')
msgs = [ircmsgs.privmsg('#foo', str(i)) for i in range(10)]
kick = ircmsgs.kick('#foo', 'PeterB')
pong = ircmsgs.pong('123')
ping = ircmsgs.ping('123')
topic = ircmsgs.topic('#foo')
notice = ircmsgs.notice('jemfinch', 'supybot here')
join = ircmsgs.join('#foo')
who = ircmsgs.who('#foo')
def testInit(self):
q = irclib.IrcMsgQueue([self.msg, self.topic, self.ping])
self.assertEqual(len(q), 3)
def testLen(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.msg)
self.assertEqual(len(q), 1)
q.enqueue(self.mode)
self.assertEqual(len(q), 2)
q.enqueue(self.kick)
self.assertEqual(len(q), 3)
q.enqueue(self.topic)
self.assertEqual(len(q), 4)
q.dequeue()
self.assertEqual(len(q), 3)
q.dequeue()
self.assertEqual(len(q), 2)
q.dequeue()
self.assertEqual(len(q), 1)
q.dequeue()
self.assertEqual(len(q), 0)
def testRepr(self):
q = irclib.IrcMsgQueue()
self.assertEqual(repr(q), 'IrcMsgQueue([])')
q.enqueue(self.msg)
try:
repr(q)
except Exception, e:
self.fail('repr(q) raised an exception: %s' % utils.exnToString(e))
def testEmpty(self):
q = irclib.IrcMsgQueue()
self.failIf(q)
def testEnqueueDequeue(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.msg)
self.failUnless(q)
self.assertEqual(self.msg, q.dequeue())
self.failIf(q)
q.enqueue(self.msg)
q.enqueue(self.notice)
self.assertEqual(self.msg, q.dequeue())
self.assertEqual(self.notice, q.dequeue())
for msg in self.msgs:
q.enqueue(msg)
for msg in self.msgs:
self.assertEqual(msg, q.dequeue())
def testPrioritizing(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.msg)
q.enqueue(self.mode)
self.assertEqual(self.mode, q.dequeue())
self.assertEqual(self.msg, q.dequeue())
q.enqueue(self.msg)
q.enqueue(self.kick)
self.assertEqual(self.kick, q.dequeue())
self.assertEqual(self.msg, q.dequeue())
q.enqueue(self.ping)
q.enqueue(self.msgs[0])
q.enqueue(self.kick)
q.enqueue(self.msgs[1])
q.enqueue(self.mode)
self.assertEqual(self.kick, q.dequeue())
self.assertEqual(self.mode, q.dequeue())
self.assertEqual(self.ping, q.dequeue())
self.assertEqual(self.msgs[0], q.dequeue())
self.assertEqual(self.msgs[1], q.dequeue())
def testNoIdenticals(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.msg)
q.enqueue(self.msg)
self.assertEqual(self.msg, q.dequeue())
self.failIf(q)
def testJoinBeforeWho(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.join)
q.enqueue(self.who)
self.assertEqual(self.join, q.dequeue())
self.assertEqual(self.who, q.dequeue())
q.enqueue(self.who)
q.enqueue(self.join)
self.assertEqual(self.join, q.dequeue())
self.assertEqual(self.who, q.dequeue())
def testTopicBeforePrivmsg(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.msg)
q.enqueue(self.topic)
self.assertEqual(self.topic, q.dequeue())
self.assertEqual(self.msg, q.dequeue())
def testModeBeforePrivmsg(self):
q = irclib.IrcMsgQueue()
q.enqueue(self.msg)
q.enqueue(self.mode)
self.assertEqual(self.mode, q.dequeue())
self.assertEqual(self.msg, q.dequeue())
q.enqueue(self.mode)
q.enqueue(self.msg)
self.assertEqual(self.mode, q.dequeue())
self.assertEqual(self.msg, q.dequeue())
class ChannelStateTestCase(SupyTestCase):
def testPickleCopy(self):
c = irclib.ChannelState()
self.assertEqual(pickle.loads(pickle.dumps(c)), c)
c.addUser('jemfinch')
c1 = pickle.loads(pickle.dumps(c))
self.assertEqual(c, c1)
c.removeUser('jemfinch')
self.failIf('jemfinch' in c.users)
self.failUnless('jemfinch' in c1.users)
def testCopy(self):
c = irclib.ChannelState()
c.addUser('jemfinch')
c1 = copy.deepcopy(c)
c.removeUser('jemfinch')
self.failIf('jemfinch' in c.users)
self.failUnless('jemfinch' in c1.users)
def testAddUser(self):
c = irclib.ChannelState()
c.addUser('foo')
self.failUnless('foo' in c.users)
self.failIf('foo' in c.ops)
self.failIf('foo' in c.voices)
self.failIf('foo' in c.halfops)
c.addUser('+bar')
self.failUnless('bar' in c.users)
self.failUnless('bar' in c.voices)
self.failIf('bar' in c.ops)
self.failIf('bar' in c.halfops)
c.addUser('%baz')
self.failUnless('baz' in c.users)
self.failUnless('baz' in c.halfops)
self.failIf('baz' in c.voices)
self.failIf('baz' in c.ops)
c.addUser('@quuz')
self.failUnless('quuz' in c.users)
self.failUnless('quuz' in c.ops)
self.failIf('quuz' in c.halfops)
self.failIf('quuz' in c.voices)
class IrcStateTestCase(SupyTestCase):
class FakeIrc:
nick = 'nick'
prefix = 'nick!user@host'
irc = FakeIrc()
def testAddMsgRemovesOpsProperly(self):
st = irclib.IrcState()
st.channels['#foo'] = irclib.ChannelState()
st.channels['#foo'].ops.add('bar')
m = ircmsgs.mode('#foo', ('-o', 'bar'))
st.addMsg(self.irc, m)
self.failIf('bar' in st.channels['#foo'].ops)
def testNickChangesChangeChannelUsers(self):
st = irclib.IrcState()
st.channels['#foo'] = irclib.ChannelState()
st.channels['#foo'].addUser('@bar')
self.failUnless('bar' in st.channels['#foo'].users)
self.failUnless(st.channels['#foo'].isOp('bar'))
st.addMsg(self.irc, ircmsgs.IrcMsg(':bar!asfd@asdf.com NICK baz'))
self.failIf('bar' in st.channels['#foo'].users)
self.failIf(st.channels['#foo'].isOp('bar'))
self.failUnless('baz' in st.channels['#foo'].users)
self.failUnless(st.channels['#foo'].isOp('baz'))
def testHistory(self):
oldconfmaxhistory = conf.supybot.protocols.irc.maxHistoryLength()
conf.supybot.protocols.irc.maxHistoryLength.setValue(10)
state = irclib.IrcState()
for msg in msgs:
try:
state.addMsg(self.irc, msg)
except Exception:
pass
self.failIf(len(state.history) >
conf.supybot.protocols.irc.maxHistoryLength())
self.assertEqual(len(state.history),
conf.supybot.protocols.irc.maxHistoryLength())
self.assertEqual(list(state.history),
msgs[len(msgs) -
conf.supybot.protocols.irc.maxHistoryLength():])
conf.supybot.protocols.irc.maxHistoryLength.setValue(oldconfmaxhistory)
def testWasteland005(self):
state = irclib.IrcState()
# Here we're testing if PREFIX works without the (ov) there.
state.addMsg(self.irc, ircmsgs.IrcMsg(':desolate.wasteland.org 005 jemfinch NOQUIT WATCH=128 SAFELIST MODES=6 MAXCHANNELS=10 MAXBANS=100 NICKLEN=30 TOPICLEN=307 KICKLEN=307 CHANTYPES=&# PREFIX=@+ NETWORK=DALnet SILENCE=10 :are available on this server'))
self.assertEqual(state.supported['prefix']['o'], '@')
self.assertEqual(state.supported['prefix']['v'], '+')
def testEmptyTopic(self):
state = irclib.IrcState()
state.addMsg(self.irc, ircmsgs.topic('#foo'))
def testPickleCopy(self):
state = irclib.IrcState()
self.assertEqual(state, pickle.loads(pickle.dumps(state)))
for msg in msgs:
try:
state.addMsg(self.irc, msg)
except Exception:
pass
self.assertEqual(state, pickle.loads(pickle.dumps(state)))
def testCopy(self):
state = irclib.IrcState()
self.assertEqual(state, state.copy())
for msg in msgs:
try:
state.addMsg(self.irc, msg)
except Exception:
pass
self.assertEqual(state, state.copy())
def testCopyCopiesChannels(self):
state = irclib.IrcState()
stateCopy = state.copy()
state.channels['#foo'] = None
self.failIf('#foo' in stateCopy.channels)
def testJoin(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix))
self.failUnless('#foo' in st.channels)
self.failUnless(self.irc.nick in st.channels['#foo'].users)
st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz'))
self.failUnless('foo' in st.channels['#foo'].users)
st2 = st.copy()
st.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz'))
self.failIf('foo' in st.channels['#foo'].users)
self.failUnless('foo' in st2.channels['#foo'].users)
def testEq(self):
state1 = irclib.IrcState()
state2 = irclib.IrcState()
self.assertEqual(state1, state2)
for msg in msgs:
try:
state1.addMsg(self.irc, msg)
state2.addMsg(self.irc, msg)
self.assertEqual(state1, state2)
except Exception:
pass
def testHandlesModes(self):
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix))
self.failIf('bar' in st.channels['#foo'].ops)
st.addMsg(self.irc, ircmsgs.op('#foo', 'bar'))
self.failUnless('bar' in st.channels['#foo'].ops)
st.addMsg(self.irc, ircmsgs.deop('#foo', 'bar'))
self.failIf('bar' in st.channels['#foo'].ops)
self.failIf('bar' in st.channels['#foo'].voices)
st.addMsg(self.irc, ircmsgs.voice('#foo', 'bar'))
self.failUnless('bar' in st.channels['#foo'].voices)
st.addMsg(self.irc, ircmsgs.devoice('#foo', 'bar'))
self.failIf('bar' in st.channels['#foo'].voices)
self.failIf('bar' in st.channels['#foo'].halfops)
st.addMsg(self.irc, ircmsgs.halfop('#foo', 'bar'))
self.failUnless('bar' in st.channels['#foo'].halfops)
st.addMsg(self.irc, ircmsgs.dehalfop('#foo', 'bar'))
self.failIf('bar' in st.channels['#foo'].halfops)
def testDoModeOnlyChannels(self):
st = irclib.IrcState()
self.assert_(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1)
class IrcTestCase(SupyTestCase):
def setUp(self):
self.irc = irclib.Irc('test')
_ = self.irc.takeMsg() # NICK
_ = self.irc.takeMsg() # USER
def testPingResponse(self):
self.irc.feedMsg(ircmsgs.ping('123'))
self.assertEqual(ircmsgs.pong('123'), self.irc.takeMsg())
def test433Response(self):
# This is necessary; it won't change nick if irc.originalName==irc.nick
self.irc.nick = 'somethingElse'
self.irc.feedMsg(ircmsgs.IrcMsg('433 * %s :Nickname already in use.' %\
self.irc.nick))
msg = self.irc.takeMsg()
self.failUnless(msg.command == 'NICK' and msg.args[0] != self.irc.nick)
self.irc.feedMsg(ircmsgs.IrcMsg('433 * %s :Nickname already in use.' %\
self.irc.nick))
msg = self.irc.takeMsg()
self.failUnless(msg.command == 'NICK' and msg.args[0] != self.irc.nick)
def testSendBeforeQueue(self):
while self.irc.takeMsg() is not None:
self.irc.takeMsg()
self.irc.queueMsg(ircmsgs.IrcMsg('NOTICE #foo bar'))
self.irc.sendMsg(ircmsgs.IrcMsg('PRIVMSG #foo yeah!'))
msg = self.irc.takeMsg()
self.failUnless(msg.command == 'PRIVMSG')
msg = self.irc.takeMsg()
self.failUnless(msg.command == 'NOTICE')
def testNoMsgLongerThan512(self):
self.irc.queueMsg(ircmsgs.privmsg('whocares', 'x'*1000))
msg = self.irc.takeMsg()
self.failUnless(len(msg) <= 512, 'len(msg) was %s' % len(msg))
def testReset(self):
for msg in msgs:
try:
self.irc.feedMsg(msg)
except:
pass
self.irc.reset()
self.failIf(self.irc.fastqueue)
self.failIf(self.irc.state.history)
self.failIf(self.irc.state.channels)
self.failIf(self.irc.outstandingPing)
def testHistory(self):
self.irc.reset()
msg1 = ircmsgs.IrcMsg('PRIVMSG #linux :foo bar baz!')
self.irc.feedMsg(msg1)
self.assertEqual(self.irc.state.history[0], msg1)
msg2 = ircmsgs.IrcMsg('JOIN #sourcereview')
self.irc.feedMsg(msg2)
self.assertEqual(list(self.irc.state.history), [msg1, msg2])
class IrcCallbackTestCase(SupyTestCase):
class FakeIrc:
pass
irc = FakeIrc()
def testName(self):
class UnnamedIrcCallback(irclib.IrcCallback):
pass
unnamed = UnnamedIrcCallback()
class NamedIrcCallback(irclib.IrcCallback):
myName = 'foobar'
def name(self):
return self.myName
named = NamedIrcCallback()
self.assertEqual(unnamed.name(), unnamed.__class__.__name__)
self.assertEqual(named.name(), named.myName)
def testDoCommand(self):
def makeCommand(msg):
return 'do' + msg.command.capitalize()
class DoCommandCatcher(irclib.IrcCallback):
def __init__(self):
self.L = []
def __getattr__(self, attr):
self.L.append(attr)
return lambda *args: None
doCommandCatcher = DoCommandCatcher()
for msg in msgs:
doCommandCatcher(self.irc, msg)
commands = map(makeCommand, msgs)
self.assertEqual(doCommandCatcher.L, commands)
def testFirstCommands(self):
try:
originalNick = conf.supybot.nick()
originalUser = conf.supybot.user()
originalPassword = conf.supybot.networks.test.password()
nick = 'nick'
conf.supybot.nick.setValue(nick)
user = 'user any user'
conf.supybot.user.setValue(user)
expected = [ircmsgs.nick(nick), ircmsgs.user('supybot', user)]
irc = irclib.Irc('test')
msgs = [irc.takeMsg()]
while msgs[-1] != None:
msgs.append(irc.takeMsg())
msgs.pop()
self.assertEqual(msgs, expected)
password = 'password'
conf.supybot.networks.test.password.setValue(password)
irc = irclib.Irc('test')
msgs = [irc.takeMsg()]
while msgs[-1] != None:
msgs.append(irc.takeMsg())
msgs.pop()
expected.insert(0, ircmsgs.password(password))
self.assertEqual(msgs, expected)
finally:
conf.supybot.nick.setValue(nick)
conf.supybot.user.setValue(user)
conf.supybot.networks.test.password.setValue(password)
conf.supybot.nick.setValue(nick)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

240
test/test_ircmsgs.py Normal file
View File

@ -0,0 +1,240 @@
###
# 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 testsupport import *
import copy
import pickle
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
class IrcMsgTestCase(SupyTestCase):
def testLen(self):
for msg in msgs:
if msg.prefix:
strmsg = str(msg)
self.failIf(len(msg) != len(strmsg) and \
strmsg.replace(':', '') == strmsg)
def testRepr(self):
IrcMsg = ircmsgs.IrcMsg
ignore(IrcMsg) # Make pychecker happy.
for msg in msgs:
self.assertEqual(msg, eval(repr(msg)))
def testStr(self):
for (rawmsg, msg) in zip(rawmsgs, msgs):
strmsg = str(msg).strip()
self.failIf(rawmsg != strmsg and \
strmsg.replace(':', '') == strmsg)
def testEq(self):
for msg in msgs:
self.assertEqual(msg, msg)
self.failIf(msgs[0] == []) # Comparison to unhashable type.
def testNe(self):
for msg in msgs:
self.failIf(msg != msg)
## def testImmutability(self):
## s = 'something else'
## t = ('foo', 'bar', 'baz')
## for msg in msgs:
## self.assertRaises(AttributeError, setattr, msg, 'prefix', s)
## self.assertRaises(AttributeError, setattr, msg, 'nick', s)
## self.assertRaises(AttributeError, setattr, msg, 'user', s)
## self.assertRaises(AttributeError, setattr, msg, 'host', s)
## self.assertRaises(AttributeError, setattr, msg, 'command', s)
## self.assertRaises(AttributeError, setattr, msg, 'args', t)
## if msg.args:
## def setArgs(msg):
## msg.args[0] = s
## self.assertRaises(TypeError, setArgs, msg)
def testInit(self):
for msg in msgs:
self.assertEqual(msg, ircmsgs.IrcMsg(prefix=msg.prefix,
command=msg.command,
args=msg.args))
self.assertEqual(msg, ircmsgs.IrcMsg(msg=msg))
self.assertRaises(ValueError,
ircmsgs.IrcMsg,
args=('foo', 'bar'),
prefix='foo!bar@baz')
def testPickleCopy(self):
for msg in msgs:
self.assertEqual(msg, pickle.loads(pickle.dumps(msg)))
self.assertEqual(msg, copy.copy(msg))
def testHashNotZero(self):
zeroes = 0
for msg in msgs:
if hash(msg) == 0:
zeroes += 1
self.failIf(zeroes > (len(msgs)/10), 'Too many zero hashes.')
def testMsgKeywordHandledProperly(self):
msg = ircmsgs.notice('foo', 'bar')
msg2 = ircmsgs.IrcMsg(msg=msg, command='PRIVMSG')
self.assertEqual(msg2.command, 'PRIVMSG')
self.assertEqual(msg2.args, msg.args)
def testMalformedIrcMsgRaised(self):
self.assertRaises(ircmsgs.MalformedIrcMsg, ircmsgs.IrcMsg, ':foo')
self.assertRaises(ircmsgs.MalformedIrcMsg, ircmsgs.IrcMsg,
args=('biff',), prefix='foo!bar@baz')
def testTags(self):
m = ircmsgs.privmsg('foo', 'bar')
self.failIf(m.repliedTo)
m.tag('repliedTo')
self.failUnless(m.repliedTo)
m.tag('repliedTo')
self.failUnless(m.repliedTo)
m.tag('repliedTo', 12)
self.assertEqual(m.repliedTo, 12)
class FunctionsTestCase(SupyTestCase):
def testIsAction(self):
L = [':jemfinch!~jfincher@ts26-2.homenet.ohio-state.edu PRIVMSG'
' #sourcereview :ACTION does something',
':supybot!~supybot@underthemain.net PRIVMSG #sourcereview '
':ACTION beats angryman senseless with a Unix manual (#2)',
':supybot!~supybot@underthemain.net PRIVMSG #sourcereview '
':ACTION beats ang senseless with a 50lb Unix manual (#2)',
':supybot!~supybot@underthemain.net PRIVMSG #sourcereview '
':ACTION resizes angryman\'s terminal to 40x24 (#16)']
msgs = map(ircmsgs.IrcMsg, L)
for msg in msgs:
self.failUnless(ircmsgs.isAction(msg))
def testIsActionIsntStupid(self):
m = ircmsgs.privmsg('#x', '\x01NOTANACTION foo\x01')
self.failIf(ircmsgs.isAction(m))
m = ircmsgs.privmsg('#x', '\x01ACTION foo bar\x01')
self.failUnless(ircmsgs.isAction(m))
def testIsCtcp(self):
self.failUnless(ircmsgs.isCtcp(ircmsgs.privmsg('foo',
'\x01VERSION\x01')))
self.failIf(ircmsgs.isCtcp(ircmsgs.privmsg('foo', '\x01')))
def testIsActionFalseWhenNoSpaces(self):
msg = ircmsgs.IrcMsg('PRIVMSG #foo :\x01ACTIONfoobar\x01')
self.failIf(ircmsgs.isAction(msg))
def testUnAction(self):
s = 'foo bar baz'
msg = ircmsgs.action('#foo', s)
self.assertEqual(ircmsgs.unAction(msg), s)
def testBan(self):
channel = '#osu'
ban = '*!*@*.edu'
exception = '*!*@*ohio-state.edu'
noException = ircmsgs.ban(channel, ban)
self.assertEqual(ircutils.separateModes(noException.args[1:]),
[('+b', ban)])
withException = ircmsgs.ban(channel, ban, exception)
self.assertEqual(ircutils.separateModes(withException.args[1:]),
[('+b', ban), ('+e', exception)])
def testBans(self):
channel = '#osu'
bans = ['*!*@*', 'jemfinch!*@*']
exceptions = ['*!*@*ohio-state.edu']
noException = ircmsgs.bans(channel, bans)
self.assertEqual(ircutils.separateModes(noException.args[1:]),
[('+b', bans[0]), ('+b', bans[1])])
withExceptions = ircmsgs.bans(channel, bans, exceptions)
self.assertEqual(ircutils.separateModes(withExceptions.args[1:]),
[('+b', bans[0]), ('+b', bans[1]),
('+e', exceptions[0])])
def testUnban(self):
channel = '#supybot'
ban = 'foo!bar@baz'
self.assertEqual(str(ircmsgs.unban(channel, ban)),
'MODE %s -b :%s\r\n' % (channel, ban))
def testJoin(self):
channel = '#osu'
key = 'michiganSucks'
self.assertEqual(ircmsgs.join(channel).args, ('#osu',))
self.assertEqual(ircmsgs.join(channel, key).args,
('#osu', 'michiganSucks'))
def testJoins(self):
channels = ['#osu', '#umich']
keys = ['michiganSucks', 'osuSucks']
self.assertEqual(ircmsgs.joins(channels).args, ('#osu,#umich',))
self.assertEqual(ircmsgs.joins(channels, keys).args,
('#osu,#umich', 'michiganSucks,osuSucks'))
keys.pop()
self.assertEqual(ircmsgs.joins(channels, keys).args,
('#osu,#umich', 'michiganSucks'))
def testQuit(self):
self.failUnless(ircmsgs.quit(prefix='foo!bar@baz'))
def testOps(self):
m = ircmsgs.ops('#foo', ['foo', 'bar', 'baz'])
self.assertEqual(str(m), 'MODE #foo +ooo foo bar :baz\r\n')
def testDeops(self):
m = ircmsgs.deops('#foo', ['foo', 'bar', 'baz'])
self.assertEqual(str(m), 'MODE #foo -ooo foo bar :baz\r\n')
def testVoices(self):
m = ircmsgs.voices('#foo', ['foo', 'bar', 'baz'])
self.assertEqual(str(m), 'MODE #foo +vvv foo bar :baz\r\n')
def testDevoices(self):
m = ircmsgs.devoices('#foo', ['foo', 'bar', 'baz'])
self.assertEqual(str(m), 'MODE #foo -vvv foo bar :baz\r\n')
def testHalfops(self):
m = ircmsgs.halfops('#foo', ['foo', 'bar', 'baz'])
self.assertEqual(str(m), 'MODE #foo +hhh foo bar :baz\r\n')
def testDehalfops(self):
m = ircmsgs.dehalfops('#foo', ['foo', 'bar', 'baz'])
self.assertEqual(str(m), 'MODE #foo -hhh foo bar :baz\r\n')
def testMode(self):
m = ircmsgs.mode('#foo', ('-b', 'foo!bar@baz'))
s = str(m)
self.assertEqual(s, 'MODE #foo -b :foo!bar@baz\r\n')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

354
test/test_ircutils.py Normal file
View File

@ -0,0 +1,354 @@
###
# 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 testsupport import *
import copy
import random
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
class FunctionsTestCase(SupyTestCase):
hostmask = 'foo!bar@baz'
def testHostmaskPatternEqual(self):
for msg in msgs:
if msg.prefix and ircutils.isUserHostmask(msg.prefix):
s = msg.prefix
self.failUnless(ircutils.hostmaskPatternEqual(s, s),
'%r did not match itself.' % s)
banmask = ircutils.banmask(s)
self.failUnless(ircutils.hostmaskPatternEqual(banmask, s),
'%r did not match %r' % (s, banmask))
s = 'supybot!~supybot@dhcp065-024-075-056.columbus.rr.com'
self.failUnless(ircutils.hostmaskPatternEqual(s, s))
s = 'jamessan|work!~jamessan@209-6-166-196.c3-0.' \
'abr-ubr1.sbo-abr.ma.cable.rcn.com'
self.failUnless(ircutils.hostmaskPatternEqual(s, s))
def testIsUserHostmask(self):
self.failUnless(ircutils.isUserHostmask(self.hostmask))
self.failUnless(ircutils.isUserHostmask('a!b@c'))
self.failIf(ircutils.isUserHostmask('!bar@baz'))
self.failIf(ircutils.isUserHostmask('!@baz'))
self.failIf(ircutils.isUserHostmask('!bar@'))
self.failIf(ircutils.isUserHostmask('!@'))
self.failIf(ircutils.isUserHostmask('foo!@baz'))
self.failIf(ircutils.isUserHostmask('foo!bar@'))
self.failIf(ircutils.isUserHostmask(''))
self.failIf(ircutils.isUserHostmask('!'))
self.failIf(ircutils.isUserHostmask('@'))
self.failIf(ircutils.isUserHostmask('!bar@baz'))
def testIsChannel(self):
self.failUnless(ircutils.isChannel('#'))
self.failUnless(ircutils.isChannel('&'))
self.failUnless(ircutils.isChannel('+'))
self.failUnless(ircutils.isChannel('!'))
self.failUnless(ircutils.isChannel('#foo'))
self.failUnless(ircutils.isChannel('&foo'))
self.failUnless(ircutils.isChannel('+foo'))
self.failUnless(ircutils.isChannel('!foo'))
self.failIf(ircutils.isChannel('#foo bar'))
self.failIf(ircutils.isChannel('#foo,bar'))
self.failIf(ircutils.isChannel('#foobar\x07'))
self.failIf(ircutils.isChannel('foo'))
self.failIf(ircutils.isChannel(''))
def testBold(self):
s = ircutils.bold('foo')
self.assertEqual(s[0], '\x02')
self.assertEqual(s[-1], '\x02')
def testUnderline(self):
s = ircutils.underline('foo')
self.assertEqual(s[0], '\x1f')
self.assertEqual(s[-1], '\x1f')
def testReverse(self):
s = ircutils.reverse('foo')
self.assertEqual(s[0], '\x16')
self.assertEqual(s[-1], '\x16')
def testMircColor(self):
# No colors provided should return the same string
s = 'foo'
self.assertEqual(s, ircutils.mircColor(s))
# Test positional args
self.assertEqual('\x0300foo\x03', ircutils.mircColor(s, 'white'))
self.assertEqual('\x031,02foo\x03',ircutils.mircColor(s,'black','blue'))
self.assertEqual('\x03,03foo\x03', ircutils.mircColor(s, None, 'green'))
# Test keyword args
self.assertEqual('\x0304foo\x03', ircutils.mircColor(s, fg='red'))
self.assertEqual('\x03,05foo\x03', ircutils.mircColor(s, bg='brown'))
self.assertEqual('\x036,07foo\x03',
ircutils.mircColor(s, bg='orange', fg='purple'))
# Commented out because we don't map numbers to colors anymore.
## def testMircColors(self):
## # Make sure all (k, v) pairs are also (v, k) pairs.
## for (k, v) in ircutils.mircColors.items():
## if k:
## self.assertEqual(ircutils.mircColors[v], k)
def testStripBold(self):
self.assertEqual(ircutils.stripBold(ircutils.bold('foo')), 'foo')
def testStripColor(self):
self.assertEqual(ircutils.stripColor('\x02bold\x0302,04foo\x03bar\x0f'),
'\x02boldfoobar\x0f')
self.assertEqual(ircutils.stripColor('\x03foo\x03'), 'foo')
self.assertEqual(ircutils.stripColor('\x03foo\x0F'), 'foo\x0F')
self.assertEqual(ircutils.stripColor('\x0312foo\x03'), 'foo')
self.assertEqual(ircutils.stripColor('\x0312,14foo\x03'), 'foo')
self.assertEqual(ircutils.stripColor('\x03,14foo\x03'), 'foo')
self.assertEqual(ircutils.stripColor('\x03,foo\x03'), ',foo')
self.assertEqual(ircutils.stripColor('\x0312foo\x0F'), 'foo\x0F')
self.assertEqual(ircutils.stripColor('\x0312,14foo\x0F'), 'foo\x0F')
self.assertEqual(ircutils.stripColor('\x03,14foo\x0F'), 'foo\x0F')
self.assertEqual(ircutils.stripColor('\x03,foo\x0F'), ',foo\x0F')
def testStripReverse(self):
self.assertEqual(ircutils.stripReverse(ircutils.reverse('foo')), 'foo')
def testStripUnderline(self):
self.assertEqual(ircutils.stripUnderline(ircutils.underline('foo')),
'foo')
def testStripFormatting(self):
self.assertEqual(ircutils.stripFormatting(ircutils.bold('foo')), 'foo')
self.assertEqual(ircutils.stripFormatting(ircutils.reverse('foo')),
'foo')
self.assertEqual(ircutils.stripFormatting(ircutils.underline('foo')),
'foo')
self.assertEqual(ircutils.stripFormatting('\x02bold\x0302,04foo\x03'
'bar\x0f'),
'boldfoobar')
s = ircutils.mircColor('[', 'blue') + ircutils.bold('09:21')
self.assertEqual(ircutils.stripFormatting(s), '[09:21')
def testSafeArgument(self):
s = 'I have been running for 9 seconds'
bolds = ircutils.bold(s)
colors = ircutils.mircColor(s, 'pink', 'orange')
self.assertEqual(s, ircutils.safeArgument(s))
self.assertEqual(bolds, ircutils.safeArgument(bolds))
self.assertEqual(colors, ircutils.safeArgument(colors))
def testSafeArgumentConvertsToString(self):
self.assertEqual('1', ircutils.safeArgument(1))
self.assertEqual(str(None), ircutils.safeArgument(None))
def testIsNick(self):
try:
original = conf.supybot.protocols.irc.strictRfc()
conf.supybot.protocols.irc.strictRfc.setValue(True)
self.failUnless(ircutils.isNick('jemfinch'))
self.failUnless(ircutils.isNick('jemfinch0'))
self.failUnless(ircutils.isNick('[0]'))
self.failUnless(ircutils.isNick('{jemfinch}'))
self.failUnless(ircutils.isNick('[jemfinch]'))
self.failUnless(ircutils.isNick('jem|finch'))
self.failUnless(ircutils.isNick('\\```'))
self.failUnless(ircutils.isNick('`'))
self.failUnless(ircutils.isNick('A'))
self.failIf(ircutils.isNick(''))
self.failIf(ircutils.isNick('8foo'))
self.failIf(ircutils.isNick('10'))
conf.supybot.protocols.irc.strictRfc.setValue(False)
self.failUnless(ircutils.isNick('services@something.undernet.net'))
finally:
conf.supybot.protocols.irc.strictRfc.setValue(original)
def testIsNickNeverAllowsSpaces(self):
try:
original = conf.supybot.protocols.irc.strictRfc()
conf.supybot.protocols.irc.strictRfc.setValue(True)
self.failIf(ircutils.isNick('foo bar'))
conf.supybot.protocols.irc.strictRfc.setValue(False)
self.failIf(ircutils.isNick('foo bar'))
finally:
conf.supybot.protocols.irc.strictRfc.setValue(original)
def testBanmask(self):
for msg in msgs:
if ircutils.isUserHostmask(msg.prefix):
banmask = ircutils.banmask(msg.prefix)
self.failUnless(ircutils.hostmaskPatternEqual(banmask,
msg.prefix),
'%r didn\'t match %r' % (msg.prefix, banmask))
self.assertEqual(ircutils.banmask('foobar!user@host'), '*!*@host')
self.assertEqual(ircutils.banmask('foo!bar@2001::'), '*!*@2001::*')
def testSeparateModes(self):
self.assertEqual(ircutils.separateModes(['+ooo', 'x', 'y', 'z']),
[('+o', 'x'), ('+o', 'y'), ('+o', 'z')])
self.assertEqual(ircutils.separateModes(['+o-o', 'x', 'y']),
[('+o', 'x'), ('-o', 'y')])
self.assertEqual(ircutils.separateModes(['+s-o', 'x']),
[('+s', None), ('-o', 'x')])
self.assertEqual(ircutils.separateModes(['+sntl', '100']),
[('+s', None),('+n', None),('+t', None),('+l', 100)])
def testNickFromHostmask(self):
self.assertEqual(ircutils.nickFromHostmask('nick!user@host.domain.tld'),
'nick')
def testToLower(self):
self.assertEqual('jemfinch', ircutils.toLower('jemfinch'))
self.assertEqual('{}|^', ircutils.toLower('[]\\~'))
def testReplyTo(self):
prefix = 'foo!bar@baz'
channel = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix)
private = ircmsgs.privmsg('jemfinch', 'bar baz', prefix=prefix)
self.assertEqual(ircutils.replyTo(channel), channel.args[0])
self.assertEqual(ircutils.replyTo(private), private.nick)
def testJoinModes(self):
plusE = ('+e', '*!*@*ohio-state.edu')
plusB = ('+b', '*!*@*umich.edu')
minusL = ('-l', None)
modes = [plusB, plusE, minusL]
self.assertEqual(ircutils.joinModes(modes),
['+be-l', plusB[1], plusE[1]])
def testDccIpStuff(self):
def randomIP():
def rand():
return random.randrange(0, 256)
return '.'.join(map(str, [rand(), rand(), rand(), rand()]))
for _ in range(100): # 100 should be good :)
ip = randomIP()
self.assertEqual(ip, ircutils.unDccIP(ircutils.dccIP(ip)))
class IrcDictTestCase(SupyTestCase):
def test(self):
d = ircutils.IrcDict()
d['#FOO'] = 'bar'
self.assertEqual(d['#FOO'], 'bar')
self.assertEqual(d['#Foo'], 'bar')
self.assertEqual(d['#foo'], 'bar')
del d['#fOO']
d['jemfinch{}'] = 'bar'
self.assertEqual(d['jemfinch{}'], 'bar')
self.assertEqual(d['jemfinch[]'], 'bar')
self.assertEqual(d['JEMFINCH[]'], 'bar')
def testKeys(self):
d = ircutils.IrcDict()
self.assertEqual(d.keys(), [])
def testSetdefault(self):
d = ircutils.IrcDict()
d.setdefault('#FOO', []).append(1)
self.assertEqual(d['#foo'], [1])
self.assertEqual(d['#fOO'], [1])
self.assertEqual(d['#FOO'], [1])
def testGet(self):
d = ircutils.IrcDict()
self.assertEqual(d.get('#FOO'), None)
d['#foo'] = 1
self.assertEqual(d.get('#FOO'), 1)
def testContains(self):
d = ircutils.IrcDict()
d['#FOO'] = None
self.failUnless('#foo' in d)
d['#fOOBAR[]'] = None
self.failUnless('#foobar{}' in d)
def testGetSetItem(self):
d = ircutils.IrcDict()
d['#FOO'] = 12
self.assertEqual(12, d['#foo'])
d['#fOOBAR[]'] = 'blah'
self.assertEqual('blah', d['#foobar{}'])
def testCopyable(self):
d = ircutils.IrcDict()
d['foo'] = 'bar'
self.failUnless(d == copy.copy(d))
self.failUnless(d == copy.deepcopy(d))
class IrcSetTestCase(SupyTestCase):
def test(self):
s = ircutils.IrcSet()
s.add('foo')
s.add('bar')
self.failUnless('foo' in s)
self.failUnless('FOO' in s)
s.discard('alfkj')
s.remove('FOo')
self.failIf('foo' in s)
self.failIf('FOo' in s)
def testCopy(self):
s = ircutils.IrcSet()
s.add('foo')
s.add('bar')
s1 = copy.deepcopy(s)
self.failUnless('foo' in s)
self.failUnless('FOO' in s)
s.discard('alfkj')
s.remove('FOo')
self.failIf('foo' in s)
self.failIf('FOo' in s)
self.failUnless('foo' in s1)
self.failUnless('FOO' in s1)
s1.discard('alfkj')
s1.remove('FOo')
self.failIf('foo' in s1)
self.failIf('FOo' in s1)
class IrcStringTestCase(SupyTestCase):
def testEquality(self):
self.assertEqual('#foo', ircutils.IrcString('#foo'))
self.assertEqual('#foo', ircutils.IrcString('#FOO'))
self.assertEqual('#FOO', ircutils.IrcString('#foo'))
self.assertEqual('#FOO', ircutils.IrcString('#FOO'))
self.assertEqual(hash(ircutils.IrcString('#FOO')),
hash(ircutils.IrcString('#foo')))
def testInequality(self):
s1 = 'supybot'
s2 = ircutils.IrcString('Supybot')
self.failUnless(s1 == s2)
self.failIf(s1 != s2)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

44
test/test_plugins.py Normal file
View File

@ -0,0 +1,44 @@
###
# 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 testsupport import *
import sets
import supybot.irclib as irclib
import supybot.plugins as plugins

65
test/test_privmsgs.py Normal file
View File

@ -0,0 +1,65 @@
###
# 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 testsupport import *
import supybot.ircmsgs as ircmsgs
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
class FunctionsTest(SupyTestCase):
def testGetChannel(self):
channel = '#foo'
msg = ircmsgs.privmsg(channel, 'foo bar baz')
args = msg.args[1].split()
originalArgs = args[:]
self.assertEqual(privmsgs.getChannel(msg, args), channel)
self.assertEqual(args, originalArgs)
msg = ircmsgs.privmsg('nick', '%s bar baz' % channel)
args = msg.args[1].split()
originalArgs = args[:]
self.assertEqual(privmsgs.getChannel(msg, args), channel)
self.assertEqual(args, originalArgs[1:])
def testGetArgs(self):
args = ['foo', 'bar', 'baz']
self.assertEqual(privmsgs.getArgs(args), ' '.join(args))
self.assertEqual(privmsgs.getArgs(args, required=2),
[args[0], ' '.join(args[1:])])
self.assertEqual(privmsgs.getArgs(args, required=3), args)
self.assertRaises(callbacks.ArgumentError,
privmsgs.getArgs, args, required=4)
self.assertEqual(privmsgs.getArgs(args, required=3, optional=1),
args + [''])
self.assertEqual(privmsgs.getArgs(args, required=0, optional=1),
' '.join(args))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

171
test/test_registry.py Normal file
View File

@ -0,0 +1,171 @@
###
# Copyright (c) 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 testsupport import *
import re
import supybot.conf as conf
import supybot.registry as registry
join = registry.join
split = registry.split
escape = registry.escape
unescape = registry.unescape
class FunctionsTestCase(SupyTestCase):
def testEscape(self):
self.assertEqual('foo', escape('foo'))
self.assertEqual('foo\\.bar', escape('foo.bar'))
self.assertEqual('foo\\:bar', escape('foo:bar'))
def testUnescape(self):
self.assertEqual('foo', unescape('foo'))
self.assertEqual('foo.bar', unescape('foo\\.bar'))
self.assertEqual('foo:bar', unescape('foo\\:bar'))
def testEscapeAndUnescapeAreInverses(self):
for s in ['foo', 'foo.bar']:
self.assertEqual(s, unescape(escape(s)))
self.assertEqual(escape(s), escape(unescape(escape(s))))
def testSplit(self):
self.assertEqual(['foo'], split('foo'))
self.assertEqual(['foo', 'bar'], split('foo.bar'))
self.assertEqual(['foo.bar'], split('foo\\.bar'))
def testJoin(self):
self.assertEqual('foo', join(['foo']))
self.assertEqual('foo.bar', join(['foo', 'bar']))
self.assertEqual('foo\\.bar', join(['foo.bar']))
def testJoinAndSplitAreInverses(self):
for s in ['foo', 'foo.bar', 'foo\\.bar']:
self.assertEqual(s, join(split(s)))
self.assertEqual(split(s), split(join(split(s))))
class ValuesTestCase(SupyTestCase):
def testBoolean(self):
v = registry.Boolean(True, """Help""")
self.failUnless(v())
v.setValue(False)
self.failIf(v())
v.set('True')
self.failUnless(v())
v.set('False')
self.failIf(v())
v.set('On')
self.failUnless(v())
v.set('Off')
self.failIf(v())
v.set('enable')
self.failUnless(v())
v.set('disable')
self.failIf(v())
v.set('toggle')
self.failUnless(v())
v.set('toggle')
self.failIf(v())
def testInteger(self):
v = registry.Integer(1, 'help')
self.assertEqual(v(), 1)
v.setValue(10)
self.assertEqual(v(), 10)
v.set('100')
self.assertEqual(v(), 100)
v.set('-1000')
self.assertEqual(v(), -1000)
def testPositiveInteger(self):
v = registry.PositiveInteger(1, 'help')
self.assertEqual(v(), 1)
self.assertRaises(registry.InvalidRegistryValue, v.setValue, -1)
self.assertRaises(registry.InvalidRegistryValue, v.set, '-1')
def testFloat(self):
v = registry.Float(1.0, 'help')
self.assertEqual(v(), 1.0)
v.setValue(10)
self.assertEqual(v(), 10.0)
v.set('0')
self.assertEqual(v(), 0.0)
def testString(self):
v = registry.String('foo', 'help')
self.assertEqual(v(), 'foo')
v.setValue('bar')
self.assertEqual(v(), 'bar')
v.set('"biff"')
self.assertEqual(v(), 'biff')
v.set("'buff'")
self.assertEqual(v(), 'buff')
v.set('"xyzzy')
self.assertEqual(v(), '"xyzzy')
def testNormalizedString(self):
v = registry.NormalizedString("""foo
bar baz
biff
""", 'help')
self.assertEqual(v(), 'foo bar baz biff')
v.setValue('foo bar baz')
self.assertEqual(v(), 'foo bar baz')
v.set('"foo bar baz"')
self.assertEqual(v(), 'foo bar baz')
def testStringSurroundedBySpaces(self):
v = registry.StringSurroundedBySpaces('foo', 'help')
self.assertEqual(v(), ' foo ')
v.setValue('||')
self.assertEqual(v(), ' || ')
v.set('&&')
self.assertEqual(v(), ' && ')
def testCommaSeparatedListOfStrings(self):
v = registry.CommaSeparatedListOfStrings(['foo', 'bar'], 'help')
self.assertEqual(v(), ['foo', 'bar'])
v.setValue(['foo', 'bar', 'baz'])
self.assertEqual(v(), ['foo', 'bar', 'baz'])
v.set('foo,bar')
self.assertEqual(v(), ['foo', 'bar'])
def testRegexp(self):
v = registry.Regexp(None, 'help')
self.assertEqual(v(), None)
v.set('m/foo/')
self.failUnless(v().match('foo'))
v.set('')
self.assertEqual(v(), None)
self.assertRaises(registry.InvalidRegistryValue,
v.setValue, re.compile(r'foo'))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

78
test/test_schedule.py Normal file
View File

@ -0,0 +1,78 @@
###
# 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 testsupport import *
import time
import supybot.schedule as schedule
class TestSchedule(SupyTestCase):
def testSchedule(self):
sched = schedule.Schedule()
i = [0]
def add10():
i[0] = i[0] + 10
def add1():
i[0] = i[0] + 1
sched.addEvent(add10, time.time() + 3)
sched.addEvent(add1, time.time() + 1)
time.sleep(1.2)
sched.run()
self.assertEqual(i[0], 1)
time.sleep(1.9)
sched.run()
self.assertEqual(i[0], 11)
sched.addEvent(add10, time.time() + 3, 'test')
sched.run()
self.assertEqual(i[0], 11)
sched.removeEvent('test')
self.assertEqual(i[0], 11)
time.sleep(3)
self.assertEqual(i[0], 11)
def testReschedule(self):
sched = schedule.Schedule()
i = [0]
def inc():
i[0] += 1
n = sched.addEvent(inc, time.time() + 1)
sched.rescheduleEvent(n, time.time() + 3)
time.sleep(1.2)
sched.run()
self.assertEqual(i[0], 0)
time.sleep(2)
sched.run()
self.assertEqual(i[0], 1)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -0,0 +1,91 @@
###
# 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 testsupport import *
import sets
import supybot.irclib as irclib
import supybot.ircutils as ircutils
class holder:
users = sets.Set(map(str, range(1000)))
class FunctionsTestCase(SupyTestCase):
class irc:
class state:
channels = {'#foo': holder()}
nick = 'foobar'
def testStandardSubstitute(self):
f = ircutils.standardSubstitute
msg = ircmsgs.privmsg('#foo', 'filler', prefix='biff!quux@xyzzy')
s = f(self.irc, msg, '$rand')
try:
int(s)
except ValueError:
self.fail('$rand wasn\'t an int.')
s = f(self.irc, msg, '$randomInt')
try:
int(s)
except ValueError:
self.fail('$randomint wasn\'t an int.')
self.assertEqual(f(self.irc, msg, '$botnick'), self.irc.nick)
self.assertEqual(f(self.irc, msg, '$who'), msg.nick)
self.assertEqual(f(self.irc, msg, '$WHO'),
msg.nick, 'stand. sub. not case-insensitive.')
self.assertEqual(f(self.irc, msg, '$nick'), msg.nick)
self.assertNotEqual(f(self.irc, msg, '$randomdate'), '$randomdate')
q = f(self.irc,msg,'$randomdate\t$randomdate')
dl = q.split('\t')
if dl[0] == dl[1]:
self.fail ('Two $randomdates in the same string were the same')
q = f(self.irc, msg, '$randomint\t$randomint')
dl = q.split('\t')
if dl[0] == dl[1]:
self.fail ('Two $randomints in the same string were the same')
self.assertNotEqual(f(self.irc, msg, '$today'), '$today')
self.assertNotEqual(f(self.irc, msg, '$now'), '$now')
n = f(self.irc, msg, '$randnick')
self.failUnless(n in self.irc.state.channels['#foo'].users)
n = f(self.irc, msg, '$randomnick')
self.failUnless(n in self.irc.state.channels['#foo'].users)
n = f(self.irc, msg, '$randomnick '*100)
L = n.split()
self.failIf(all(L[0].__eq__, L), 'all $randomnicks were the same')
c = f(self.irc, msg, '$channel')
self.assertEqual(c, msg.args[0])

614
test/test_structures.py Normal file
View File

@ -0,0 +1,614 @@
###
# 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 testsupport import *
import pickle
from supybot.structures import *
class RingBufferTestCase(SupyTestCase):
def testInit(self):
self.assertRaises(ValueError, RingBuffer, -1)
self.assertRaises(ValueError, RingBuffer, 0)
self.assertEqual(range(10), list(RingBuffer(10, range(10))))
def testLen(self):
b = RingBuffer(3)
self.assertEqual(0, len(b))
b.append(1)
self.assertEqual(1, len(b))
b.append(2)
self.assertEqual(2, len(b))
b.append(3)
self.assertEqual(3, len(b))
b.append(4)
self.assertEqual(3, len(b))
b.append(5)
self.assertEqual(3, len(b))
def testNonzero(self):
b = RingBuffer(3)
self.failIf(b)
b.append(1)
self.failUnless(b)
def testAppend(self):
b = RingBuffer(3)
self.assertEqual([], list(b))
b.append(1)
self.assertEqual([1], list(b))
b.append(2)
self.assertEqual([1, 2], list(b))
b.append(3)
self.assertEqual([1, 2, 3], list(b))
b.append(4)
self.assertEqual([2, 3, 4], list(b))
b.append(5)
self.assertEqual([3, 4, 5], list(b))
b.append(6)
self.assertEqual([4, 5, 6], list(b))
def testContains(self):
b = RingBuffer(3, range(3))
self.failUnless(0 in b)
self.failUnless(1 in b)
self.failUnless(2 in b)
self.failIf(3 in b)
def testGetitem(self):
L = range(10)
b = RingBuffer(len(L), L)
for i in range(len(b)):
self.assertEqual(L[i], b[i])
for i in range(len(b)):
self.assertEqual(L[-i], b[-i])
for i in range(len(b)):
b.append(i)
for i in range(len(b)):
self.assertEqual(L[i], b[i])
for i in range(len(b)):
self.assertEqual(list(b), list(b[:i]) + list(b[i:]))
def testSliceGetitem(self):
L = range(10)
b = RingBuffer(len(L), L)
for i in range(len(b)):
self.assertEqual(L[:i], b[:i])
self.assertEqual(L[i:], b[i:])
self.assertEqual(L[i:len(b)-i], b[i:len(b)-i])
self.assertEqual(L[:-i], b[:-i])
self.assertEqual(L[-i:], b[-i:])
self.assertEqual(L[i:-i], b[i:-i])
for i in range(len(b)):
b.append(i)
for i in range(len(b)):
self.assertEqual(L[:i], b[:i])
self.assertEqual(L[i:], b[i:])
self.assertEqual(L[i:len(b)-i], b[i:len(b)-i])
self.assertEqual(L[:-i], b[:-i])
self.assertEqual(L[-i:], b[-i:])
self.assertEqual(L[i:-i], b[i:-i])
def testSetitem(self):
L = range(10)
b = RingBuffer(len(L), [0]*len(L))
for i in range(len(b)):
b[i] = i
for i in range(len(b)):
self.assertEqual(b[i], i)
for i in range(len(b)):
b.append(0)
for i in range(len(b)):
b[i] = i
for i in range(len(b)):
self.assertEqual(b[i], i)
def testSliceSetitem(self):
L = range(10)
b = RingBuffer(len(L), [0]*len(L))
self.assertRaises(ValueError, b.__setitem__, slice(0, 10), [])
b[2:4] = L[2:4]
self.assertEquals(b[2:4], L[2:4])
for _ in range(len(b)):
b.append(0)
b[2:4] = L[2:4]
self.assertEquals(b[2:4], L[2:4])
def testExtend(self):
b = RingBuffer(3, range(3))
self.assertEqual(list(b), range(3))
b.extend(range(6))
self.assertEqual(list(b), range(6)[3:])
def testRepr(self):
b = RingBuffer(3)
self.assertEqual(repr(b), 'RingBuffer(3, [])')
b.append(1)
self.assertEqual(repr(b), 'RingBuffer(3, [1])')
b.append(2)
self.assertEqual(repr(b), 'RingBuffer(3, [1, 2])')
b.append(3)
self.assertEqual(repr(b), 'RingBuffer(3, [1, 2, 3])')
b.append(4)
self.assertEqual(repr(b), 'RingBuffer(3, [2, 3, 4])')
b.append(5)
self.assertEqual(repr(b), 'RingBuffer(3, [3, 4, 5])')
b.append(6)
self.assertEqual(repr(b), 'RingBuffer(3, [4, 5, 6])')
def testPickleCopy(self):
b = RingBuffer(10, range(10))
self.assertEqual(pickle.loads(pickle.dumps(b)), b)
def testEq(self):
b = RingBuffer(3, range(3))
self.failIf(b == range(3))
b1 = RingBuffer(3)
self.failIf(b == b1)
b1.append(0)
self.failIf(b == b1)
b1.append(1)
self.failIf(b == b1)
b1.append(2)
self.failUnless(b == b1)
b = RingBuffer(100, range(10))
b1 = RingBuffer(10, range(10))
self.failIf(b == b1)
def testIter(self):
b = RingBuffer(3, range(3))
L = []
for elt in b:
L.append(elt)
self.assertEqual(L, range(3))
for elt in range(3):
b.append(elt)
del L[:]
for elt in b:
L.append(elt)
self.assertEqual(L, range(3))
class QueueTest(SupyTestCase):
def testReset(self):
q = queue()
q.enqueue(1)
self.assertEqual(len(q), 1)
q.reset()
self.assertEqual(len(q), 0)
def testGetitem(self):
q = queue()
n = 10
self.assertRaises(IndexError, q.__getitem__, 0)
for i in xrange(n):
q.enqueue(i)
for i in xrange(n):
self.assertEqual(q[i], i)
for i in xrange(n, 0, -1):
self.assertEqual(q[-i], n-i)
for i in xrange(len(q)):
self.assertEqual(list(q), list(q[:i]) + list(q[i:]))
self.assertRaises(IndexError, q.__getitem__, -(n+1))
self.assertRaises(IndexError, q.__getitem__, n)
self.assertEqual(q[3:7], queue([3, 4, 5, 6]))
def testSetitem(self):
q1 = queue()
self.assertRaises(IndexError, q1.__setitem__, 0, 0)
for i in xrange(10):
q1.enqueue(i)
q2 = eval(repr(q1))
for (i, elt) in enumerate(q2):
q2[i] = elt*2
self.assertEqual([x*2 for x in q1], list(q2))
def testNonzero(self):
q = queue()
self.failIf(q, 'queue not zero after initialization')
q.enqueue(1)
self.failUnless(q, 'queue zero after adding element')
q.dequeue()
self.failIf(q, 'queue not zero after dequeue of only element')
def testLen(self):
q = queue()
self.assertEqual(0, len(q), 'queue len not 0 after initialization')
q.enqueue(1)
self.assertEqual(1, len(q), 'queue len not 1 after enqueue')
q.enqueue(2)
self.assertEqual(2, len(q), 'queue len not 2 after enqueue')
q.dequeue()
self.assertEqual(1, len(q), 'queue len not 1 after dequeue')
q.dequeue()
self.assertEqual(0, len(q), 'queue len not 0 after dequeue')
for i in range(10):
L = range(i)
q = queue(L)
self.assertEqual(len(q), i)
def testEq(self):
q1 = queue()
q2 = queue()
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
self.failUnless(q1 == q2, 'initialized queues not equal')
q1.enqueue(1)
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
q2.enqueue(1)
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
self.failUnless(q1 == q2, 'queues not equal after identical enqueue')
q1.dequeue()
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
self.failIf(q1 == q2, 'queues equal after one dequeue')
q2.dequeue()
self.failUnless(q1 == q2, 'queues not equal after both are dequeued')
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
def testInit(self):
self.assertEqual(len(queue()), 0, 'queue len not 0 after init')
q = queue()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
self.assertEqual(queue((1, 2, 3)),q, 'init not equivalent to enqueues')
q = queue((1, 2, 3))
self.assertEqual(q.dequeue(), 1, 'values not returned in proper order')
self.assertEqual(q.dequeue(), 2, 'values not returned in proper order')
self.assertEqual(q.dequeue(), 3, 'values not returned in proper order')
def testRepr(self):
q = queue()
q.enqueue(1)
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue('foo')
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue(None)
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue(1.0)
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue([])
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue(())
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue([1])
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue((1,))
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
def testEnqueueDequeue(self):
q = queue()
self.assertRaises(IndexError, q.dequeue)
q.enqueue(1)
self.assertEqual(q.dequeue(), 1,
'first dequeue didn\'t return same as first enqueue')
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
self.assertEqual(q.dequeue(), 1)
self.assertEqual(q.dequeue(), 2)
self.assertEqual(q.dequeue(), 3)
def testPeek(self):
q = queue()
self.assertRaises(IndexError, q.peek)
q.enqueue(1)
self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue')
q.enqueue(2)
self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue')
q.dequeue()
self.assertEqual(q.peek(), 2, 'peek didn\'t return second enqueue')
q.dequeue()
self.assertRaises(IndexError, q.peek)
def testContains(self):
q = queue()
self.failIf(1 in q, 'empty queue cannot have elements')
q.enqueue(1)
self.failUnless(1 in q, 'recent enqueued element not in q')
q.enqueue(2)
self.failUnless(1 in q, 'original enqueued element not in q')
self.failUnless(2 in q, 'second enqueued element not in q')
q.dequeue()
self.failIf(1 in q, 'dequeued element in q')
self.failUnless(2 in q, 'not dequeued element not in q')
q.dequeue()
self.failIf(2 in q, 'dequeued element in q')
def testIter(self):
q1 = queue((1, 2, 3))
q2 = queue()
for i in q1:
q2.enqueue(i)
self.assertEqual(q1, q2, 'iterate didn\'t return all elements')
for _ in queue():
self.fail('no elements should be in empty queue')
def testPickleCopy(self):
q = queue(range(10))
self.assertEqual(q, pickle.loads(pickle.dumps(q)))
queue = smallqueue
class SmallQueueTest(SupyTestCase):
def testReset(self):
q = queue()
q.enqueue(1)
self.assertEqual(len(q), 1)
q.reset()
self.assertEqual(len(q), 0)
def testGetitem(self):
q = queue()
n = 10
self.assertRaises(IndexError, q.__getitem__, 0)
for i in xrange(n):
q.enqueue(i)
for i in xrange(n):
self.assertEqual(q[i], i)
for i in xrange(n, 0, -1):
self.assertEqual(q[-i], n-i)
for i in xrange(len(q)):
self.assertEqual(list(q), list(q[:i]) + list(q[i:]))
self.assertRaises(IndexError, q.__getitem__, -(n+1))
self.assertRaises(IndexError, q.__getitem__, n)
self.assertEqual(q[3:7], queue([3, 4, 5, 6]))
def testSetitem(self):
q1 = queue()
self.assertRaises(IndexError, q1.__setitem__, 0, 0)
for i in xrange(10):
q1.enqueue(i)
q2 = eval(repr(q1))
for (i, elt) in enumerate(q2):
q2[i] = elt*2
self.assertEqual([x*2 for x in q1], list(q2))
def testNonzero(self):
q = queue()
self.failIf(q, 'queue not zero after initialization')
q.enqueue(1)
self.failUnless(q, 'queue zero after adding element')
q.dequeue()
self.failIf(q, 'queue not zero after dequeue of only element')
def testLen(self):
q = queue()
self.assertEqual(0, len(q), 'queue len not 0 after initialization')
q.enqueue(1)
self.assertEqual(1, len(q), 'queue len not 1 after enqueue')
q.enqueue(2)
self.assertEqual(2, len(q), 'queue len not 2 after enqueue')
q.dequeue()
self.assertEqual(1, len(q), 'queue len not 1 after dequeue')
q.dequeue()
self.assertEqual(0, len(q), 'queue len not 0 after dequeue')
for i in range(10):
L = range(i)
q = queue(L)
self.assertEqual(len(q), i)
def testEq(self):
q1 = queue()
q2 = queue()
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
self.failUnless(q1 == q2, 'initialized queues not equal')
q1.enqueue(1)
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
q2.enqueue(1)
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
self.failUnless(q1 == q2, 'queues not equal after identical enqueue')
q1.dequeue()
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
self.failIf(q1 == q2, 'queues equal after one dequeue')
q2.dequeue()
self.failUnless(q1 == q2, 'queues not equal after both are dequeued')
self.failUnless(q1 == q1, 'queue not equal to itself')
self.failUnless(q2 == q2, 'queue not equal to itself')
def testInit(self):
self.assertEqual(len(queue()), 0, 'queue len not 0 after init')
q = queue()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
self.assertEqual(queue((1, 2, 3)),q, 'init not equivalent to enqueues')
q = queue((1, 2, 3))
self.assertEqual(q.dequeue(), 1, 'values not returned in proper order')
self.assertEqual(q.dequeue(), 2, 'values not returned in proper order')
self.assertEqual(q.dequeue(), 3, 'values not returned in proper order')
def testRepr(self):
q = queue()
q.enqueue(1)
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue('foo')
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue(None)
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue(1.0)
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue([])
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue(())
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue([1])
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
q.enqueue((1,))
self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue')
def testEnqueueDequeue(self):
q = queue()
self.assertRaises(IndexError, q.dequeue)
q.enqueue(1)
self.assertEqual(q.dequeue(), 1,
'first dequeue didn\'t return same as first enqueue')
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
self.assertEqual(q.dequeue(), 1)
self.assertEqual(q.dequeue(), 2)
self.assertEqual(q.dequeue(), 3)
def testPeek(self):
q = queue()
self.assertRaises(IndexError, q.peek)
q.enqueue(1)
self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue')
q.enqueue(2)
self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue')
q.dequeue()
self.assertEqual(q.peek(), 2, 'peek didn\'t return second enqueue')
q.dequeue()
self.assertRaises(IndexError, q.peek)
def testContains(self):
q = queue()
self.failIf(1 in q, 'empty queue cannot have elements')
q.enqueue(1)
self.failUnless(1 in q, 'recent enqueued element not in q')
q.enqueue(2)
self.failUnless(1 in q, 'original enqueued element not in q')
self.failUnless(2 in q, 'second enqueued element not in q')
q.dequeue()
self.failIf(1 in q, 'dequeued element in q')
self.failUnless(2 in q, 'not dequeued element not in q')
q.dequeue()
self.failIf(2 in q, 'dequeued element in q')
def testIter(self):
q1 = queue((1, 2, 3))
q2 = queue()
for i in q1:
q2.enqueue(i)
self.assertEqual(q1, q2, 'iterate didn\'t return all elements')
for _ in queue():
self.fail('no elements should be in empty queue')
def testPickleCopy(self):
q = queue(range(10))
self.assertEqual(q, pickle.loads(pickle.dumps(q)))
class MaxLengthQueueTestCase(SupyTestCase):
def testInit(self):
q = MaxLengthQueue(3, (1, 2, 3))
self.assertEqual(list(q), [1, 2, 3])
self.assertRaises(TypeError, MaxLengthQueue, 3, 1, 2, 3)
def testMaxLength(self):
q = MaxLengthQueue(3)
q.enqueue(1)
self.assertEqual(len(q), 1)
q.enqueue(2)
self.assertEqual(len(q), 2)
q.enqueue(3)
self.assertEqual(len(q), 3)
q.enqueue(4)
self.assertEqual(len(q), 3)
self.assertEqual(q.peek(), 2)
q.enqueue(5)
self.assertEqual(len(q), 3)
self.assertEqual(q[0], 3)
class TwoWayDictionaryTestCase(SupyTestCase):
def testInit(self):
d = TwoWayDictionary(foo='bar')
self.failUnless('foo' in d)
self.failUnless('bar' in d)
d = TwoWayDictionary({1: 2})
self.failUnless(1 in d)
self.failUnless(2 in d)
def testSetitem(self):
d = TwoWayDictionary()
d['foo'] = 'bar'
self.failUnless('foo' in d)
self.failUnless('bar' in d)
def testDelitem(self):
d = TwoWayDictionary(foo='bar')
del d['foo']
self.failIf('foo' in d)
self.failIf('bar' in d)
d = TwoWayDictionary(foo='bar')
del d['bar']
self.failIf('bar' in d)
self.failIf('foo' in d)
class TestTimeoutQueue(SupyTestCase):
def test(self):
q = TimeoutQueue(1)
q.enqueue(1)
self.assertEqual(len(q), 1)
q.enqueue(2)
self.assertEqual(len(q), 2)
q.enqueue(3)
self.assertEqual(len(q), 3)
self.assertEqual(sum(q), 6)
time.sleep(1.1)
self.assertEqual(len(q), 0)
self.assertEqual(sum(q), 0)
def testCallableTimeout(self):
q = TimeoutQueue(lambda : 1)
q.enqueue(1)
self.assertEqual(len(q), 1)
q.enqueue(2)
self.assertEqual(len(q), 2)
q.enqueue(3)
self.assertEqual(len(q), 3)
self.assertEqual(sum(q), 6)
time.sleep(1.1)
self.assertEqual(len(q), 0)
self.assertEqual(sum(q), 0)
def testContains(self):
q = TimeoutQueue(1)
q.enqueue(1)
self.failUnless(1 in q)
self.failIf(2 in q)
time.sleep(1.1)
self.failIf(1 in q)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

411
test/test_utils.py Normal file
View File

@ -0,0 +1,411 @@
###
# 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 testsupport import *
import sets
import supybot.utils as utils
class UtilsTest(SupyTestCase):
def testExnToString(self):
try:
raise KeyError, 1
except Exception, e:
self.assertEqual(utils.exnToString(e), 'KeyError: 1')
try:
raise EOFError
except Exception, e:
self.assertEqual(utils.exnToString(e), 'EOFError')
def testMatchCase(self):
f = utils.matchCase
self.assertEqual('bar', f('foo', 'bar'))
self.assertEqual('Bar', f('Foo', 'bar'))
self.assertEqual('BAr', f('FOo', 'bar'))
self.assertEqual('BAR', f('FOO', 'bar'))
self.assertEqual('bAR', f('fOO', 'bar'))
self.assertEqual('baR', f('foO', 'bar'))
self.assertEqual('BaR', f('FoO', 'bar'))
def testPluralize(self):
f = utils.pluralize
self.assertEqual('bike', f('bike', 1))
self.assertEqual('bikes', f('bike', 2))
self.assertEqual('BIKE', f('BIKE', 1))
self.assertEqual('BIKES', f('BIKE', 2))
self.assertEqual('match', f('match', 1))
self.assertEqual('matches', f('match', 2))
self.assertEqual('Patch', f('Patch', 1))
self.assertEqual('Patches', f('Patch', 2))
self.assertEqual('fish', f('fish', 1))
self.assertEqual('fishes', f('fish', 2))
self.assertEqual('try', f('try', 1))
self.assertEqual('tries', f('try', 2))
self.assertEqual('day', f('day', 1))
self.assertEqual('days', f('day', 2))
def testDepluralize(self):
f = utils.depluralize
self.assertEqual('bike', f('bikes'))
self.assertEqual('Bike', f('Bikes'))
self.assertEqual('BIKE', f('BIKES'))
self.assertEqual('match', f('matches'))
self.assertEqual('Match', f('Matches'))
self.assertEqual('fish', f('fishes'))
self.assertEqual('try', f('tries'))
def testTimeElapsed(self):
self.assertRaises(ValueError, utils.timeElapsed, 0,
leadingZeroes=False, seconds=False)
then = 0
now = 0
for now, expected in [(0, '0 seconds'),
(1, '1 second'),
(60, '1 minute and 0 seconds'),
(61, '1 minute and 1 second'),
(62, '1 minute and 2 seconds'),
(122, '2 minutes and 2 seconds'),
(3722, '1 hour, 2 minutes, and 2 seconds'),
(7322, '2 hours, 2 minutes, and 2 seconds'),
(90061,'1 day, 1 hour, 1 minute, and 1 second'),
(180122, '2 days, 2 hours, 2 minutes, '
'and 2 seconds')]:
self.assertEqual(utils.timeElapsed(now - then), expected)
def timeElapsedShort(self):
self.assertEqual(utils.timeElapsed(123, short=True), '2m 3s')
def testEachSubstring(self):
s = 'foobar'
L = ['f', 'fo', 'foo', 'foob', 'fooba', 'foobar']
self.assertEqual(list(utils.eachSubstring(s)), L)
def testDistance(self):
self.assertEqual(utils.distance('', ''), 0)
self.assertEqual(utils.distance('a', 'b'), 1)
self.assertEqual(utils.distance('a', 'a'), 0)
self.assertEqual(utils.distance('foobar', 'jemfinch'), 8)
self.assertEqual(utils.distance('a', 'ab'), 1)
self.assertEqual(utils.distance('foo', ''), 3)
self.assertEqual(utils.distance('', 'foo'), 3)
self.assertEqual(utils.distance('appel', 'nappe'), 2)
self.assertEqual(utils.distance('nappe', 'appel'), 2)
def testAbbrev(self):
L = ['abc', 'bcd', 'bbe', 'foo', 'fool']
d = utils.abbrev(L)
def getItem(s):
return d[s]
self.assertRaises(KeyError, getItem, 'f')
self.assertRaises(KeyError, getItem, 'fo')
self.assertRaises(KeyError, getItem, 'b')
self.assertEqual(d['bb'], 'bbe')
self.assertEqual(d['bc'], 'bcd')
self.assertEqual(d['a'], 'abc')
self.assertEqual(d['ab'], 'abc')
self.assertEqual(d['fool'], 'fool')
self.assertEqual(d['foo'], 'foo')
def testAbbrevFailsWithDups(self):
L = ['english', 'english']
self.assertRaises(ValueError, utils.abbrev, L)
def testSoundex(self):
L = [('Euler', 'E460'),
('Ellery', 'E460'),
('Gauss', 'G200'),
('Ghosh', 'G200'),
('Hilbert', 'H416'),
('Heilbronn', 'H416'),
('Knuth', 'K530'),
('Kant', 'K530'),
('Lloyd', 'L300'),
('Ladd', 'L300'),
('Lukasiewicz', 'L222'),
('Lissajous', 'L222')]
for (name, key) in L:
soundex = utils.soundex(name)
self.assertEqual(soundex, key,
'%s was %s, not %s' % (name, soundex, key))
self.assertRaises(ValueError, utils.soundex, '3')
self.assertRaises(ValueError, utils.soundex, "'")
def testDQRepr(self):
L = ['foo', 'foo\'bar', 'foo"bar', '"', '\\', '', '\x00']
for s in L:
r = utils.dqrepr(s)
self.assertEqual(s, eval(r), s)
self.failUnless(r[0] == '"' and r[-1] == '"', s)
## def testQuoted(self):
## s = 'foo'
## t = 'let\'s'
## self.assertEqual("'%s'" % s, utils.quoted(s), s)
## self.assertEqual('"%s"' % t, utils.quoted(t), t)
def testPerlReToPythonRe(self):
r = utils.perlReToPythonRe('m/foo/')
self.failUnless(r.search('foo'))
r = utils.perlReToPythonRe('/foo/')
self.failUnless(r.search('foo'))
r = utils.perlReToPythonRe('m/\\//')
self.failUnless(r.search('/'))
r = utils.perlReToPythonRe('m/cat/i')
self.failUnless(r.search('CAT'))
self.assertRaises(ValueError, utils.perlReToPythonRe, 'm/?/')
def testP2PReDifferentSeparator(self):
r = utils.perlReToPythonRe('m!foo!')
self.failUnless(r.search('foo'))
def testPerlReToReplacer(self):
f = utils.perlReToReplacer('s/foo/bar/')
self.assertEqual(f('foobarbaz'), 'barbarbaz')
f = utils.perlReToReplacer('s/fool/bar/')
self.assertEqual(f('foobarbaz'), 'foobarbaz')
f = utils.perlReToReplacer('s/foo//')
self.assertEqual(f('foobarbaz'), 'barbaz')
f = utils.perlReToReplacer('s/ba//')
self.assertEqual(f('foobarbaz'), 'foorbaz')
f = utils.perlReToReplacer('s/ba//g')
self.assertEqual(f('foobarbaz'), 'foorz')
f = utils.perlReToReplacer('s/ba\\///g')
self.assertEqual(f('fooba/rba/z'), 'foorz')
f = utils.perlReToReplacer('s/cat/dog/i')
self.assertEqual(f('CATFISH'), 'dogFISH')
f = utils.perlReToReplacer('s/foo/foo\/bar/')
self.assertEqual(f('foo'), 'foo/bar')
f = utils.perlReToReplacer('s/^/foo/')
self.assertEqual(f('bar'), 'foobar')
def testPReToReplacerDifferentSeparator(self):
f = utils.perlReToReplacer('s#foo#bar#')
self.assertEqual(f('foobarbaz'), 'barbarbaz')
def testPerlReToReplacerBug850931(self):
f = utils.perlReToReplacer('s/\b(\w+)\b/\1./g')
self.assertEqual(f('foo bar baz'), 'foo. bar. baz.')
def testPerlVariableSubstitute(self):
f = utils.perlVariableSubstitute
vars = {'foo': 'bar', 'b a z': 'baz', 'b': 'c', 'i': 100,
'f': lambda: 'called'}
self.assertEqual(f(vars, '$foo'), 'bar')
self.assertEqual(f(vars, '${foo}'), 'bar')
self.assertEqual(f(vars, '$b'), 'c')
self.assertEqual(f(vars, '${b}'), 'c')
self.assertEqual(f(vars, '$i'), '100')
self.assertEqual(f(vars, '${i}'), '100')
self.assertEqual(f(vars, '$f'), 'called')
self.assertEqual(f(vars, '${f}'), 'called')
self.assertEqual(f(vars, '${b a z}'), 'baz')
self.assertEqual(f(vars, '$b:$i'), 'c:100')
def testFindBinaryInPath(self):
if os.name == 'posix':
self.assertEqual(None, utils.findBinaryInPath('asdfhjklasdfhjkl'))
self.failUnless(utils.findBinaryInPath('sh').endswith('/bin/sh'))
def testCommaAndify(self):
L = ['foo']
original = L[:]
self.assertEqual(utils.commaAndify(L), 'foo')
self.assertEqual(utils.commaAndify(L, And='or'), 'foo')
self.assertEqual(L, original)
L.append('bar')
original = L[:]
self.assertEqual(utils.commaAndify(L), 'foo and bar')
self.assertEqual(utils.commaAndify(L, And='or'), 'foo or bar')
self.assertEqual(L, original)
L.append('baz')
original = L[:]
self.assertEqual(utils.commaAndify(L), 'foo, bar, and baz')
self.assertEqual(utils.commaAndify(L, And='or'), 'foo, bar, or baz')
self.assertEqual(utils.commaAndify(L, comma=';'), 'foo; bar; and baz')
self.assertEqual(utils.commaAndify(L, comma=';', And='or'),
'foo; bar; or baz')
self.assertEqual(L, original)
self.failUnless(utils.commaAndify(sets.Set(L)))
def testCommaAndifyRaisesTypeError(self):
L = [(2,)]
self.assertRaises(TypeError, utils.commaAndify, L)
L.append((3,))
self.assertRaises(TypeError, utils.commaAndify, L)
def testUnCommaThe(self):
self.assertEqual(utils.unCommaThe('foo bar'), 'foo bar')
self.assertEqual(utils.unCommaThe('foo bar, the'), 'the foo bar')
self.assertEqual(utils.unCommaThe('foo bar, The'), 'The foo bar')
self.assertEqual(utils.unCommaThe('foo bar,the'), 'the foo bar')
def testNormalizeWhitespace(self):
self.assertEqual(utils.normalizeWhitespace('foo bar'), 'foo bar')
self.assertEqual(utils.normalizeWhitespace('foo\nbar'), 'foo bar')
self.assertEqual(utils.normalizeWhitespace('foo\tbar'), 'foo bar')
def testSortBy(self):
L = ['abc', 'z', 'AD']
utils.sortBy(len, L)
self.assertEqual(L, ['z', 'AD', 'abc'])
utils.sortBy(str.lower, L)
self.assertEqual(L, ['abc', 'AD', 'z'])
L = ['supybot', 'Supybot']
utils.sortBy(str.lower, L)
self.assertEqual(L, ['supybot', 'Supybot'])
def testSorted(self):
L = ['a', 'c', 'b']
self.assertEqual(utils.sorted(L), ['a', 'b', 'c'])
self.assertEqual(L, ['a', 'c', 'b'])
def mycmp(x, y):
return -cmp(x, y)
self.assertEqual(utils.sorted(L, mycmp), ['c', 'b', 'a'])
def testNItems(self):
self.assertEqual(utils.nItems('tool', 1, 'crazy'), '1 crazy tool')
self.assertEqual(utils.nItems('tool', 1), '1 tool')
self.assertEqual(utils.nItems('tool', 2, 'crazy'), '2 crazy tools')
self.assertEqual(utils.nItems('tool', 2), '2 tools')
def testItersplit(self):
itersplit = utils.itersplit
L = [1, 2, 3] * 3
s = 'foo bar baz'
self.assertEqual(list(itersplit(lambda x: x == 3, L)),
[[1, 2], [1, 2], [1, 2]])
self.assertEqual(list(itersplit(lambda x: x == 3, L, yieldEmpty=True)),
[[1, 2], [1, 2], [1, 2], []])
self.assertEqual(list(itersplit(lambda x: x, [])), [])
self.assertEqual(list(itersplit(lambda c: c.isspace(), s)),
map(list, s.split()))
self.assertEqual(list(itersplit('for'.__eq__, ['foo', 'for', 'bar'])),
[['foo'], ['bar']])
self.assertEqual(list(itersplit('for'.__eq__,
['foo','for','bar','for', 'baz'], 1)),
[['foo'], ['bar', 'for', 'baz']])
def testIterableMap(self):
class alist(utils.IterableMap):
def __init__(self):
self.L = []
def __setitem__(self, key, value):
self.L.append((key, value))
def iteritems(self):
for (k, v) in self.L:
yield (k, v)
AL = alist()
self.failIf(AL)
AL[1] = 2
AL[2] = 3
AL[3] = 4
self.failUnless(AL)
self.assertEqual(AL.items(), [(1, 2), (2, 3), (3, 4)])
self.assertEqual(list(AL.iteritems()), [(1, 2), (2, 3), (3, 4)])
self.assertEqual(AL.keys(), [1, 2, 3])
self.assertEqual(list(AL.iterkeys()), [1, 2, 3])
self.assertEqual(AL.values(), [2, 3, 4])
self.assertEqual(list(AL.itervalues()), [2, 3, 4])
self.assertEqual(len(AL), 3)
def testFlatten(self):
def lflatten(seq):
return list(utils.flatten(seq))
self.assertEqual(lflatten([]), [])
self.assertEqual(lflatten([1]), [1])
self.assertEqual(lflatten(range(10)), range(10))
twoRanges = range(10)*2
twoRanges.sort()
self.assertEqual(lflatten(zip(range(10), range(10))), twoRanges)
self.assertEqual(lflatten([1, [2, 3], 4]), [1, 2, 3, 4])
self.assertEqual(lflatten([[[[[[[[[[]]]]]]]]]]), [])
self.assertEqual(lflatten([1, [2, [3, 4], 5], 6]), [1, 2, 3, 4, 5, 6])
self.assertRaises(TypeError, lflatten, 1)
def testEllipsisify(self):
f = utils.ellipsisify
self.assertEqual(f('x'*30, 30), 'x'*30)
self.failUnless(len(f('x'*35, 30)) <= 30)
self.failUnless(f(' '.join(['xxxx']*10), 30)[:-3].endswith('xxxx'))
def testSaltHash(self):
s = utils.saltHash('jemfinch')
(salt, hash) = s.split('|')
self.assertEqual(utils.saltHash('jemfinch', salt=salt), s)
def testSafeEval(self):
for s in ['1', '()', '(1,)', '[]', '{}', '{1:2}', '{1:(2,3)}',
'1.0', '[1,2,3]', 'True', 'False', 'None',
'(True,False,None)', '"foo"', '{"foo": "bar"}']:
self.assertEqual(eval(s), utils.safeEval(s))
for s in ['lambda: 2', 'import foo', 'foo.bar']:
self.assertRaises(ValueError, utils.safeEval, s)
def testSafeEvalTurnsSyntaxErrorIntoValueError(self):
self.assertRaises(ValueError, utils.safeEval, '/usr/local/')
def testLines(self):
L = ['foo', 'bar', '#baz', ' ', 'biff']
self.assertEqual(list(utils.nonEmptyLines(L)),
['foo', 'bar', '#baz', 'biff'])
self.assertEqual(list(utils.nonCommentLines(L)),
['foo', 'bar', ' ', 'biff'])
self.assertEqual(list(utils.nonCommentNonEmptyLines(L)),
['foo', 'bar', 'biff'])
def testIsIP(self):
self.failIf(utils.isIP('a.b.c'))
self.failIf(utils.isIP('256.0.0.0'))
self.failUnless(utils.isIP('127.1'))
self.failUnless(utils.isIP('0.0.0.0'))
self.failUnless(utils.isIP('100.100.100.100'))
# This test is too flaky to bother with.
# self.failUnless(utils.isIP('255.255.255.255'))
def testIsIPV6(self):
f = utils.isIPV6
self.failUnless(f('2001::'))
self.failUnless(f('2001:888:0:1::666'))
def testInsensitivePreservingDict(self):
ipd = utils.InsensitivePreservingDict
d = ipd(dict(Foo=10))
self.failUnless(d['foo'] == 10)
self.assertEqual(d.keys(), ['Foo'])
self.assertEqual(d.get('foo'), 10)
self.assertEqual(d.get('Foo'), 10)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

47
test/test_webutils.py Normal file
View File

@ -0,0 +1,47 @@
###
# Copyright (c) 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 testsupport import *
import supybot.webutils as webutils
class WebutilsTestCase(SupyTestCase):
def testGetDomain(self):
self.assertEqual(webutils.getDomain('http://slashdot.org/foo/bar.exe'),
'slashdot.org')
if network:
def testGetUrlWithSize(self):
url = 'http://slashdot.org/'
self.failUnless(len(webutils.getUrl(url, 1024)) == 1024)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: