mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-26 12:49:24 +01:00
Initial import.
This commit is contained in:
parent
dbfec8afb9
commit
8ca625d339
7
ACKS
Normal file
7
ACKS
Normal 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
10
BUGS
Normal 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.
|
67
DEVS
Normal file
67
DEVS
Normal 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
110
INSTALL
Normal 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
28
LICENSE
Normal 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
38
README
Normal 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
257
RELNOTES
Normal 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
46
plugins/Admin/__init__.py
Normal 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
48
plugins/Admin/config.py
Normal 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
349
plugins/Admin/plugin.py
Normal 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
117
plugins/Admin/test.py
Normal 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:
|
||||
|
49
plugins/Channel/__init__.py
Normal file
49
plugins/Channel/__init__.py
Normal 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
82
plugins/Channel/config.py
Normal 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
822
plugins/Channel/plugin.py
Normal 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
190
plugins/Channel/test.py
Normal 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:
|
||||
|
47
plugins/Config/__init__.py
Normal file
47
plugins/Config/__init__.py
Normal 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
48
plugins/Config/config.py
Normal 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
270
plugins/Config/plugin.py
Normal 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
83
plugins/Config/test.py
Normal 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
49
plugins/Misc/__init__.py
Normal 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
63
plugins/Misc/config.py
Normal 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
597
plugins/Misc/plugin.py
Normal 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
285
plugins/Misc/test.py
Normal 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
47
plugins/Owner/__init__.py
Normal 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
55
plugins/Owner/config.py
Normal 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
675
plugins/Owner/plugin.py
Normal 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
109
plugins/Owner/test.py
Normal 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
47
plugins/User/__init__.py
Normal 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
48
plugins/User/config.py
Normal 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
434
plugins/User/plugin.py
Normal 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
111
plugins/User/test.py
Normal 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
350
scripts/supybot
Normal 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
142
scripts/supybot-adduser
Normal 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
296
scripts/supybot-newplugin
Normal 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
618
scripts/supybot-wizard
Normal 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
150
setup.py
Normal 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
111
src/ansi.py
Normal 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
1394
src/callbacks.py
Normal file
File diff suppressed because it is too large
Load Diff
475
src/cdb.py
Normal file
475
src/cdb.py
Normal 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
893
src/commands.py
Normal 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
995
src/conf.py
Normal 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
440
src/dbi.py
Normal 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
218
src/drivers/Socket.py
Normal 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
141
src/drivers/Twisted.py
Normal 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
220
src/drivers/__init__.py
Normal 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
239
src/fix.py
Normal 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
1105
src/ircdb.py
Normal file
File diff suppressed because it is too large
Load Diff
991
src/irclib.py
Normal file
991
src/irclib.py
Normal 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
767
src/ircmsgs.py
Normal 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('&', '&')
|
||||
s = s.replace('"', '"')
|
||||
s = s.replace('<', '<')
|
||||
s = s.replace('>', '>')
|
||||
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
671
src/ircutils.py
Normal 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
405
src/log.py
Normal 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
138
src/privmsgs.py
Normal 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
140
src/questions.py
Normal 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
623
src/registry.py
Normal 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
150
src/schedule.py
Normal 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
419
src/structures.py
Normal 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
77
src/unpreserve.py
Normal 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
875
src/utils.py
Normal 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
129
src/webutils.py
Normal 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
202
src/world.py
Normal 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
570
test/test_callbacks.py
Normal 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
138
test/test_commands.py
Normal 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
127
test/test_fix.py
Normal 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
564
test/test_ircdb.py
Normal 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
457
test/test_irclib.py
Normal 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
240
test/test_ircmsgs.py
Normal 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
354
test/test_ircutils.py
Normal 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
44
test/test_plugins.py
Normal 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
65
test/test_privmsgs.py
Normal 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
171
test/test_registry.py
Normal 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
78
test/test_schedule.py
Normal 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:
|
||||
|
91
test/test_standardSubstitute.py
Normal file
91
test/test_standardSubstitute.py
Normal 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
614
test/test_structures.py
Normal 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
411
test/test_utils.py
Normal 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
47
test/test_webutils.py
Normal 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:
|
||||
|
Loading…
Reference in New Issue
Block a user