Compare commits

...

No commits in common. "gh-pages" and "v0.77.2" have entirely different histories.

270 changed files with 57733 additions and 1546 deletions

3
.cvsignore Normal file
View File

@ -0,0 +1,3 @@
*.pyc
*.pyo
*~

View File

@ -1,8 +0,0 @@
root = true
[*]
end_of_line = lf
trim_trailing_whitespace = false
insert_final_newline = true
charset = utf-8
indent_style = space

1
.gitattributes vendored
View File

@ -1 +0,0 @@
* text=auto eol=lf

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @Mikaela

View File

@ -1,5 +0,0 @@
/** @format */
{
extends: ["local>Mikaela/shell-things:.renovate-shared"],
}

View File

@ -1,25 +0,0 @@
name: HTML5 Validator
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Required will all actions
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Jekyll build
run: |
bundle exec jekyll build --drafts --profile
- name: Checks HTML5 validity
uses: Cyb3r-Jak3/html5validator-action@v7.2.0
with:
root: _site/
blacklist: n r or ir

32
.gitignore vendored
View File

@ -1,32 +0,0 @@
*.mo
*.py[cdo]
*~
.*.swp
.swp
MANIFEST
backup/
build/
debian/compat
debian/files
debian/limnoria*
debian/python-module-stampdir/
dist/
docs/_build/
docs/plugins/
limnoria.egg-info/
merge.sh
nano.save
push.sh
py3k/
src/version.py
supybot.egg-info/
test-conf/
test-data/
test-logs/
src/version.py
_site
.sass-cache
vendor/
.bundle
node_modules/
pnpm-lock.yaml

View File

@ -1,83 +0,0 @@
# @format
# SPDX-FileCopyrightText: 2023 Aminda Suomalainen <suomalainen+git@mikaela.info>
#
# SPDX-License-Identifier: CC0-1.0
# See https://pre-commit.com for more information
# See https://pre-commit.ci for more information
ci:
# I don't need so many duplicated notifications on the same thing as I keep
# autoupdating manually too. Besides it just creates extra branch I never
# touch.
# https://github.com/pre-commit-ci/issues/issues/83
autoupdate_schedule: quarterly
skip: [pnpm-install-dev, prettier]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
args: ["--markdown-linebreak-ext", "md,markdown"]
exclude_types: [svg, tsv]
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-shebang-scripts-are-executable
- id: destroyed-symlinks
- id: detect-private-key
- id: fix-byte-order-marker
- id: check-merge-conflict
- id: mixed-line-ending
args: [--fix=auto]
- id: pretty-format-json
args: [--autofix, --no-ensure-ascii]
- repo: https://github.com/pre-commit-ci/pre-commit-ci-config
rev: v1.6.1
hooks:
- id: check-pre-commit-ci-config
- repo: https://github.com/thlorenz/doctoc
rev: v2.2.0
hooks:
- id: doctoc
args: [--update-only, --notitle]
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0
hooks:
- id: check-dependabot
- id: check-github-workflows
- id: check-gitlab-ci
# - repo: https://github.com/fsfe/reuse-tool
# rev: v3.0.2
# hooks:
# - id: reuse
- repo: local
hooks:
- id: pnpm-install-dev
name: Install pnpm dev dependencies
entry: corepack pnpm install -D
language: system
always_run: true
#verbose: true
pass_filenames: false
- id: prettier
name: prettier
entry: corepack pnpm exec prettier --cache --ignore-unknown --write
language: system
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: "3.2.1"
hooks:
- id: editorconfig-checker
alias: ec
args: [-disable-max-line-length]

View File

@ -1,5 +0,0 @@
_includes/
_layouts/
_sass/
css/
feed.xml

View File

@ -1 +0,0 @@
{}

View File

@ -1 +0,0 @@
3.4.2

View File

@ -1,4 +0,0 @@
# @format
language: ruby
script: "bundle exec jekyll build"

5
ACKS Normal file
View File

@ -0,0 +1,5 @@
johhnyace, who gave me the modem that helped me tremendously in development.
bwp, who rewrote the Http.weather command, and also currently is hosting the
example "supybot" in #supybot on OFTC and Freenode.
sweede, for hosting the "main" supybot for awhile.

11
BUGS Normal file
View File

@ -0,0 +1,11 @@
I'm sure there are tons of them. When you find them, send them to us and we'll
fix them ASAP. I'd love to have a bugless bot someday...
(BTW, 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:
Sometimes something bolded is split between chunks (the chunks you use @more
to read). This throws off the bolding of the "(2 more messages)" part, and
the bolding is lost when the next message starts. This would involve way too
much work to be fixed.

1
CNAME
View File

@ -1 +0,0 @@
supybot.mikaela.info

689
ChangeLog Normal file
View File

@ -0,0 +1,689 @@
2004-04-16 Jeremy Fincher <jemfinch@supybot.org>
* Version 0.77.2!
* Fixed numerous bugs, high and low, big and small and
in-between. Definitely worthy of a release.
* Added supybot.plugins.ChannelLogger.includeNetworkName, so the
logs aren't strangified when relaying between channels.
* Added a --capability option to User.list, to allow people to
list all the users possessing a certain capability. The main
reason this was added is so jemfinch can tell who owns which
Supybots on #supybot :)
* Added Utilities.success, mostly for making aliases such that
they can respond with success if no errors were encountered in
any nested commands.
* Changed the name of the new filter, colorstrip, to be
stripcolor. Better now than after it was highly established :)
* Added configuration variables
supybot.plugins.Services.NickServ.password (which replaces the
old supybot.plugins.Services.password) for specifying the
NickServ password, as well as
supybot.plugins.Services.ChanServ.{op,halfop,voice}, which
determine what the bot should request of ChanServ when it
identifies or joins a channel. These latter variables are, of
course, channel variables.
* Added configuration variable
supybot.plugins.Babelfish.languages (which replaces the old
supybot.plugins.Babelfish.disabledLanguages) for specifying
which languages will be translated/spoken.
* Fixed bug #863601, plugin BadWords fails on color codes.
* Replaced Sourceforge.{rfe,bug} with Sourceforge.tracker, which
can query any tracker type (not just RFEs and bugs) and responds
with more information, a la trackerSnarfer.
* Added supybot.log.individualPluginLogfiles, which determines
whether plugin logs will be logged to their individual logfiles
in addition to the misc.log logfile.
* Added supybot.plugins.WordStats.ignoreQueries, which, when
true, makes the bot ignore queries (and not increment its word
statistics).
* Added the LogToIrc plugin, for sending logs to an IRC
channel or nick. Useful for traceback notification and whatnot.
* Changed supybot.log.timestampFormat to specially handle the
empty string -- if it's set to the empty string, it will log
times in seconds-since-epoch format.
* Added supybot.plugins.Weather.convert, which determines whether
or not Weather.{weather,cnn,ham} will convert temperature to the
configured temperatureUnit.
* Changed User.setpassword not to require the <old password> to
be correct if the requesting user has the owner capability (and
isn't the owner himself).
* Added ircutils.strip{Bold,Reverse,Underline,Formatting}, which
will remove the specified formatting or all forms of formatting
in the case of stripFormatting.
2004-04-09 Jeremy Fincher <jemfinch@supybot.org>
* Version 0.77.1!
* Added supybot.reply.errorWithNotice to make the bot give its
error messages in a notice.
* Added Filter.colorstrip, an outfilter that strips all color
codes from messages.
* Added supybot.plugins.BadWords.{replaceMethod, nastyChars,
simpleReplacement, requireWordBoundaries}; see the associated
help strings to find out what they do.
* Added supybot.plugins.Babelfish.disabledLanguages, to disable
certain languages from being translated/spoken.
* Added supybot.reply.maximumMores, to give users the ability to
set the maximum number of "more" chunks replies will generate.
* Added supybot.reply.truncate, to turn off the normal chunking
of replies that later can be retrieved with the more command.
Setting this variable to On means that no chunks will ever be
created.
* Added supybot.plugins.Enforcer.takeRevengeOnOps, which makes
the bot even take revenge on #channel,ops who try to violate the
channel configuration. Of course, such people can change the
channel configuration to their bidding, but it's a decent
protection against errant botscripts or something.
* Added supybot.plugins.Channels.alwaysRejoin, to make the bot
always rejoin when it's kicked.
* Added supybot.databases.users.hash, allowing those running bots
to specify the default hashed/unhashed state of user passwords in
the config file.
* Added Debian.bug, which retrieve bug info from Debian's BTS.
* Changed Relay.names to Relay.nicks, to be consistent with
Channel.nicks.
* Added supybot.brackets, a configuration variable for specifying
which set of matching brackets to use for nested commands. Valid
values are [] (the default), {}, <>, and ().
* Added a configuration variable to ChannelLogger, timestamp,
which determines whether the bot will timestamp its logfiles.
This is a channel-specific variable, of course.
* Updated ChannelLogger not to log messages starting with
[nolog], in order to allow private information to remain private.
* Added a configuration variable to ChannelLogger,
flushImmediately, to cause all channel logs to be flushed every
time they're modified, for those people who like to read the logs
through tail -f or something similar.
* Updated WordStats to allow removing of tracked words.
* Updated Seen.seen to accept no arguments and return the last
message spoken by anyone.
* Updated the Herald plugin to use the standard substitute until
we get it updated to use commands as heralds instead of plain
strings.
* Updated echo to use the standard substitute on its reply.
* Updated Network.whois so that it can now retrieve
information on all domains.
* Updated the Weather plugin to be retrieve weather from either
hamweather.net or cnn.com. Also added
supybot.plugins.Weather.{command,temperatureUnit} so that you can
specify which command (Weather.cnn or Weather.ham) to use when
Weather.weather is called and in which unit (C, F, K) to report
the weather.
* Updated standard replies (such as supybot.replies.success, etc.)
to use the standard substitute (such as $nick, $who, $channel,
etc.) in their text.
* Fixed snarfers to respect lobotomies.
* Fixed Admin.join not to add the channel to the supybot.channels
registry entry if joining the channel wasn't successful.
2004-02-20 Jeremy Fincher <jemfinch@supybot.org>
* Version 0.77.0!
* Changed the format of the user and channel databases to
something much more readable and user-editable. Be sure, if
you're upgrading, to run tools/ircdbConvert.py on your user and
channel databases *BEFORE* installing the new version.
* Updated almost every document we have, and added a
GETTING_STARTED document. Yay!
* Added several new options for Channel.kban: --exact, --nick,
--user, --host, for more flexibility in determining what the
banmask is.
* Added a Scheduler plugin for scheduling events to occur at
specific times.
* Added a Herald plugin for announcing to the channel the arrival
of certain users.
* Changed the reply method of the irc object given to plugins not
to require a msg object.
* Added inter-factoid references for MoobotFactoids. Simply
define a factoid as "see <factoid-key>" (and nothing else) and it
will automatically go pick up the value for that factoid and
display it in its place. Sort of a "symlink" for MoobotFactoids.
* Added the ability to reply to factoids without the "whatis" in
the Factoids plugin. Now, you can use "@foo" instead of "@whatis
foo".
* Added --{from,to} and --sent options to Note.sent.
* Changed Note.get to simply be "note". Instead of "note get
145", you should use "note 145".
* Changed channel capabilities to use a comma to separate the
channel from the capability instead of a period. This is because
channels can include periods (but not commas) and since we now
allow "plugin.command" capabilities, there's no way to know
whether a given dot is separating the channel from the capability
or the plugin from the command.
* Removed Admin.setprefixchar, since it's unneeded with the new
configuration.
* Removed Status.bestuptime, since it was buggy and no one felt
like maintaining it.
* Added Http.cyborg, a fun little command for getting cyborg
abbreviations for names.
* Added Sourceforge.totalbugs and Sourceforge.totalrfes
* Removed Owner.{set,unset} since they matter not.
* Made the smileys and frowns configurable in ChannelDB.
* Added a vast array of new configurables -- check out the various
config commands in Configurable plugins to see.
* Added better error reporting for Admin.join, explaining to the
user if the bot can't join the channel.
* Added a title-snarfer to the URL plugin.
* Added Relay.command, a command for sending commands to the bot
on a different network.
* Added Bugzilla.search, a new command for searching a bugzilla
installation
* Added an INVITE handler in Admin, allowing users with the admin
capability to INVITE the bot to a channel. There's also a config
variable, alwaysJoinOnInvite, that will cause the bot to join
a channel whenever someone invites it to a channel, not just users
with the admin capability.
* Added conf.requireChannelCommandsToBeSentInChannel for requiring
all channel-related commands to be sent in the channel.
* Added conf.followIdentificationThroughNickChanges for having the
bot update user's identified hostmask when they change nicks.
* Added conf.replyWhenNotAddressed, a configuration variable for
having the bot always attempt to parse a message as a command.
* Added conf.replyWhenAddressedByNick, a configuration variable
for having the bot *not* respond when addressed by his nick.
* Added conf.replyWithNickPrefix, a configuration variable for
having the bot not prefix the nick of the person giving a command
to the reply.
* Changed all "zegrep" stuff to "zgrep -e" stuff, since zegrep is
on fewer platforms than zgrep.
2003-12-12 Jeremy Fincher <jemfinch@supybot.org>
* Version 0.76.1! The birthday release!
* Fixed a bug in irclib.py that could cause the bot to loop,
connecting and disconnecting from the server because it didn't
think the server responded to its PING.
* Fixed a bug in the Services implementation that could cause
the bot to continually loop, connecting and disconnecting from the
server.
* Fixed Misc.help to follow the same default plugin rules that the
rest of the bot follows.
* Added better error reporting to Admin.join, so the various error
conditions that might make the bot incapable of joining a channel
will be reported.
* Updated RootWarner to be configurable, offering each channel the
ability to define whether or not people who IRC as root will be
warned, what that warning will be, and whether or not they should
be kicked.
* Added a configurable 'topic-sync' to the Relay plugin to
determine whether the topics of the two channels should be kept
synchronized.
* Added Lookup.search for searching the various loaded lookups.
* Added Topic.reorder, a new command for reordering the topics in a
specific manner.
* Added Topic.list, a new command for listing the topics in a
channel (mostly in order to help out with Topic.reorder :))
* Added Http.extension, a new command to retrieve file extension
information from filext.com.
* Updated Todo.remove to allow removing multiple taskids
* Updated Relay.whois to include a user's away status and identified
state, if the server supports it.
* Added utils.sorted, which does what list.sorted will do when 2.4
is released.
* Added ircutils.isCtcp, for determining whether or not a message
is a CTCP message.
2003-12-6 Jeremy Fincher <jemfinch@supybot.org>
* Version 0.76.0!
* Added a "trusted" capability that defaults to off (-trusted) and
is required for Utilities.re (with which it's possible to DoS the
bot) and for the new Math.icalc (with which it is trivially
possible to DoS the bot).
* Added Math.icalc, a new command for doing integer arithmetic.
It requires the "trusted" capability.
* Added the Fun.ping command, because MozBot has it.
* Added Note.unsend command, to allow users to "unsend" notes
they've sent but whose recipient has not yet read.
* Added Channel.{deop,devoice,dehalfop,kick}.
* Added Http.size and Http.doctype and Http.headers to retrieve
various meta-information on URLs.
* Added a ranking to ChannelDB.wordstats.
* Added Karma.most for determining various "mosts" in the Karma
database.
* Added User.list command to list registered users.
* Added 'roulette' command to Fun plugin.
* Added a Channel.lobotomies command to list the channels in which
the bot is lobotomized.
* Added a swap function to Math.rpn.
* Changed the name of User.changeusername to User.changename.
* Changed the logging infrastructure significantly; each plugin
now has its own logger (and thus logfile), in addition to being
logged in the main logfile.
* Fixed bug in which the bot wouldn't rejoin channels after a
reconnect. Thank the Lord for tcpkill :)
* Fixed Http.freshmeat for projects with a space in their names.
* Changed RSS so RSS feed commands may be added while the bot is
running, and so added RSS feed commands are in the RSS plugin.
* Changed Lookup so added lookups are added in the Lookup plugin
itself so there's no dependency on Alias, and so loaded lookups
can be seen via 'list Lookup'.
* Fixed bug #850931 as well as several other minor bugs in
Utilities.re.
* Fixed bug #851254, Factoids.whatis didn't work on numeric keys.
* Added the ability to turn on/off the showing of ids in FunDB
excuse/insult/praise/lart.
* Added the to= keyword argument to the various reply functions to
you can specify a target to send the message to.
* Changed socketDrivers to allow more time (10x more, to be exact)
for connecting to the network than for the normal read.
* Fixed a bug in Http.weather that would cause an ugly error
message for a command like "weather hell".
* Added the number of strings to the Fun.object output.
* Fixed bug in RSS.configure; no aliases could be added.
* Changed Alias.freeze to Alias.lock.
* Fixed sorting in Status' uptime database.
* Updated the Gameknot tests for expired games, and updated the
Gameknot plugin to handle such links in its snarfer.
* Added a 'server' attribute to Irc objects to unify the way to
access such information.
* Added revision command for finding out the revision of the files
in a running bot; also added __revision__ strings so CVS would be
happy to keep such information for us :)
* Fixed bug #848475 -- bad error message from regexp-expecting
commands.
* Stopped listing the plugin dispatcher command in the list of
commands for that plugin.
* Fixed bug in Gameknot.gkstats when retrieving the stats for
users who haven't yet played a game.
* Added a numUsers() method to ircdb.UsersDB (ircdb.users).
2003-11-18 Jeremy Fincher <jemfinch@users.sf.net>
* Changed commands so that plugins no longer have to make sure
that their commands are unique within the bot. Ambiguous commands
will reply with an error message and instruct the user to
disambiguate the command by prefixing with the appropriate plugin
name.. Many commands that formerly contained the plugin name (or a
portion thereof) have had it removed, and many plugins have had
their names changed so prefixing a command with the plugin name is
less bulky. Rather than list each individual example, you can
read the plugin documentation posted at ***TODO***
* Renamed numerous plugins: OwnerCommands became Owner,
AdminCommands became Admin, ChannelCommands became Channel,
MiscCommands became Misc, UserCommands became User, URLSnarfer
became URL, Notes became Note, FunCommands became Fun, IMDB became
Movies, and Aliases became Alias.
* Made aliases persistent across reloads/bot restarts. You should
probably change your botscripts not to add the aliases onStart, but
(assuming those aliases don't change) it should still work fine.
* Added the ability for users to specify that their passwords
should be hashed on disk.
* Added MoobotFactoids plugin for moobot-style factoids (which are
meant to mimic blootbot-style factoids). People used to
traditional IRC bot factoids plugins will probably find this
plugin more to their taste than Factoids.
* Added Ebay plugin for snarfing eBay URLs as well as getting info on
certain auctions.
* Added monitoring of occurrences of certain words on a per-user
basis, adding two new commands to ChannelDB (addword and
wordstats).
* Added Bugzilla module for accessing various data in Bugzilla
pages.
* Added QuoteGrabs module which allows people to grab interesting
quotes from other people and store them in the bot for later retrieval
(also optionally have the bot randomly snarf quotes).
* Added a "change" command to change factoid values in the Factoids
plugin.
* Added Dunno plugin as an optional replacement for the boring 'no
such command' response.
* Changed FundB to allow accessing excuses, larts, and praises by
id.
* Added substitutions for 'me' and 'my' in insult/praise/lart.
* Added 'change' and 'old' commands for News.
* Added ASPN Python Cookbook URL snarfer.
* Moved karma out of ChannelDB and into its own Karma plugin.
* Moved uptime-related commands to from FunDB to the Status plugin.
* Renamed the Network.internic command to whois, since we can now fix
ambiguity by prefixing the plugin name.
* Removed the "googlesite" function.
* Removed "dictserver" command in favor of using the Configurable
framework with the Dict plugin instead.
* Removed TwistedCommands plugin to the sandbox; the one command
it provided (dict) is now better provided in the Dict plugin.
* Removed the Moobot plugin (the commands were moved to the Fun
plugin or dropped entirely).
* Removed all example strings from plugins. To be replaced with an
automated process later.
* Converted several plugins to the new Configurable plugin type
Plugins modified include Bugzilla, ChannelDB, Dict, Ebay,
Enforcer, Gameknot, Google, Python, Relay, and URL (formerly
URLSnarfer).
* Changed ChannelDB database to use integer user ids instead of text
usernames.
* Changed Http.geekquote to use multiline geekquotes (and removed
the option to do so, since it's now the default).
* Added a --id switch to geekquote to pick a specific geekquote.
* Changed most commands in News to require the 'news' capability.
* Changed Relay.names output to show (and sort by) status in the
channel.
* Removed 'relaycolor' command in favor of Configurable framework.
* Added total memory usage to 'cpustats' output for several *nix
platforms.
* Removed the total percentage of CPU time from 'cpustats'. Not
only was it inaccurate, but we needed the room for memory stats.
* Changed Topic.shuffle to ensure that the topic is actually
shuffled.
* Changed all commands which take an index (various Topic and
Factoids commands) to index from 1 instead of 0.
* Fixed several bugs in Unix.spell whereby the bot could be
frozen.
* Changed the name of the "bug" command in the AdminCommands
plugin to "reportbug" instead.
* Added QUIT stat-keeping to ChannelDB.
* Removed the OwnerCommands.say command; it wasn't useful enough,
and is so easily written that anyone can have it back if they want
it.
* Changed OwnerCommands.load (and reload) to be case-insensitive,
so "load funcommands" works just as well as "load FunCommands".
* Changed the keyword parameter "needed" to privmsgs.getArgs to be
"required" instead. It just sounds better, works with "optional"
better, and we won't get an oppurtunity later to change it.
* Updated IrcObjectProxy.reply to allow a "notice" boolean keyword
to determine whether or not to reply via a notice.
* Added privmsgs.urlSnarfer, a wrapper around snarfer methods that
handles both the threading and the limiting of replies so loops
between two snarfing bots can't happen.
* Added structures.PersistentDictionary for dictionaries that
should be saved to a file in repr()ed form.
* Added structures.TwoWayDictionary for dictionaries that should
map keys to values and vice versa.
* Added a curry function to fix.py for doing currying (if you
don't know what it is, don't worry about it :))
* Added utils.depluralize to do the opposite of utils.pluralize.
* Added utils.safeEval for safe evaluation of Python data
structures (lists, dictionaries, tuples, numbers, strings, etc.,
but no operations on them).
* Added utils.saltHash for handling the hashing of passwords with
a salt.
* Added plugins.standardSubstitute to do standard substitutions
for things like $who, $nick, $channel, etc.
* Added plugins.Configurable, a plugin mixin that allows easy
specification and persistence of configurable data on a global and
per-channel basis.
* Fixed plugins.ChannelDBHandler (and added plugins.DBHandler) to
be threadsafe, so threaded plugins could still use a database, and
non-threaded database-using plugins could still receive the
results of a threaded command.
* Removed ircutils.privmsgPayload and ircutils.shrinkList, both of
which existed prior to the addition of more, and aren't needed
anymore.
2003-10-12 Jeremy Fincher <jemfinch@users.sf.net>
* Version 0.73.1!
* Fixed a bug in Math.{calc,rpn} where certain functions
("degrees" in particular) that didn't like complex arguments would
fail on numbers on which they shouldn't.
* Added an optional "key" argument to ChannelCommands.cycle.
* Fixed bolding in supybot-wizard.py.
* Fixed handling of the secure flag by ircdb.IrcUser.setAuth;
previously it didn't prevent someone with an unmatched hostmask
from identifying.
* Fixed imports in the DCC plugin.
* Fixed a bug where the bot would not reply to nick-addressed
messages on a channel if his nick wasn't entirely lowercased.
* Fixed the Relay plugin to relay topic changes; an oversight not
caught earlier because supybot has for a long time managed our
topics.
* Fixed a bug in the Services plugin where the bot would ghost
himself if his nick didn't match in case the nick given.
* Added the ability for PrivmsgCommandAndRegexp to have regexps
that are called *after* callbacks.addressed has been called on the
message -- see ChannelDB.{increase,decrease}Karma for an example.
* Fixed bug in supybot-wizard.py where plugins without configure
functions would raise an uncaught exception.
* Fixed debincoming to work since the removal of baseplugin; it
was missing an import.
* Fixed MiscCommands.doPrivmsg to send an IrcObjectProxyRegexp to
the replyWhenNotCommand function.
* Fixed debversion to display the correct output when no matching
packages were found.
* Fixed ChannelDB to import conf; karma didn't work otherwise.
* Fixed a bug in the Enforcer plugin that would cause the bot to
ban everyone from the channel whenever ChanServ deopped someone.
* Changed the "whois" command in the Network plugin to "internic"
instead.
2003-10-05 Jeremy Fincher <jemfinch@users.sf.net>
* Version 0.73.0!
* Added the News plugin, news handling for channels.
* Changed the initial character of anti capabilities to '-'
instead of '!'. '!' can be the initial character in a channel
name, and so any command using getChannel and accepting a
capability as its first argument (several in ChannelCommands) will
have difficulties (the channel then *must* be specified since
getChannel will consider !capability to be a channel name). Note
that this means you'll need to either remove and re-create or edit
your config files to change ! to - in capabilities.
* Removed the "cvsup" command; it's been useless since we had a
global install, and can only serve to confuse.
* Added a "private" command to MiscCommands to allow users to
specify that a response should be made in private.
* Added a "secure" flag to user database records that *requires*
that one of the user's hostmasks match if the user is to be
recognized. This flag may be set with the "setsecure" command.
* Added a convert command to the Math plugin. More conversions
are necessary, if anyone's interested in doing it. The available
units are available via the "units" command.
* Fixed the pydoc command to allow access to standard Python
modules that are shared libraries rather than pure Python code.
* Added a "Python" plugin and moved FunCommands.{pydoc,zen} to
it.
* Fixed the supybot- scripts to use optparse so they now
accept many command line options (but most importantly, the --help
option :))
* Added a debincoming command to the Debian plugin; it searches
the available packages at http://incoming.debian.org/
* Moved the "bug" command from MiscCommands to AdminCommands in
order to prevent possible abuse.
* Changed ChannelDB.seen to default to using nicks; a --user
option can be passed to use the old behavior. Note that this
means you'll have to update your ChannelDB database; use this
SQL statement to do so:
CREATE TABLE nick_seen (
name TEXT UNIQUE ON CONFLICT REPLACE,
last_seen TIMESTAMP,
last_msg TEXT
);

66
DEVS Normal file
View File

@ -0,0 +1,66 @@
These are the developers of Supybot, in approximate order of ____.
Jeremy Fincher (jemfinch) is a Computer Science student at The Ohio State
University. He spends most of his free time with his girlfriend Meg, but
also plays chess and is trying to break into the Rugby world. He hopes to
graduate with good enough grades to go to law school 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 attribute anything that isn't
otherwise attributed to other people to him.
Daniel DiPaolo (Strike/ddipaolo) is a lazy Texan punk with no job 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.

View File

@ -1,8 +0,0 @@
source "https://rubygems.org"
ruby file: ".ruby-version"
# For now this is a GitHub Pages hosted website.
# Ref: https://github.com/Mikaela/mikaela.github.io/issues/153
gem 'github-pages', group: :jekyll_plugins
gem 'jekyll-seo-tag'
# Required for `bundle exec jekyll serve`
gem "webrick"

View File

@ -1,286 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
colorator (1.1.0)
commonmarker (0.23.11)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
csv (3.3.3)
dnsruby (1.72.4)
base64 (~> 0.2.0)
logger (~> 1.6.5)
simpleidn (~> 0.2.1)
drb (2.2.1)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
ethon (0.16.0)
ffi (>= 1.15.0)
eventmachine (1.2.7)
execjs (2.10.0)
faraday (2.13.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
ffi (1.17.1-x86_64-linux-gnu)
forwardable-extended (2.6.0)
gemoji (4.1.0)
github-pages (232)
github-pages-health-check (= 1.18.2)
jekyll (= 3.10.0)
jekyll-avatar (= 0.8.0)
jekyll-coffeescript (= 1.2.2)
jekyll-commonmark-ghpages (= 0.5.1)
jekyll-default-layout (= 0.1.5)
jekyll-feed (= 0.17.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.16.1)
jekyll-include-cache (= 0.2.1)
jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.8.0)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.2.0)
jekyll-theme-cayman (= 0.2.0)
jekyll-theme-dinky (= 0.2.0)
jekyll-theme-hacker (= 0.2.0)
jekyll-theme-leap-day (= 0.2.0)
jekyll-theme-merlot (= 0.2.0)
jekyll-theme-midnight (= 0.2.0)
jekyll-theme-minimal (= 0.2.0)
jekyll-theme-modernist (= 0.2.0)
jekyll-theme-primer (= 0.6.0)
jekyll-theme-slate (= 0.2.0)
jekyll-theme-tactile (= 0.2.0)
jekyll-theme-time-machine (= 0.2.0)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.13.0)
kramdown (= 2.4.0)
kramdown-parser-gfm (= 1.1.0)
liquid (= 4.0.4)
mercenary (~> 0.3)
minima (= 2.5.1)
nokogiri (>= 1.16.2, < 2.0)
rouge (= 3.30.0)
terminal-table (~> 1.4)
webrick (~> 1.8)
github-pages-health-check (1.18.2)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (>= 4, < 8)
public_suffix (>= 3.0, < 6.0)
typhoeus (~> 1.3)
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.8.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jekyll (3.10.0)
addressable (~> 2.4)
colorator (~> 1.0)
csv (~> 3.0)
em-websocket (~> 0.5)
i18n (>= 0.7, < 2)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
webrick (>= 1.0)
jekyll-avatar (0.8.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.2.2)
coffee-script (~> 2.2)
coffee-script-source (~> 1.12)
jekyll-commonmark (1.4.0)
commonmarker (~> 0.22)
jekyll-commonmark-ghpages (0.5.1)
commonmarker (>= 0.23.7, < 1.1.0)
jekyll (>= 3.9, < 4.0)
jekyll-commonmark (~> 1.4.0)
rouge (>= 2.0, < 5.0)
jekyll-default-layout (0.1.5)
jekyll (>= 3.0, < 5.0)
jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.16.1)
jekyll (>= 3.4, < 5.0)
octokit (>= 4, < 7, != 4.4.0)
jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0)
jekyll-mentions (1.6.0)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.3)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.6.0)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.13.0)
gemoji (>= 3, < 5)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
json (2.10.2)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.6)
mercenary (0.3.6)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.25.5)
net-http (0.6.0)
uri
nokogiri (1.18.7-x86_64-linux-gnu)
racc (~> 1.4)
octokit (4.25.1)
faraday (>= 1, < 3)
sawyer (~> 0.9)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.1.1)
racc (1.8.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.4.1)
rouge (3.30.0)
rubyzip (2.4.1)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
securerandom (0.4.1)
simpleidn (0.2.3)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (1.8.0)
uri (1.0.3)
webrick (1.9.1)
PLATFORMS
x86_64-linux
DEPENDENCIES
github-pages
jekyll-seo-tag
webrick
RUBY VERSION
ruby 3.4.2p28
BUNDLED WITH
2.6.2

28
LICENSE Normal file
View File

@ -0,0 +1,28 @@
Copyright (c) 2002, 2003, 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 remains subject to its associated license.

42
README Normal file
View File

@ -0,0 +1,42 @@
EVERYONE:
---------
Read LICENSE. It's a 2-clause BSD license, but you should read it anyway.
USERS:
------
Read docs/GETTING_STARTED for an introduction to the bot. Read
docs/CAPABILITIES to see how to use capabilities more to your
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 EXAMPLE to
see some examples of callbacks and commands written for the bot.
Read INTEFACES 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..
Use PyLint. It's even better than PyChecker. A sample .pylintrc file
is included as tools/pylintrc. Copy this to ~/.pylintrc and you'll be
able to check your code with the same stringent guidelines I've found
useful to check my code. (deja vu? :))
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.

View File

@ -1,11 +0,0 @@
<!-- @format -->
# Mikaela's fork of Limnoria.
There are mainly two branches. This one which you are looking at, gh-pages which
is the source of <https://supybot.mikaela.info/>.
**testing** which will be synced with [ProgVal/Limnoria] when needed. It is used
as base for my changes which will be pull requested.
[ProgVal/Limnoria]: https://github.com/ProgVal/Limnoria.git

93
RELNOTES Normal file
View File

@ -0,0 +1,93 @@
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.

View File

@ -1,71 +0,0 @@
---
layout: page
title: Ignoring RelayBot
permalink: /Relaybot.html
---
<!-- @format -->
RelayBot is the bot which relays between #supybot,#limnoria at a couple of
networks (TODO/FIXME, which ones?). It is currently using the
[LinkRelay](https://github.com/ProgVal/Supybot-plugins/tree/master/LinkRelay)
plugin to do this.
It's sometimes considered as annoyance as it has lately mostly spammed with join
(part messages aren't working, because of a bug (2014-06-23)) messages of people
who usually say nothing and this is why this page is here to tell how to ignore
it on various client.
We(who? I?) encourage you to ignore only notices from RelayBot instead of
everything as there are people whom should be heard at OFTC (mainly main Supybot
developer). (TODO/FIXME: is this the case in 2021?)
Related links:
- [LinkRelay plugin](https://github.com/ProgVal/Supybot-plugins/tree/master/LinkRelay)
- [Feature request for smart filtering of joins/quits/parts](https://github.com/ProgVal/Supybot-plugins/issues/66)
- [Feature request for RELAYMSG for more native look&feel](https://github.com/ProgVal/Supybot-plugins/issues/338)
Hostmask of RelayBot on Libera.Chat 2021-06-06:
- `RelayBot!~limnoria@helium.progval.net`
- This is absolute hostmask, also known as NUH (`nick!user@host`)
- `RelayBot*!*@helium.progval.net`
- This is recommended hostmask as it matches RelayBot even if it cannot use
it's primary nickname or networks cannot connect to it's identd.
## HexChat
From the "Window" menu you can find "Ignore list". Click "Add" and add one of
the hostmasks mentioned above (the lower is recommended).
Uncheck the other checkboxes than "Notice" and you can close the window and you
won't see spamming.
## KVIRC
I am not primarily KVIRC user and I cannot say anything else than right click
RelayBot and select something that matches only RelayBot.
**WARNING: KVIRC makes it very easy to also ignore pinkieval which you don't
want to do as they are author of Limnoria and help people often!**
## Linkinus
According to another person, there is a GUI where you can easily ignore notices
from specific hostmask.
## WeeChat
`/filter add relaybotnotices * irc_notice+nick_RelayBot *`
This creates a new filter with the name "relaybotnotices" which filters all
notices from the nickname "RelayBot".
---
This page is very likely missing many IRC clients. Could you
[open an issue](https://github.com/mikaela/limnoria/issues) about how to do this
with your IRC client that isn't mentioned here?
---

View File

@ -1,145 +0,0 @@
---
layout: page
title: Security issues
permalink: /Supybot.html
---
<!-- @format -->
Supybot git repository was declared dead on 2018-05-10 and archived on GitHub.
[v0.84.0 was the last release at that time](https://github.com/Supybot/Supybot/releases/tag/v0.84.0).
0.83.4.1 used to be a very common release available through several Linux
distributions for years and thus I made this page, which I guess is now
available more of for historical reasons.
**_WARNING: most of the content originates from 2014!_**
## The issues of 0.83.4.1.
### 1. Anyone can crash it and computer where it's running on
And this is very easy. Just run the command
`!misc last --regexp m/(.*\w){512}/`
where ! is the prefix character.
Misc is loaded by default and cannot be unloaded without modifying the config.
- [Limnoria issue #157](https://github.com/ProgVal/Limnoria/issues/157)
- Fixing commits:
[3526d5d](https://github.com/ProgVal/Limnoria/commit/3526d5dabf587457a43af8bee8d4db21986e8222)
&
[e11dc28](https://github.com/ProgVal/Limnoria/commit/e11dc28025de877b1b6cf059013eef88337b7e44)
- [Ubuntu bug #996947](https://bugs.launchpad.net/ubuntu/+source/supybot/+bug/996947)
- [Debian bug #672214](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=672214)
### 2. The previous wasn't the only way to do this
Everyone can also make the bot count an equation, which brings it and the host
computer down.
For example:
`!math calc factorial(999999)`
This requires Math plugin which comes with Supybot, but isn't load by default.
- [Limnoria issue #354](https://github.com/ProgVal/Limnoria/issues/354)
- Fixing commit:
[695078e](https://github.com/ProgVal/Limnoria/commit/695078edeb91e5ff1eec728fedf0e0c27b55c505)
- [Ubuntu bug #996950](https://bugs.launchpad.net/ubuntu/+source/supybot/+bug/996950)
- [Debian bug 672215](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=672215)
### 3. Anyone can access network services via the bot.
I don't have example command for this, but it happens by nesting "format cut"
and "misc tell".
What does this mean? Anyone can tell the bot to ghost someone else on same
account, take over a channel by telling the bot to give flags (if it has correct
flags), change password of the account and everything else what you do with
network services.
- _This was only reported at IRC and I am unable to find issue report or fixing
commit. ~~Mikaela on 2015-01-04._
### 4. Web page with special characters in \<title\> can be used to send DCC/CTCP commands.
This doesn't mean only things like CTCP actions (also known as /me), but known
problems with old routers ( `FF ? DCC SEND “ff???f??????????????” 0 0 0` ) which
make them reconnect to the internet.
Usage:
- `!web title <malicious.page.here>`
- `!web fetch <malicious.page.here>`
_This was only reported at IRC and I am unable to find issue report or fixing
commit. ~~Mikaela on 2015-01-04._
### 5. Web Titlte/Fetch can be used for DoS
They are vulnerable to queries to servers which have custom headers which can
lead to DoS.
_This was only reported at IRC and I am unable to find issue report or fixing
commit. ~~Mikaela on 2015-01-04._
### 6. QuoteGrabs grab command also works in PM
and can grab private content such as `user register` or `user identify` or with
the case of owner possibly NickServ passwords and others not so nice things.
- _It appears this issue was only reported at IRC._
- Fixing commit:
[a3346343679f3bdf8c77d9efb5a2097e215d51df](https://github.com/ProgVal/Limnoria/commit/a3346343679f3bdf8c77d9efb5a2097e215d51df)
### Are these issues publicly known?
**Of course they are.** Issue reports are below the actual issues.
The first issue has been also used to take down some of
[Ubuntu IRC bots](https://wiki.ubuntu.com/IRC/Bots) several times. At least
UbotX (I don't remember the number) and meetingology.
Some of these issues are fixed in git repository, but most people aren't using
it. If you wish to start using it, please scroll down to installation
instructions lower this page even though [Limnoria] and [gribble] are more
recommended.
### How to avoid them?
You can add anticapability for these commands using `owner defaultcapability`,
but that is only a temporary solution. There can also be other issues.
There are also two active Supybot forks, known as [Limnoria] and [Gribble],
which are actively developed and have fixed these issues. If you want permanent
solution, you should install either of them.
## Possibly interesting links
- [Comparsion of commit activity between Limnoria, Gribble and Supybot](https://www.openhub.net/p/compare?project_0=Limnoria&project_1=Gribble%3A+Support+Bottie&project_2=Supybot).
- [Gribble's modifications to stock Supybot](https://sourceforge.net/p/gribble/wiki/Gribble_Project_Git_Repository/)
- [Limnoria's modifications to Gribble.](https://github.com/ProgVal/Limnoria/wiki/LGC)
- Features of Gribble are fully merged to Limnoria.
Your current botname.conf is **100% compatible with forks**.
[Join Supybot channels on LiberaChat!](ircs://irc.libera.chat:6697/#supybot,#gribble,#limnoria)
[Limnoria]: https://github.com/ProgVal/Limnoria
[Gribble]: http://github.com/nanotube/supybot_fixes
## Installing forks
_This section has been removed in order to not duplicate
[Limnoria's documentation.](http://doc.supybot.aperio.fr/en/latest/use/install.html)_
---
Do you know issue that isn't mentioned here? If it's not already reported,
please report it
on [Limnoria's issue tracker.](https://github.com/ProgVal/Limnoria/issues) If
it's known, but just not reported here,
[please feel free to add it.](https://github.com/Mikaela/limnoria/edit/gh-pages/Supybot.markdown)

11
TODO Normal file
View File

@ -0,0 +1,11 @@
Roughly in order of precedence (the closer to the front of the file,
the more likely it'll be done before next release):
Update the database infrastructure to allow other RDBMSes.
Improved (and perhaps integrated) Services and Enforcer plugins.
To be done as soon as someone with the necessary expertise is found to
help us:
Finish the Infobot plugin.

43
__init__.py Normal file
View File

@ -0,0 +1,43 @@
#!/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
import os.path
installDir = os.path.dirname(sys.modules[__name__].__file__)
srcDir = os.path.join(installDir, 'src')
othersDir = os.path.join(installDir, 'others')
sys.path.insert(0, srcDir)
sys.path.insert(0, othersDir)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,38 +0,0 @@
# @format
theme: minima
title: Mikaela's Supybot site
tagline: Things official documentation may not tell you
author:
name: "Aminda Suomalainen"
url: "https://aminda.eu/"
description: > # this means to ignore newlines until "baseurl:"
Mikaela's Supybot site where nowadays the only content is security issues of
stock Supybot.
baseurl: "" # the subpath of your site, e.g. /blog/
url: "https://supybot.mikaela.info/" # the base hostname & protocol for your site
github_username: Mikaela
lang: en
timezone: Etc/UTC
encoding: utf-8
plugins:
# - jekyll-mentions
- jekyll-redirect-from
- jekyll-sitemap
- jekyll-seo-tag
sitemap:
file: "/sitemap.xml"
include: [robots.txt]
robots: nofollow, noai
icon: https://github.com/ProgVal/Supybot-website/raw/master/static/logo.png
markdown: kramdown
kramdown:
parse_block_html: true
#webmaster_verifications:
#google:
#bing:
defaults:
- scope:
path: "*"
values:
image: https://github.com/ProgVal/Supybot-website/raw/master/static/logo.png

View File

@ -1,32 +0,0 @@
<footer class="site-footer h-card">
<data class="u-url" href="{{ "/" | relative_url }}"></data>
<div class="wrapper">
<h2 class="footer-heading">{{ site.title | escape }}</h2>
<div class="footer-col-wrapper">
<div class="footer-col footer-col-1">
<ul class="contact-list">
<li class="p-name">
<a rel="me prefetch" href="{{ site.author.url }}">{{ site.author.name }}</a><br>
{{ site.title | escape }}
</li>
{%- if site.email -%}
<li><a class="u-email" href="mailto:{{ site.email }}">{{ site.email }}</a></li>
{%- endif -%}
</ul>
</div>
<div class="footer-col footer-col-2">
{%- include social.html -%}
</div>
<div class="footer-col footer-col-3">
<p>{{- site.description | escape -}}</p>
</div>
</div>
</div>
</footer>

View File

@ -1,15 +0,0 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!---->
{%- seo -%}
<!---->
<meta name="robots" content="{% if page.robots %}{{ page.robots }} {% else %}{{ site.robots }} {% endif %}">
<link rel="icon" href="https://github.com/ProgVal/Supybot-website/raw/master/static/logo.png">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#ffb700">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fdfdfd">
<link rel="stylesheet" href="{{ "/assets/main.css" | prepend: site.baseurl }}">
<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ "/feed.xml" | prepend: site.baseurl | prepend: site.url }}">
</head>

View File

@ -1,210 +0,0 @@
:root {
color-scheme: dark light;
}
/**
* Reset some basic elements
*/
body, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, hr,
dl, dd, ol, ul, figure {
margin: 0;
padding: 0;
}
/**
* Basic styling
*/
body {
font-family: $base-font-family;
font-size: $base-font-size;
line-height: $base-line-height;
font-weight: 300;
color: $text-color;
background-color: $background-color;
-webkit-text-size-adjust: 100%;
}
/**
* Set `margin-bottom` to maintain vertical rhythm
*/
h1, h2, h3, h4, h5, h6,
p, blockquote, pre,
ul, ol, dl, figure,
%vertical-rhythm {
margin-bottom: $spacing-unit / 2;
}
/**
* Images
*/
img {
max-width: 100%;
vertical-align: middle;
}
/**
* Figures
*/
figure > img {
display: block;
}
figcaption {
font-size: $small-font-size;
}
/**
* Lists
*/
ul, ol {
margin-left: $spacing-unit;
}
li {
> ul,
> ol {
margin-bottom: 0;
}
}
/**
* Headings
*/
h1, h2, h3, h4, h5, h6 {
font-weight: 300;
}
/**
* Links
*/
a {
color: $brand-color;
//text-decoration: none;
text-decoration: underline;
&:visited {
//color: darken($brand-color, 15%);
}
&:hover {
color: $text-color;
//text-decoration: underline;
}
}
/**
* Blockquotes
*/
blockquote {
color: $grey-color;
border-left: 4px solid $grey-color-light;
padding-left: $spacing-unit / 2;
font-size: 18px;
letter-spacing: -1px;
font-style: italic;
> :last-child {
margin-bottom: 0;
}
}
/**
* Code formatting
*/
pre,
code {
font-family: $monospace-font-family;
font-size: 15px;
border: 1px solid; //$grey-color-light;
border-radius: 3px;
background-color: revert; //#eef;
}
code {
padding: 1px 5px;
}
pre {
padding: 8px 12px;
overflow-x: scroll;
> code {
border: 0;
padding-right: 0;
padding-left: 0;
}
}
/**
* Wrapper
*/
.wrapper {
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));
max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));
margin-right: auto;
margin-left: auto;
padding-right: $spacing-unit;
padding-left: $spacing-unit;
@extend %clearfix;
@include media-query($on-laptop) {
max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));
max-width: calc(#{$content-width} - (#{$spacing-unit}));
padding-right: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
}
/**
* Clearfix
*/
%clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
/**
* Icons
*/
.icon {
> svg {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
path {
fill: $grey-color;
}
}
}

View File

@ -1,236 +0,0 @@
/**
* Site header
*/
.site-header {
border-top: 5px solid $grey-color-dark;
border-bottom: 1px solid $grey-color-light;
min-height: 56px;
// Positioning context for the mobile navigation icon
position: relative;
}
.site-title {
font-size: 26px;
line-height: 56px;
letter-spacing: -1px;
margin-bottom: 0;
float: left;
&,
&:visited {
color: $grey-color-dark;
}
}
.site-nav {
float: right;
line-height: 56px;
.menu-icon {
display: none;
}
.page-link {
color: $text-color;
line-height: $base-line-height;
// Gaps between nav items, but not on the first one
&:not(:first-child) {
margin-left: 20px;
}
}
@include media-query($on-palm) {
position: absolute;
top: 9px;
right: 30px;
background-color: $background-color;
border: 1px solid $grey-color-light;
border-radius: 5px;
text-align: right;
.menu-icon {
display: block;
float: right;
width: 36px;
height: 26px;
line-height: 0;
padding-top: 10px;
text-align: center;
> svg {
width: 18px;
height: 15px;
path {
fill: $grey-color-dark;
}
}
}
.trigger {
clear: both;
display: none;
}
&:hover .trigger {
display: block;
padding-bottom: 5px;
}
.page-link {
display: block;
padding: 5px 10px;
}
}
}
/**
* Site footer
*/
.site-footer {
border-top: 1px solid $grey-color-light;
padding: $spacing-unit 0;
}
.footer-heading {
font-size: 18px;
margin-bottom: $spacing-unit / 2;
}
.contact-list,
.social-media-list {
list-style: none;
margin-left: 0;
}
.footer-col-wrapper {
font-size: 15px;
color: $grey-color;
margin-left: -$spacing-unit / 2;
@extend %clearfix;
}
.footer-col {
float: left;
margin-bottom: $spacing-unit / 2;
padding-left: $spacing-unit / 2;
}
.footer-col-1 {
width: -webkit-calc(35% - (#{$spacing-unit} / 2));
width: calc(35% - (#{$spacing-unit} / 2));
}
.footer-col-2 {
width: -webkit-calc(20% - (#{$spacing-unit} / 2));
width: calc(20% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(45% - (#{$spacing-unit} / 2));
width: calc(45% - (#{$spacing-unit} / 2));
}
@include media-query($on-laptop) {
.footer-col-1,
.footer-col-2 {
width: -webkit-calc(50% - (#{$spacing-unit} / 2));
width: calc(50% - (#{$spacing-unit} / 2));
}
.footer-col-3 {
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
@include media-query($on-palm) {
.footer-col {
float: none;
width: -webkit-calc(100% - (#{$spacing-unit} / 2));
width: calc(100% - (#{$spacing-unit} / 2));
}
}
/**
* Page content
*/
.page-content {
padding: $spacing-unit 0;
}
.page-heading {
font-size: 20px;
}
.post-list {
margin-left: 0;
list-style: none;
> li {
margin-bottom: $spacing-unit;
}
}
.post-meta {
font-size: $small-font-size;
color: $grey-color;
}
.post-link {
display: block;
font-size: 24px;
}
/**
* Posts
*/
.post-header {
margin-bottom: $spacing-unit;
}
.post-title {
font-size: 42px;
letter-spacing: -1px;
line-height: 1;
@include media-query($on-laptop) {
font-size: 36px;
}
}
.post-content {
margin-bottom: $spacing-unit;
h2 {
font-size: 32px;
@include media-query($on-laptop) {
font-size: 28px;
}
}
h3 {
font-size: 26px;
@include media-query($on-laptop) {
font-size: 22px;
}
}
h4 {
font-size: 20px;
@include media-query($on-laptop) {
font-size: 18px;
}
}
}

View File

@ -1,67 +0,0 @@
/**
* Syntax highlighting styles
*/
.highlight {
background: #fff;
@extend %vertical-rhythm;
.c { color: #998; font-style: italic } // Comment
.err { color: #a61717; background-color: #e3d2d2 } // Error
.k { font-weight: bold } // Keyword
.o { font-weight: bold } // Operator
.cm { color: #998; font-style: italic } // Comment.Multiline
.cp { color: #999; font-weight: bold } // Comment.Preproc
.c1 { color: #998; font-style: italic } // Comment.Single
.cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special
.gd { color: #000; background-color: #fdd } // Generic.Deleted
.gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific
.ge { font-style: italic } // Generic.Emph
.gr { color: #a00 } // Generic.Error
.gh { color: #999 } // Generic.Heading
.gi { color: #000; background-color: #dfd } // Generic.Inserted
.gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific
.go { color: #888 } // Generic.Output
.gp { color: #555 } // Generic.Prompt
.gs { font-weight: bold } // Generic.Strong
.gu { color: #aaa } // Generic.Subheading
.gt { color: #a00 } // Generic.Traceback
.kc { font-weight: bold } // Keyword.Constant
.kd { font-weight: bold } // Keyword.Declaration
.kp { font-weight: bold } // Keyword.Pseudo
.kr { font-weight: bold } // Keyword.Reserved
.kt { color: #458; font-weight: bold } // Keyword.Type
.m { color: #099 } // Literal.Number
.s { color: #d14 } // Literal.String
.na { color: #008080 } // Name.Attribute
.nb { color: #0086B3 } // Name.Builtin
.nc { color: #458; font-weight: bold } // Name.Class
.no { color: #008080 } // Name.Constant
.ni { color: #800080 } // Name.Entity
.ne { color: #900; font-weight: bold } // Name.Exception
.nf { color: #900; font-weight: bold } // Name.Function
.nn { color: #555 } // Name.Namespace
.nt { color: #000080 } // Name.Tag
.nv { color: #008080 } // Name.Variable
.ow { font-weight: bold } // Operator.Word
.w { color: #bbb } // Text.Whitespace
.mf { color: #099 } // Literal.Number.Float
.mh { color: #099 } // Literal.Number.Hex
.mi { color: #099 } // Literal.Number.Integer
.mo { color: #099 } // Literal.Number.Oct
.sb { color: #d14 } // Literal.String.Backtick
.sc { color: #d14 } // Literal.String.Char
.sd { color: #d14 } // Literal.String.Doc
.s2 { color: #d14 } // Literal.String.Double
.se { color: #d14 } // Literal.String.Escape
.sh { color: #d14 } // Literal.String.Heredoc
.si { color: #d14 } // Literal.String.Interpol
.sx { color: #d14 } // Literal.String.Other
.sr { color: #009926 } // Literal.String.Regex
.s1 { color: #d14 } // Literal.String.Single
.ss { color: #990073 } // Literal.String.Symbol
.bp { color: #999 } // Name.Builtin.Pseudo
.vc { color: #008080 } // Name.Variable.Class
.vg { color: #008080 } // Name.Variable.Global
.vi { color: #008080 } // Name.Variable.Instance
.il { color: #099 } // Literal.Number.Integer.Long
}

View File

@ -1,145 +0,0 @@
---
# front-matter
---
@charset "utf-8";
// Font specifications. I keep changing my mind on what are the most pleasant
// fonts to my eyes, so I won't bother commenting them here.'
$serif-font-family:
ui-serif, "Roboto Serif", "Noto Serif", Tinos, serif, "Noto Emoji",
"Noto Color Emoji", "Segoe UI Emoji", emoji;
$sans-serif-font-family:
"Inclusive Sans", ui-sans-serif, "Roboto Flex", "Segoe UI Variable", Roboto,
"Noto Sans", Arimo, sans-serif, "Noto Emoji", "Noto Color Emoji",
"Segoe UI Emoji", emoji;
$monospace-font-family:
"Comic Shanns Mono", ui-monospace, "Roboto Mono", "Segoe UI Mono",
"Noto Mono", Cousine, monospace, "Noto Emoji", "Noto Color Emoji",
"Segoe UI Emoji", emoji;
// Must be in the end under threat of undefined variable error.
$base-font-family: $sans-serif-font-family;
@font-face {
font-family: "Inclusive Sans";
src: url("https://raw.githubusercontent.com/LivKing/Inclusive-Sans/refs/heads/main/fonts/webfonts/InclusiveSans[wght].woff2")
format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Inclusive Sans Italic";
src: url("https://raw.githubusercontent.com/LivKing/Inclusive-Sans/refs/heads/main/fonts/webfonts/InclusiveSans-Italic[wght].woff2")
format("woff2");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Comic Shanns Mono";
src: url("https://raw.githubusercontent.com/jesusmgg/comic-shanns-mono/refs/heads/master/fonts/ComicShannsMono-Regular.otf")
format("opentype");
}
@import "{{ site.theme }}";
:root {
color-scheme: dark light !important;
}
* {
// box-sizing: border-box !important;
color: revert !important;
background-color: revert !important;
//margin: auto !important;
// line-height: 1.2 !important;
// A4 paper
//max-width: 210mm !important;
font-size: revert;
//padding: auto !important;
overflow-wrap: break-word !important;
hyphens: auto !important;
// Experimental trick to make all emojis text if supported.
//font-variant-emoji: text;
/* WCAG minimum suggestions */
margin-bottom: 2 !important;
line-height: 1.5 !important;
letter-spacing: 0.12 !important;
word-spacing: 0.16 !important;
}
a {
text-decoration: underline !important;
}
a.site-title {
font-family: $serif-font-family;
}
// Monospace preferred for code
code,
pre {
font-family: $monospace-font-family !important;
}
// So it will not look bigger than normal text
code {
font-size: 0.8em !important;
}
// Sans-Serif for headings to constrast with aminda.eu
// h2,
// h3,
// h4,
// h5,
// h6 {
// font-family: $sans-serif-font-family !important;
// }
h2.footer-heading {
font-family: $sans-serif-font-family !important;
}
// The introduction on top
#bio {
text-align: center;
font-style: italic;
font-family: ui-cursive, $sans-serif-font-family;
}
img {
border-radius: 50% !important;
display: block;
margin-left: auto !important;
margin-right: auto !important;
@media (min-width: 395px) {
display: float !important;
float: right !important;
}
}
ul.linklist {
list-style: none inside;
font-family: $monospace-font-family;
//font-variant: small-caps;
}
// Customize the dark theme to be more me
@media (prefers-color-scheme: dark) {
* {
color: #ffb700 !important;
border-color: #ffb700 !important;
background-color: #000000 !important;
}
.site-nav {
color-scheme: only dark !important;
color: #ffb700 !important;
background-color: #000000 !important;
color: #ffb700 !important;
}
// I don't want links to be restored to amber'
a {
color: revert !important;
}
}

6
debian/changelog vendored Normal file
View File

@ -0,0 +1,6 @@
supybot (0.76.1-1) unstable; urgency=low
* Initial Release.
-- Jonathan Hseu <vomjom@debian.org> Tue, 3 Feb 2004 21:45:46 -0600

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
4

20
debian/control vendored Normal file
View File

@ -0,0 +1,20 @@
Source: supybot
Section: net
Priority: optional
Maintainer: Jonathan Hseu <vomjom@debian.org>
Build-Depends: debhelper (>= 4.0.0), python (>= 2.3), python-dev (>= 2.3)
Standards-Version: 3.6.0
Package: supybot
Architecture: all
Depends: ${python:Depends}
Recommends: python-sqlite
Suggests: python-twisted
Description: A robust and user friendly Python IRC bot.
Supybot is a robust (it doesn't crash), user friendly (it's easy
to configure) and programmer friendly (plugins are *extremely*
easy to write) Python IRC bot. It aims to be an adequate
replacement for most existing IRC bots. It includes a very
flexible and powerful ACL system for controlling access to
commands, as well as more than 50 builtin plugins providing
around 400 actual commands.

25
debian/copyright vendored Normal file
View File

@ -0,0 +1,25 @@
This package was debianized by Jonathan Hseu <vomjom@debian.org> on
Tue, 3 Feb 2004 21:45:46 -0600.
It was downloaded from http://supybot.sf.net/
Upstream Authors:
/usr/share/doc/supybot/DEVS includes the names and descriptions of the
developers.
Copyright:
A BSD-style license which you can find in:
/usr/share/doc/supybot/LICENSE
Files located in /usr/lib/python2.3/site-packages/supybot/others/ have their
own respective licenses, which are either commented at the top of the file, or
are held in the __license__ variable.
The licenses in that directory are either:
BSD-style, GPL, or the Python License
The first two can be found in /usr/share/common-licenses/
The Python License can be found in:
/usr/share/doc/python/copyright

1
debian/dirs vendored Normal file
View File

@ -0,0 +1 @@
usr/bin

8
debian/docs vendored Normal file
View File

@ -0,0 +1,8 @@
BUGS
DEVS
LICENSE
README
RELNOTES
TODO
docs/CAPABILITIES
docs/GETTING_STARTED

87
debian/rules vendored Executable file
View File

@ -0,0 +1,87 @@
#!/usr/bin/make -f
# -*- makefile -*-
# GNU copyright 1997 to 1999 by Joey Hess.
# Modified for supybot by Jonathan Hseu
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
CFLAGS = -Wall -g
configure: configure-stamp
configure-stamp:
dh_testdir
# Add here commands to configure the package.
touch configure-stamp
build: build-stamp
build-stamp: configure-stamp
dh_testdir
# Add here commands to compile the package.
touch build-stamp
clean:
dh_testdir
dh_testroot
rm -f build-stamp configure-stamp
# Add here commands to clean up after the build process.
python setup.py clean --all
dh_clean
install: build
dh_testdir
dh_testroot
dh_clean -k
dh_installdirs
# Add here commands to install the package into debian/supybot.
python setup.py install --prefix=$(CURDIR)/debian/supybot/usr
# Build architecture-independent files here.
binary-indep: build install
# We have nothing to do by default.
# Build architecture-dependent files here.
binary-arch: build install
dh_testdir
dh_testroot
dh_installchangelogs ChangeLog
dh_installdocs
dh_installexamples
# dh_install
# dh_installmenu
# dh_installdebconf
# dh_installlogrotate
# dh_installemacsen
# dh_installpam
# dh_installmime
# dh_installinit
# dh_installcron
# dh_installinfo
dh_installman
dh_link
dh_strip
dh_compress
dh_fixperms
# dh_perl
dh_python
# dh_makeshlibs
dh_installdeb
dh_shlibdeps
dh_gencontrol
dh_md5sums
dh_builddeb
binary: binary-indep binary-arch
.PHONY: build clean binary-indep binary-arch binary install configure

118
docs/CAPABILITIES Normal file
View File

@ -0,0 +1,118 @@
Ok, some some explanation of the capabilities system is probably in
order. With most IRC bots (including the ones I've written myself
prior to this one) "what a user can do" is set in one of two ways. On
the *really* simple bots, each user has a numeric "level" and commands
check to see if a user has a "high enough level" to perform some
operation. On bots that are slightly more complicated, users have a
list of "flags" whose meanings are hardcoded, and the bot checks to
see if a user possesses the necessary flag before performing some
operation. Both methods, IMO, are rather arbitrary, and force the
user and the programmer to be unduly confined to less expressive
constructs.
This bot is different. Every user has a set of "capabilities" that is
consulted every time they give the bot a command. Commands, rather
than checking for a user level of 100, or checking if the user has an
"o" flag, are instead able to check if a user has the "owner"
capability. At this point such a difference might not seem
revolutionary, but at least we can already tell that this method is
self-documenting, and easier for users and developers to understand
what's truly going on.
If that was all, well, the capability system would be "cool", but not
many people would say it was "awesome". But it *is* awesome! Several
things are happening behind the scene that make it awesome, and these
are things that couldn't happen if the bot was using numeric
userlevels or single-character flags. First, whenever a user issues
the bot a command, the command dispatcher checks to make sure the user
doesn't have the "anticapability" for that command. An anticapability
is a capability that, instead of saying "what a user can do", says
what a user *cannot* do. It's formed rather simply by adding a dash
("-") to the beginning of a capability; "rot13" is a capability, and
"-rot13" is an anticapability. Anyway, when a user issues the bot a
command, perhaps "calc" or "help", the bot first checks to make sure
the user doesn't have the "-calc" or the "-help" capabilities before
even considering responding to the user. So commands can be turned on
or off on a *per user* basis, offering finegrained control not often
(if at all!) seen in other bots.
But that's not all! The capabilities system also supports *Channel*
capabilities, which are capabilities that only apply to a specific
channel; they're of the form "#channel,capability". Whenever a user
issues a command to the bot in a channel, the command dispatcher also
checks to make sure the user doesn't have the anticapability for that
command *in that channel*, and if the user does, the bot won't respond
to the user in the channel. Thus now, in addition to having the
ability to turn individual commands on or off for an individual user,
we can now turn commands on or off for an individual user on an
individual channel!
So when a user "foo" sends a command "bar" to the bot on channel
"#baz", first the bot checks to see if the user has the anticapability
for the command by itself, "-bar". If so, it returns right then and
there, compltely ignoring the fact that the user issued that command
to it. If the user doesn't have that anticapability, then the bot
checks to see if the user issued the command over a channel, and if
so, checks to see if the user has the antichannelcapability for that
command, "#baz,-bar". If so, again, he returns right then and there
and doesn't even think about responding to the bot. If neither of
these anticapabilities are present, then the bot just responds to the
user like normal.
From a programmatical perspective, capabilties are easy to use and
flexible. Any command can check if a user has any capability, even
ones not thought of when the bot was originally written.
Commands/Callbacks can add their own capabilities -- it's as easy as
just checking for a capability and documenting somewhere that a user
needs that capability to do something.
From an end-user perspective, capabilities remove a lot of the mystery
and esotery of bot control, in addition to giving the user absolutely
finegrained control over what users are allowed to do with the bot.
Additionally, defaults can be set by the end-user for both individual
channels and for the bot as a whole, letting an end-user set the
policy he wants the bot to follow for users that haven't yet
registered in his user database. It's really a revolution!
There are several default capabilities the bot uses. The most
important of these is the "owner" capability. This capability allows
the person having it to use *any* command. It's best to keep this
capability reserved to people who actually have access to the shell
the bot is running on.
There is also the "admin" capability for non-owners that are highly
trusted to administer the bot appropriately. They can do things such
as change the bot's nick, globally enable/disable commands, cause the
bot to ignore a given user, set the prefixchar, report bugs, etc.
They generally cannot do administration related to channels, which is
reserved for people with the next capability.
People who are to administer channels with the bot should have the
#channel,op capability -- whatever channel they are to administrate,
they should have that channel capability for "op". For example, since
I want inkedmn to be an administrator in #supybot, I'll give him the
#supybot,op capability. This is in addition to his admin capability,
since the admin capability doesn't give the person having it control
over channels. #channel,op is used for such things as
giving/receiving ops, kickbanning people, lobotomizing the bot,
ignoring users in the channel, and managing the channel capabilities.
The #channel,op capability is also basically the equivalent of the
owner capability for capabilities involving #channel -- basically
anyone with the #channel,op capability is considered to have all
positive capabilities and no negative capabilities for #channel.
One other globally important capability exists: "trusted". This is a
command that basically says "This user can be trusted not to try and
crash the bot." It allows users to call commands like Math.icalc,
which potentially could cause the bot to begin a calculation that
could potentially never return (a calculation like 10**10**10**10).
Another command that requires the trusted capability is Utilties.re,
which (due to the regular expression implementation in Python (and any
other language that uses NFA regular expressions, like Perl or Ruby or
Lua or ...) which can allow a regular expression to take exponential
time to process). Consider what would happen if the someone gave the
bot the command 're [strjoin "" s/./ [dict go] /] [dict go]'
Other plugins may require different capabilities; the Factoids plugin
requires #channel,factoids, the Topic plugin requires #channel,topic,
etc.

188
docs/CONFIGURATION Normal file
View File

@ -0,0 +1,188 @@
So you've got your Supybot up and running and there are some things
you don't like about it. Fortunately for you, chances are that these
things are configurable, and this document is here to tell you how to
configure them.
Configuration of Supybot is handled via the Config plugin, which
controls runtime access to Supybot's registry (the configuration file
generated by the supybot-wizard program you ran). The Config plugin
provides a way to get or set variables, to list the available
variables, and even to get help for certain variables. Take a moment
now to read the help for each of those commands: get, set, list, and
help. If you don't know how to get help on those commands, go ahead
and read our GETTING_STARTED document before this one.
Now, if you're used to the Windows registry, don't worry, Supybot's
registry is completely different. For one, it's completely plain
text. There's no binary database sensitive to corruption, it's not
necessary to use another program to edit it -- all you need is a
simple text editor. But there is at least one good idea in Windows'
registry: hierarchical configuration. Supybot's configuration
variables are organized in a hierarchy: variables having to do with
the way Supybot makes replies all start with supybot.reply; variables
having to do with the way a plugin works all start with
supybot.plugins.Plugin (where Plugin is the name of the plugin in
question). This hierarchy is nice because it means the user isn't
inundated with hundreds of unrelated and unsorted configuration
variables.
Some of the more important configuration values are located directly
under the base group, supybot. Things like the bot's nick, its ident,
etc. Along with these config values are a few subgroups that contain
other values. Some of the more prominent subgroups are: plugins
(where all the plugin-specific configuration is held), reply (where
variables affecting the way a Supybot makes its replies resides),
replies (where all the specific standard replies are kept), and
directories (where all the directories a Supybot uses are defined).
There are other subgroups as well, but these are the ones we'll use in
our example.
Using the Config plugin, you can list the values in a subgroup and get
or set any of the values anywhere in the configuration hierarchy. For
example, let's say you wanted to see what configuration values were
under the "supybot" (the base group) hierarchy. You would simply
issue this command:
<jemfinch|lambda> @config list supybot
<supybot> jemfinch|lambda: nick, ident, user, server,
password, channels, prefixChars, defaultCapabilities,
defaultAllow, defaultIgnore, humanTimestampFormat,
externalIP, pipeSyntax,
followIdentificationThroughNickChanges, alwaysJoinOnInvite,
showSimpleSyntax, maxHistoryLength, nickmods, throttleTime,
snarfThrottle, threadAllCommands, pingServer, pingInterval,
upkeepInterval, flush, httpPeekSize, and
defaultSocketTimeout
These are all the configuration values you can set which are under the
base "supybot" group. Actually, their full names would each have a
"supybot." appended on to the front of them, but it is omitted in the
listing in order to shorten the output.
Now, to see all of the available configuration groups under the base
"supybot" group, we simply use the "--groups" flag to config list:
<jemfinch|lambda> @config list --groups supybot
<supybot> jemfinch|lambda: commands, databases,
directories, drivers, log, plugins, replies, and reply
These are all the subgroups of "supybot". Again, the full name of
these would have "supybot." prepended to them. So really, we have
supybot.commands, supybot.databases, etc.
Note: an item can show up in both lists if it is a group that itself
has a value. For example, all plugins fall under this category, as
their value is a boolean value determining whether or not that plugin
is to be loaded when the bot is started.
One last listing example, and then we'll start actually reading and
modifying the configuration values. It's important to know that when
you provide the group argument to config list that you must always
provide the full name of the group. For example, "config list
commands" would be incorrect, even though we see "commands" in the
listing above. Remember, we just shorten the names by the group we're
listing so we can fit more such names in a single message. In this
case, that would be "supybot", so to list everything in the commands
subgroup of supybot, we do:
<jemfinch|lambda> @config list supybot.commands
<supybot> jemfinch|lambda: defaultPlugins
Okay, now that you've used the Config plugin to list configuration
variables, it's time that we start looking at individual variables and
their values.
The first (and perhaps most important) thing you should know about
each configuration variable is that they all have an associated help
string to tell you what they represent. So the first command we'll
cover is "config help". To see the help string for any value or
group, simply use the "config help" command. For example, to see what
this "supybot.prefixChars" configuration variable is all about, we'd
do this:
<jemfinch|lambda> @config help supybot.prefixChars
<supybot> jemfinch|lambda: 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.
Pretty simple, eh?
Now, if you're curious what the current value of a configuration
variable is, you'll use the "config" command with one argument, the
name of the variable you want to see the value of:
<jemfinch|lambda> @config supybot.prefixChars
<supybot> jemfinch|lambda: '@'
To set this value, just stick an extra argument after the name:
<jemfinch|lambda> @config supybot.prefixChars @$
<supybot> jemfinch|lambda: The operation succeeded.
Now, check this out:
<jemfinch|lambda> $config supybot.prefixChars
<supybot> jemfinch|lambda: '@$'
Note that we used $ as our prefix character, and that the value of the
configuration variable changed. If I were to use the "flush" command
now, this change would be flushed to the registry file on disk (this
would also happen if I made the bot quit, or pressed Ctrl-C in the
terminal the bot was running in). Instead, I'll revert the change:
<jemfinch|lambda> $config supybot.prefixChars @
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> $note that this makes no response.
If you're ever curious what the default for a given configuration
variable is, use the "config default" command:
<jemfinch|lambda> @config default supybot.prefixChars
<supybot> jemfinch|lambda: ''
Thus, to reset a configuration variable to its default value, you can
simply say:
<jemfinch|lambda> @config supybot.prefixChars [config default
supybot.prefixChars]
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> @note that this does nothing
Simple, eh?
Now, let's say you want to find all configuration variables that might
be even remotely related to opping. For that, you'll want the "config
search" command. Check this out:
<jemfinch|lambda> @config search op
<supybot> jemfinch|lambda:
supybot.plugins.Enforcer.autoOp,
supybot.plugins.Enforcer.autoOp.#supybot,
supybot.plugins.Enforcer.autoHalfop,
supybot.plugins.Enforcer.cycleToGetOps, supybot.plugins.Topic,
supybot.plugins.Topic.separator, and
supybot.plugins.Relay.topicSync
Sure, it showed up all the topic-related stuff in there, but it also
showed you all the op-related stuff, too. Do note, however, that you
can only see configuration variables for plugins that you have loaded
or that you loaded in the past; if you've never loaded a plugin,
there's no way for the bot to know what configuration variables it
registers.
Some people might like editing their registry file directly rather
than manipulating all these things through the bot. For those people,
we offer the "config reload" command, which reloads both registry
configuration and user/channel/ignore database configuration. Just
edit the interesting files and then give the bot the "config reload"
command and it'll work as expected. Do note, however, that Supybot
flushes his configuration files and databases to disk every hour or
so, and if this happens after you've edited your configuration files
but before you reload your changes, you could lose the changes you
made. To prevent this, set the supybot.flush value to Off, and no
automatic flushing will occur.
Anyway, that's about it for configuration. Have fun, and enjoy your
configurable bot!

25
docs/DocBook/Makefile Normal file
View File

@ -0,0 +1,25 @@
JADE=/usr/bin/jade
JADETEX=/usr/bin/jadetex
DVIPDF=/usr/bin/dvipdfm
HTMLSTYLESHEET=/usr/share/sgml/docbook/stylesheet/dsssl/modular/html/docbook.dsl
PRINTSTYLESHEET=/usr/share/sgml/docbook/stylesheet/dsssl/modular/print/docbook.dsl
html: example.sgml capabilities.sgml
$(JADE) -t xml -d $(HTMLSTYLESHEET) $<
example.dvi: example.sgml
$(JADE) -t tex -d $(PRINTSTYLESHEET) $<
$(JADETEX) $(addsuffix .tex, $(basename $<))
example.pdf: example.dvi
$(DVIPDF) -o $(addsuffix .pdf, $(basename $<)) $<
capabilities.dvi: capabilities.sgml
$(JADE) -t tex -d $(PRINTSTYLESHEET) $<
$(JADETEX) $(addsuffix .tex, $(basename $<))
capabilities.pdf: capabilities.dvi
$(DVIPDF) -o $(addsuffix .pdf, $(basename $<)) $<
clean:
rm -f *.html

177
docs/DocBook/README.DocBook Normal file
View File

@ -0,0 +1,177 @@
The Official Supybot DocBook Metadocumentation
(or, How Does One SGML File Turn Into All Those Document Formats?)
Okay, though this isn't the case yet, ideally all of Supybot's documentation
can and will be done using DocBook and DocBook-related tools as well as a few
custom extensions that I've written.
- How does DocBook work?
First things first, you have to understand sort of how DocBook works in order
to figure out how our documentation gets generated from just one file. What
DocBook is, basically, is just a DTD (Document Type Definition). What that
means is that it simply specifies how a document can be structured and still be
considered a valid document by placing restrictions on what elements go where.
It's a popular DTD because it is structured very well and it's not only fairly
generic, but it also has nice elements that make documenting things (such as
supybot) rather easy. It focuses on structure and content instead of
presentation which is what makes it nice for writing things which are output
format agnostic.
So, let's say we've written a proper DocBook document, now what? Well, using
an output formatting tool and a stylesheet, you create whatever form of output
you want. What we use for producing the outptut is jade, and DocBook comes
with a few stylesheets that work with that tool to create output formats like
HTML and TeX. From the TeX file we produce a DVI (device independent) file
with latex, and from that we produce the print formats of our documents, like
PDF and Postscript using tools like dvips and dvipdfm.
- What extra stuff do we do?
Well, since our documents all have to do with an IRC bot, there are some very
common things that we talk about a lot that we might like to format specially.
For example, when we discuss a particular command for the bot we might like to
have that text appear slightly different to emphasize the fact that it is
special. So, for the commonly used items that weren't already covered by
DocBook's DTD, I added elements into a new DTD which just extends DocBook's
DTD. So now we have elements like <nick> and <channel> and <botcommand> that
we can use for our documentation.
Of course, now that we have used a DTD with more stuff in it (than DocBook),
the stylesheets that DocBook provides won't do any special formatting for those
new elements so we have to write new stylesheets as well. Once again I just
extended the existing ones with formatting instructions on how to treat the new
elements. So with this done, now our HTML and TeX (and whatever else) output
will be properly formatted.
- How do I make my own changes to the DTD and stylesheets?
Primarily, you don't :) Ask me (Strike) first about it, and I will generally
write them for you or explain a better way of doing things. This is especially
true for the DTD, because that must remain consistent everywhere we write/read
supybot docs based on it. The stylesheets are more lax and can be modified to
produce whatever kind of output you wish.
So, with that warning/reminder out of the way, here's how to modify each
anyway. This doesn't really assume any knowledge of how to write a DTD, nor is
it an exhaustive reference on writing one, so don't treat it as such. I'm
basically just going to explain how to add extra elements that will play well
with the DocBook DTD.
-- Adding an element to the DTD
If you've decided that there's a certain "thing" that's mentioned a lot in the
documentation and deserves classification (for potential special formatting),
you'll probably want to create a new element (or set of elements) for it. I'll
walk you through how I added the "nick" element to our DTD (though many/most of
the elements I added follow an identical process).
The very first thing you need to figure out is: where in my document does this
element "fit". That is to say, what elements should/can rightly contain this
particular one? In the case of the "nick" element, it's basically always an
inline-formatted deal that belongs in paragraphs for the most part. For those
of you scratching your heads at that last sentence, perhaps thinking "okay, so
how are we supposed to know what is relevant?" I say, "don't worry, I learned
by example as well." Basically, I just looked through the DocBook DTD and
figured out where things belong. Now, even if you don't know the DocBook DTD
front-to-back, you can still peruse it to figure out where your new element
belongs. Obviously, you should probably know *some* DocBook to figure out what
each element means, but luckily all of our docs have been converted to DocBook
and serve as nice examples of the usage of many elements :)
Now, to figure out where something like "nick" belongs. In many ways, a nick
is sort of like a variable name (at least in documentation usage). So, the
element I chose to base it off of was "varname". If you have the DocBook DTD
installed (as you should if you intend on making extensions to it), the varname
element definition is contained in the dbpoolx.mod filename (in Debian, it's
under /usr/share/sgml/docbook/dtd/4.2). How did I know this? Well, grep is
your friend and mine too, and dbpoolx is the only filename that shows up when
grepping for "varname" in the DocBook DTD directory. So, we open up dbpoolx.mod and search for varname. The first thing we find it in looks like this:
<!ENTITY % tech.char.class
"action|application
|classname|methodname|interfacename|exceptionname
|ooclass|oointerface|ooexception
|command|computeroutput
|database|email|envar|errorcode|errorname|errortype|errortext|filename
|function|guibutton|guiicon|guilabel|guimenu|guimenuitem
|guisubmenu|hardware|interface|keycap
|keycode|keycombo|keysym|literal|constant|markup|medialabel
|menuchoice|mousebutton|option|optional|parameter
|prompt|property|replaceable|returnvalue|sgmltag|structfield
|structname|symbol|systemitem|token|type|userinput|varname
%ebnf.inline.hook;
%local.tech.char.class;">
Hmm, this doesn't look like a definition of varname (to me, but I sort of
cheated by having read about DocBook before-hand ;)), but it will be important
to remember for later. Let's try and find the element definition for varname
(so, basically, let's look for the first line that starts with "<!ELEMENT ").
The first line I come up with when I search is:
<!ELEMENT varname %ho; (%smallcptr.char.mix;)*>
Rather than write a separate tutorial for interpreting DTDs, I found a good
SGML tutorial online that explains everything necessary to help you parse the
DocBook DTD to figure out what the varname element really is, as well as to
help you learn all the stuff necessary for what we will cover in creating our
new nick element. That tutorial is at
http://www.w3.org/TR/WD-html40-970708/intro/sgmltut.html#howtodtd (it's for
reading the HTML DTD, but it applies to any DTD).
So, now that we understand how to write/read things for a DTD, we arrive at the
time where we can write the actual definition of our "nick" element:
<!ELEMENT Nick - - ((%smallcptr.char.mix;)+)>
As we learned in the above tutorial, this means that we are creating an element
named "nick", which must have start and end tags, and is defined to contain one
or more of whatever is in "smallcptr.char.mix". And rather than hunt through
the DocBook DTD to figure out what that is, for now we'll just live with the
fact that whatever can go into a DocBook varname can go into our new nick
element. If you feel so inclined, feel free to try and define the content
model for nick to only include valid nick characters. It's perfectly doable,
and I'll probably do it at some point but I haven't yet.
Since we're extending the DocBook DTD, I also decided that it'd be nice to
follow the element creation conventions observed in their DTD, so there are a
few more lines associated with our new nick element. All of them are related
to the attributes of the element, and allowing for them to be extended by
external DTDs (much like we are doing, only we aren't changing attributes of
existing elements, just adding our own). The first one is:
<!ENTITY % local.nick.attrib "">
This basically defines an empty entity named local.nick.attrib which we will
include so that if anyone chooses to extend the nick attributes, all they have
to do is redefine local.nick.attrib.
<!ENTITY % nick.role.attrib "%role.attrib;">
To tell you the truth, I'm not entirely sure what this is for, but it follows the DocBook convention :)
<!ATTLIST Nick
%common.attrib;
%local.nick.attrib;
%nick.role.attrib;
>
This is, of course, our attribute list for our nick element. It consists of
the two things we just defined as well as common.attrib which contains things
like "id" and whatnot which all DocBook elements are expected to have.
-- Extending the DocBook DTD to recognize new elements
So, that's all you need to define your new element. But, we're not done just
yet! We're almost there, we just need to make it so that it works with the
existing DocBook elements, otherwise it's no good to us. Since we defined our
element to esentially be the same as varname, it probably belongs at the same
place within the DocBook schema as varname. Do you remember when we had that
large entity definition that wasn't what we were looking for at the time though
I said it'd be important later? Well, later is now. So, what that line tells
us is what class of elements DocBook has varname in, which is
"tech.char.class". And thanks to the DocBook convention of defining a
local.<classname> entity that we can extend, all we have to do is redefine
local.tech.char.class to contain "nick", and we are done.
You may notice, however, that we don't actually put varname right into the
local.tech.char.class entity, but instead we create our own
supybot.tech.char.class class of elements that are supybot-specific (and are
the equivalent of DocBook's tech.char.class elements) and instead, put all of
those into the local.tech.char.class entity. Basically, we just go through one
more level of indirection.

View File

@ -0,0 +1,206 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot capabilities system explanation</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<subtitle>
Supybot's capabilities overview and comparisons to other bots
</subtitle>
<para>
Ok, some some explanation of the capabilities system is probably
in order. With most IRC bots (including the ones I've written
myself prior to this one) &ldquo;what a user can do&rdquo; is set
in one of two ways. On the <emphasis>really</emphasis> simple
bots, each user has a numeric &ldquo;level&rdquo; and commands
check to see if a user has a &ldquo;high enough level&rdquo; to
perform some operation. On bots that are slightly more
complicated, users have a list of &ldquo;flags&rdquo; whose
meanings are hardcoded, and the bot checks to see if a user
possesses the necessary flag before performing some operation.
Both methods, IMO, are rather arbitrary, and force the user and
the programmer to be unduly confined to less expressive
constructs.
</para>
<para>
This bot is different. Every user has a set of
&ldquo;capabilities&rdquo; that is consulted every time they give
the bot a command. Commands, rather than checking for a user
level of 100, or checking if the user has an <varname>o</varname>
flag, are instead able to check if a user has the
<capability>owner</capability> capability. At this point such a
difference might not seem revolutionary, but at least we can
already tell that this method is self-documenting, and easier for
users and developers to understand what's truly going on.
</para>
</sect1>
<sect1>
<title>What sets supybot's capabilities apart</title>
<para>
If that was all, well, the capability system would be
&ldquo;cool&rdquo;, but not many people would say it was
&ldquo;awesome&rdquo;. But it <emphasis>is</emphasis> awesome!
Several things are happening behind the scene that make it
awesome, and these are things that couldn't happen if the bot was
using numeric userlevels or single-character flags. First,
whenever a user issues the bot a command, the command dispatcher
checks to make sure the user doesn't have the
&ldquo;anticapability&rdquo; for that command. An anticapability is
a capability that, instead of saying &ldquo;what a user can
do&rdquo;, says what a user <emphasis>cannot</emphasis> do. It's
formed rather simply by adding a dash (&ldquo;-&rdquo;) to the
beginning of a capability; <botcommand>rot13</botcommand> is a
capability, and <botcommand>-rot13</botcommand> is an
anticapability. Anyway, when a user issues the bot a command,
perhaps <botcommand>calc</botcommand> or
<botcommand>help</botcommand>, the bot first checks to make sure
the user doesn't have the <capability>-calc</capability> or the
<capability>-help</capability> capabilities before even
considering responding to the user. So commands can be turned on
or off on a <emphasis>per user</emphasis> basis, offering
finegrained control not often (if at all!) seen in other bots.
</para>
<sect2>
<title>Channel capabilities</title>
<para>
But that's not all! The capabilities system also supports
<emphasis>Channel</emphasis> capabilities, which are
capabilities that only apply to a specific channel; they're of
the form <capability>#channel.capability</capability>.
Whenever a user issues a command to the bot in a channel, the
command dispatcher also checks to make sure the user doesn't
have the anticapability for that command <emphasis>in that
channel</emphasis> and if the user does, the bot won't respond
to the user in the channel. Thus now, in addition to having
the ability to turn individual commands on or off for an
individual user, we can now turn commands on or off for an
individual user on an individual channel!
</para>
<para>
So when a user <nick>foo</nick> sends a command
<botcommand>bar</botcommand> to the bot on channel
<channel>#baz</channel>, first the bot checks to see if the
user has the anticapability for the command by itself,
<capability>-bar</capability>. If so, it returns right then
and there, compltely ignoring the fact that the user issued
that command to it. If the user doesn't have that
anticapability, then the bot checks to see if the user issued
the command over a channel, and if so, checks to see if the
user has the antichannelcapability for that command,
<capability>#baz.-bar</capability>. If so, again, he returns
right then and there and doesn't even think about responding
to the bot. If neither of these anticapabilities are present,
then the bot just responds to the user like normal.
</para>
</sect2>
</sect1>
<sect1>
<title>Motivations behind the capabilities system</title>
<sect2>
<title>A programmer's perspective</title>
<para>
From a programmatical perspective, capabilties are easy to use
and flexible. Any command can check if a user has any
capability, even ones not thought of when the bot was
originally written. Commands/Callbacks can add their own
capabilities &ndash; it's as easy as just checking for a
capability and documenting somewhere that a user needs that
capability to do something.
</para>
</sect2>
<sect2>
<title>An end-user's perspective</title>
<para>
From an end-user perspective, capabilities remove a lot of the
mystery and esotery of bot control, in addition to giving the
user absolutely finegrained control over what users are
allowed to do with the bot. Additionally, defaults can be set
by the end-user for both individual channels and for the bot
as a whole, letting an end-user set the policy he wants the
bot to follow for users that haven't yet registered in his
user database.
</para>
<para>
It's really a revolution!
</para>
</sect1>
<sect1>
<title>Hard-coded supybot capabilities</title>
<para>
There are several default capabilities the bot uses. The most
important of these is the <capability>owner</capability>
capability. This capability allows the person having it to use
<emphasis>any</emphasis> command. It's best to keep this
capability reserved to people who actually have access to the
shell the bot is running on.
</para>
<para>
There is also the <capability>admin</capability> capability for
non-owners that are highly trusted to administer the bot
appropriately. They can do things such as change the bot's nick,
globally enable/disable commands, cause the bot to ignore a given
user, set the prefixchar, report bugs, etc. They generally cannot
do administration related to channels, which is reserved for
people with the next capability.
</para>
<para>
People who are to administer channels with the bot should have the
<capability>#channel.op</capability> capability -- whatever
channel they are to administrate, they should have that channel
capability for <capability>op</capability>. For example, since I
want <nick>inkedmn</nick> to be an administrator in
<channel>#supybot</channel>, I'll give him the
<capability>#supybot.op</capability> capability. This is in
addition to his <capability>admin</capability> capability, since
the <capability>admin</capability> capability doesn't give the
person having it control over channels.
<capability>#channel.op</capability> is used for such things as
giving/receiving ops, kickbanning people, lobotomizing the bot,
ignoring users in the channel, and managing the channel
capabilities. The <capability>#channel.op</capability> capability
is also basically the equivalent of the owner capability for
capabilities involving <channel>#channel</channel> &ndash;
basically anyone with the <capability>#channel.op</capability>
capability is considered to have all positive capabilities and no
negative capabilities for <channel>#channel</channel>.
</para>
<para>
One other globally important capability exists:
<capability>trusted</capability>. This is a command that
basically says &ldquo;This user can be trusted not to try and
crash the bot.&rdquo; It allows users to call commands like
<botcommand>Math.icalc</botcommand>, which potentially could cause the
bot to begin a calculation that could potentially never return (a
calculation like 10**10**10**10). Another command that requires
the trusted capability is <botcommand>Utilties.re</botcommand>, which
(due to the regular expression implementation in Python (and any
other language that uses NFA regular expressions, like Perl or
Ruby or Lua or &hellip;) which can allow a regular expression to
take exponential time to process). Consider what would happen if
the someone gave the bot the command <literal>re [strjoin "" s/./
[dict go] /] [dict go]</literal>.
</para>
</sect1>
</article>

View File

@ -0,0 +1,311 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<author>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot configuration system explanation</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>26 Feb 2004</date>
<revremark>Conversion to Supybot DTD</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<para>
So you've got your Supybot up and running and there are some
things you don't like about it. Fortunately for you, chances are
that these things are configurable, and this document is here to
tell you how to configure them.
</para>
<para>
Configuration of Supybot is handled via the
<plugin>Config</plugin> plugin, which controls runtime access to
Supybot's registry (the configuration file generated by the
<script>supybot-wizard</script> program you ran). The
<plugin>Config</plugin> plugin provides a way to get or set
variables, to list the available variables, and even to get help
for certain variables. Take a moment now to read the help for
each of those commands: <botcommand>get</botcommand>,
<botcommand>set</botcommand>, <botcommand>list</botcommand>, and
<botcommand>help</botcommand>. If you don't know how to get help on
those commands, go ahead and read our
<filename>GETTING_STARTED</filename> document before this one.
</para>
</sect1>
<sect1>
<title>Supybot's registry</title>
<para>
Now, if you're used to the Windows registry, don't worry,
Supybot's registry is completely different. For one, it's
completely plain text. There's no binary database sensitive to
corruption, it's not necessary to use another program to edit it
&ndash; all you need is a simple text editor. But there is at
least one good idea in Windows' registry: hierarchical
configuration. Supybot's configuration variables are organized in
a hierarchy: variables having to do with the way Supybot makes
replies all start with
<registrygroup>supybot.reply</registrygroup>; variables having to
do with the way a plugin works all start with
<registrygroup>supybot.plugins.Plugin</registrygroup> (where
<plugin>Plugin</plugin> is the name of the plugin in question).
This hierarchy is nice because it means the user isn't inundated
with hundreds of unrelated and unsorted configuration variables.
</para>
<para>
Some of the more important configuration values are located
directly under the base group,
<registrygroup>supybot</registrygroup>. Things like the bot's
nick, its ident, etc. Along with these config values are a few
subgroups that contain other values. Some of the more prominent
subgroups are: <registrygroup>plugins</registrygroup> (where all
the plugin-specific configuration is held),
<registrygroup>reply</registrygroup> (where variables affecting
the way a Supybot makes its replies resides),
<registrygroup>replies</registrygroup> (where all the specific
standard replies are kept), and
<registrygroup>directories</registrygroup> (where all the
directories a Supybot uses are defined). There are other
subgroups as well, but these are the ones we'll use in our
example.
</para>
<sect2>
<title>Config plugin commands</title>
<sect3>
<title>Listing registry contents</title>
<para>
Using the <plugin>Config</plugin> plugin, you can list
the values in a subgroup and get or set any of the values
anywhere in the configuration hierarchy. For example,
let's say you wanted to see what configuration values were
under the <registrygroup>supybot</registrygroup> (the base
group) hierarchy. You would simply issue this command:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config list supybot
&lt;supybot&gt; jemfinch|lambda: nick, ident, user, server, password,
channels, prefixChars, defaultCapabilities, defaultAllow, defaultIgnore,
humanTimestampFormat, externalIP, pipeSyntax,
followIdentificationThroughNickChanges, alwaysJoinOnInvite,
showSimpleSyntax, maxHistoryLength, nickmods, throttleTime,
snarfThrottle, threadAllCommands, pingServer, pingInterval,
upkeepInterval, flush, httpPeekSize, and defaultSocketTimeout
</ircsession>
<para>
These are all the configuration values you can set which
are under the base <registrygroup>supybot</registrygroup>
group. Actually, their full names would each have a
&ldquo;supybot.&rdquo; appended on to the front of them,
but it is omitted in the listing in order to shorten the
output.
</para>
<para>
Now, to see all of the available configuration groups
under the base <registrygroup>supybot</registrygroup>
group, we simply use the <flag>--groups</flag> flag to
<botcommand>config list</botcommand>:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config list --groups supybot
&lt;supybot&gt; jemfinch|lambda: commands, databases, directories, drivers,
log, plugins, replies, and reply
</ircsession>
<para>
These are all the subgroups of
<registrygroup>supybot</registrygroup>. Again, the full
name of these would have &ldquo;supybot.&rdquo; prepended
to them. So really, we have
<registrygroup>supybot.commands</registrygroup>,
<registrygroup>supybot.databases</registrygroup>, etc.
</para>
<note>
<para>
An item can show up in both lists if it is a group
that itself has a value. For example, all plugins
fall under this category, as their value is a boolean
value determining whether or not that plugin is to be
loaded when the bot is started.
</para>
</note>
</sect3>
<sect3>
<title>Dealing with registry values</title>
<para>
Okay, now that you've used the <plugin>Config</plugin>
plugin to list configuration variables, it's time that we
start looking at individual variables and their values.
</para>
<sect4>
<title>Built-in help for registry values</title>
<para>
The first (and perhaps most important) thing you
should know about each configuration variable is that
they all have an associated help string to tell you
what they represent. So the first command we'll cover
is <botcommand>config help</botcommand>. To see the
help string for any value or group, simply use the
<botcommand>config help</botcommand> command. For
example, to see what this
<registrygroup>supybot.prefixChars</registrygroup>
configuration variable is all about, we'd do this:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config help supybot.prefixChars
&lt;supybot&gt; jemfinch|lambda: 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.
</ircsession>
<para>
Pretty simple, eh?
</para>
</sect4>
<sect4>
<title>Getting/setting registry values</title>
<para>
Now, if you're curious what the current value of a
configuration variable is, you'll use the
<botcommand>config</botcommand> command with one
argument, the name of the variable you want to see the
value of:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.prefixChars
&lt;supybot&gt; jemfinch|lambda: '@'
</ircsession>
<para>
To set this value, just stick an extra argument after
the name:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.prefixChars @$
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
</ircsession>
<para>
Now, check this out:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; $config supybot.prefixChars
&lt;supybot&gt; jemfinch|lambda: '@$'
</ircsession>
<para>
Note that we used <literal>$</literal> as our prefix
character, and that the value of the configuration
variable changed. If I were to use the
<botcommand>flush</botcommand> command now, this
change would be flushed to the registry file on disk
(this would also happen if I made the bot quit, or
pressed
<keycombo>
<keycap>Ctrl</keycap>
<keycap>C</keycap>
</keycombo>
in the terminal the bot was running in). Instead,
I'll revert the change:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; $config supybot.prefixChars @
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; $note that this makes no response.
</ircsession>
<para>
If you're ever curious what the default for a given
configuration variable is, use the <botcommand>config
default</botcommand> command:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config default supybot.prefixChars
&lt;supybot&gt; jemfinch|lambda: ''
</ircsession>
<para>
Thus, to reset a configuration variable to its default
value, you can simply say:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.prefixChars [config default
supybot.prefixChars]
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; @note that this does nothing
</ircsession>
<para>
Simple, eh?
</para>
</sect4>
</sect3>
<sect3>
<title>Searching the registry</title>
<para>
Now, let's say you want to find all configuration
variables that might be even remotely related to opping.
For that, you'll want the <botcommand>config
search</botcommand> command. Check this out:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config search op
&lt;supybot&gt; jemfinch|lambda: supybot.plugins.Enforcer.autoOp,
supybot.plugins.Enforcer.autoOp.#supybot,
supybot.plugins.Enforcer.autoHalfop,
supybot.plugins.Enforcer.cycleToGetOps, supybot.plugins.Topic,
supybot.plugins.Topic.separator, and supybot.plugins.Relay.topicSync
</ircsession>
<para>
Sure, it showed up all the topic-related stuff in there,
but it also showed you all the op-related stuff, too. Do
note, however, that you can only see configuration
variables for plugins that you have loaded or that you
loaded in the past; if you've never loaded a plugin,
there's no way for the bot to know what configuration
variables it registers.
</para>
<para>
Some people might like editing their registry file
directly rather than manipulating all these things through
the bot. For those people, we offer the
<botcommand>config reload</botcommand> command, which
reloads both registry configuration and
user/channel/ignore database configuration. Just edit the
interesting files and then give the bot the
<botcommand>config reload</botcommand> command and it'll
work as expected. Do note, however, that Supybot flushes
his configuration files and databases to disk every hour
or so, and if this happens after you've edited your
configuration files but before you reload your changes,
you could lose the changes you made. To prevent this, set
the <registrygroup>supybot.flush</registrygroup> value to
<literal>Off</literal>, and no automatic flushing will
occur.
</para>
</sect3>
</sect2>
</sect1>
<sect1>
<title>All done!</title>
<para>
Anyway, that's about it for configuration. Have fun, and enjoy
your configurable bot!
</para>
</sect1>
</article>

714
docs/DocBook/example.sgml Normal file
View File

@ -0,0 +1,714 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot plugin author example</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>13 Sep 2003</date>
<revremark>Initial revision</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>14 Sep 2003</date>
<revremark>Converted to DocBook</revremark>
</revision>
<revision>
<revnumber>0.3</revnumber>
<date>24 Nov 2003</date>
<revremark>
Updated to match EXAMPLE included with 0.75.0
</revremark>
</revision>
<revision>
<revnumber>0.4</revnumber>
<date>26 Feb 2004</date>
<revremark>Converted to use Supybot DTD</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<para>
Ok, so you want to write a callback for supybot. Good, then this
is the place to be. We're going to start from the top (the
highest level, where supybot code does the most work for you) and
move lower after that.
</para>
<para>
So have you used supybot? If not, you need to go use it, get a
feel for it, see how the various commands work and such.
</para>
<para>
So now that we know you've used supybot, we'll start getting into
details.
</para>
</sect1>
<sect1>
<title>Creating your own plugin</title>
<sect2>
<title>
Using <script>scripts/newplugin.py</script>
</title>
<para>
First, the easiest way to start writing a module is to use the
wizard provided, <script>scripts/newplugin.py</script>.
Here's an example session:
</para>
<screen>
functor% scripts/newplugin.py
What should the name of the plugin be? Random
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 :)
Do you want a command-based plugin or a regexp-based plugin? [command/
regexp] command
Sometimes you'll want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a signficant amount
of time to run, you'll want to thread them so they don't block
the entire bot.
Does your plugin need to be threaded? [y/n] n
Your new plugin template is in plugins/Random.py
functor%
</screen>
<para>
So that's what it looks like. Now let's look at the source
code (if you'd like to look at it in your programming editor,
the whole plugin is available as
<filename>examples/Random.py</filename>):
</para>
<programlisting>
#!/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.
###
"""
Add the module docstring here. This will be used by the setup.py script.
"""
from baseplugin import *
import utils
import privmsgs
import callbacks
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
class Random(callbacks.Privmsg):
pass
Class = Random
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
</programlisting>
</sect2>
<sect2>
<title>Customizing the boilerplate code</title>
<para>
So a few notes, before we customize it.
</para>
<para>
You'll probably want to change the copyright notice to be your
name. It wouldn't stick even if you kept my name, so you
might as well :)
</para>
<para>
Describe what you want the plugin to do in the docstring.
This is used in <script>scripts/setup.py</script> in
order to explain to the user the purpose of the module. It's
also returned when someone asks the bot for help for a given
module (instead of help for a certain command). We'll change
this one to <literal>"Lots of stuff relating to random
numbers."</literal>
</para>
<para>
Then there are the imports. The
<module>callbacks</module>
module is used (the class you're given subclasses
<classname>callbacks.Privmsg</classname>) but the
<module>privmsgs</module> module isn't used. That's
alright; we can almost guarantee you'll use it, so we go ahead
and add the import to the template.
</para>
<para>
Then you see a <function>configure</function> function. This
the function that's called when users decide to add your
module in <script>scripts/setup.py</script>. You'll
note that by default it simply adds <literal>"load
Example"</literal> (where 'Example' is the name you provided
as the name of your plugin, so in our case it is
<literal>"load Random"</literal>) at the bottom. For many
plugins this is all you need; for more complex plugins, you
might need to ask questions and add commands based on the
answers.
</para>
</sect2>
<sect2>
<title>Digging in: customizing the plugin class</title>
<para>
Now comes the meat of the plugin: the plugin class.
</para>
<para>
What you're given is a skeleton: a simple subclass of
<classname>callbacks.Privmsg</classname> for you to start
with. Now let's add a command.
</para>
<para>
I don't know what you know about random number generators, but
the short of it is that they start at a certain number (a
seed) and they continue (via some somewhat
complicated/unpredictable algorithm) from there. This seed
(and the rest of the sequence, really) is all nice and
packaged up in Python's <module>random</module> module, the
<varname>Random</varname> object. So the first thing we're
going to have to do is give our plugin a
<varname>Random</varname> object.
</para>
<para>
Normally, when we want to give instances of a class an object,
we'll do so in the <function>__init__</function> method. And
that works great for plugins, too. The one thing you have to
be careful of is that you call the superclass
<function>__init__</function> method at the end of your own
<function>__init__</function>. So to add this
<classname>random.Random</classname> object to our plugin, we
can replace the <keyword>pass</keyword> statement with
this:
</para>
<programlisting>
def __init__(self):
self.rng = random.Random()
callbacks.Privmsg.__init__(self)
</programlisting>
<para>
(<varname>rng</varname>is an abbreviation for "random number
generator," in case you were curious)
</para>
<para>
Do be careful not to give your <function>__init__</function>
any arguments (other than <varname>self</varname>, of course).
There's no way anything will ever get to them! If you have
some sort of initial values you need to get to your plugin
before it can do anything interesting, add a command that gets
those values. By convention, those commands begin with
"start" -- check out the Relay and Enforcer plugins for
examples of such commands.
</para>
<para>
There's an easier way to get our plugin to have its own rng
than to define an <function>__init__</function>. Plugins are
unique among classes because we're always certain that there
will only be one instance -- supybot doesn't allow us to load
multiple instances of a single plugin. So instead of adding
the rng in <function>__init__</function>, we can just add it
as a attribute to the class itself. Like so (replacing the
<function>pass</function> statement again):
</para>
<programlisting>
rng = random.Random()
</programlisting>
<para>
And we save two lines of code and make our code a little more
clear :)
</para>
<para>
Now that we have an RNG, we need some way to get random
numbers. So first, we'll add a command that simply gets the
next random number and gives it back to the user. It takes no
arguments, of course (what would you give it?). Here's the
command, and I'll follow that with the explanation of what
each part means.
</para>
<programlisting>
def random(self, irc, msg, args):
"""takes no arguments
Returns the next random number generated by the random number
generator.
"""
irc.reply(msg, str(self.rng.random()))
</programlisting>
<para>
And that's it! Pretty simple, huh? Anyway, you're probably
wondering what all that <emphasis>means</emphasis>. We'll
start with the <keyword>def</keyword> statement:
</para>
<programlisting>
def random(self, irc, msg, args):
</programlisting>
<para>
What that does is define a command
<function>random</function>. You can call it by saying
"@random" (or whatever prefix character your specific bot
uses). The arguments are a bit less obvious.
<varname>self</varname> is self-evident (hah!).
<varname>irc</varname> is the <classname>Irc</classname>
object passed to the command; <varname>msg</varname> is the
original <classname>IrcMsg</classname> object. But you're
really not going to have to deal with either of these too much
(with the exception of calling <function>irc.reply</function>
or <function>irc.error</function>). What you're
<emphasis>really</emphasis> interested in is the
<varname>args</varname> arg. That if a list of all the
arguments passed to your command, pre-parsed and already
evaluated (i.e., you never have to worry about nested
commands, or handling double quoted strings, or splitting on
whitespace -- the work has already been done for you). You
can read about the <classname>Irc</classname> object in
<filename>irclib.py</filename> (you won't find
<function>.reply</function> or <function>.error</function>
there, though, because you're actually getting an
<classname>IrcObjectProxy</classname>, but that's beyond the
level we want to describe here :)). You can read about the
<varname>msg</varname> object in
<filename>ircmsgs.py</filename>. But again, aside from
calling <function>irc.reply</function> or
<function>irc.error</function>, you'll very rarely be using
these objects.
</para>
<para>
(In case you're curious, the answer is yes, you
<emphasis>must</emphasis> name your arguments <varname>(self,
irc, msg, args)</varname>. The names of those arguments is
one of the ways that supybot uses to determine which methods
in a plugin class are commands and which aren't. And while
we're talking about naming restrictions, all your commands
should be named in all-lowercase with no underscores. Before
calling a command, supybot always converts the command name to
lowercase and removes all dashes and underscores. On the
other hand, you now know an easy way to make sure a method is
never called (even if its arguments are <varname>(self, irc,
msg, args)</varname>, however unlikely that may be). Just
name it with an underscore or an uppercase letter in it :))
</para>
<para>
You'll also note that the docstring is odd. The wonderful
thing about the supybot framework is that it's easy to write
complete commands with help and everything: the docstring
<emphasis>is</emphasis> the help! Given the above docstring,
this is what a supybot does:
</para>
<ircsession>
&lt;angryman&gt; jemfinch: random takes no arguments (for more help
use the morehelp command)
&lt;jemfinch&gt; $morehelp random
&lt;angryman&gt; jemfinch: Returns the next random number from the
current random number generator.
</ircsession>
<para>
'help &lt;command&gt;' replies with the command name followed
by the first line of the command's docstring; there should be
a blank line following, and then 'morehelp &lt;command&gt;'
will reply with the remainder of the docstring. So that
explains the docstring. Now on to the actual body of the
function:
</para>
<programlisting>
irc.reply(msg, str(self.rng.random()))
</programlisting>
<para>
<function>irc.reply</function> takes two arguments, an
<classname>IrcMsg</classname> (like the one passed into your
function) and a string. The <classname>IrcMsg</classname> is
used to determine who the reply should go to and whether or
not it should be sent in private message (commands sent in
private are replied to in private). The string is the reply
to be sent. Don't worry about length restrictions or anything
-- if the string you want to send is too big for an IRC
message (and oftentimes that turns out to be the case :)) the
supybot framework handles that entirely transparently to you.
Do make sure, however, that you give
<function>irc.reply</function> a string. It doesn't take
anything else (sometimes even unicode fails!). That's why we
have "str(self.rng.random())" instead of simply
"self.rng.random()" -- we had to give
<function>irc.reply</function> a string.
</para>
<para>
Anyway, now that we have an RNG, we have a need for seed! Of
course, Python gives us a good seed already (it uses the
current time as a seed if we don't give it one) but users
might want to be able to repeat "random" sequences, so letting
them set the seed is a good thing. So we'll add a seed
command to give the RNG a specific seed:
</para>
<programlisting>
def seed(self, irc, msg, args):
"""&lt;seed&gt;
Sets the seed of the random number generator. &lt;seed&gt; must be
an int or a long.
"""
seed = privmsgs.getArgs(args)
try:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error(msg, '&lt;seed&gt; must be a valid int or long.')
return
self.rng.seed(seed)
irc.reply(msg, conf.replySuccess)
</programlisting>
<para>
So this one's a bit more complicated. But it's still pretty
simple. The method name is "seed" so that'll be the command
name. The arguments are the same, the docstring is of the
same form, so we don't need to go over that again. The body
of the function, however, is significantly different.
</para>
<para>
<function>privmsgs.getArgs</function> is a function you're
going to be seeing a lot of when you write plugins for
supybot. What it does is basically give you the right number
of arguments for your comamnd. In this case, we want one
argument. But we might have been given any number of
arguments by the user. So
<function>privmsgs.getArgs</function> joins them
appropriately, leaving us with one single "seed" argument (by
default, it returns one argument as a single value; more
arguments are returned in a tuple/list). Yes, we could've
just said "seed = args[0]" and gotten the first argument, but
what if the user didn't pass us an argument at all? Then
we've got to catch the <classname>IndexError</classname> from
<varname>args[0]</varname> and complain to the user about it.
<function>privmsgs.getArgs</function>, on the other hand,
handles all that for us. If the user didn't give us enough
arguments, it'll reply with the help string for the command,
thus saving us the effort.
</para>
<para>
So we have the seed from
<function>privmsgs.getArgs</function>. But it's a string.
The next three lines is pretty darn obvious: we're just
converting the string to a int of some sort. But if it's not,
that's when we're going to call
<function>irc.error</function>. It has the same interface as
we saw before in <function>irc.reply</function>, but it makes
sure to remind the user that an error has been encountered
(currently, that means it puts "Error: " at the beginning of
the message). After erroring, we return. It's important to
remember this <keyword>return</keyword> here; otherwise,
we'll just keep going down through the function and try to use
this "seed" variable that never got assigned. A good general
rule of thumb is that any time you use
<function>irc.error</function>, you'll want to return
immediately afterwards.
</para>
<para>
Then we set the seed -- that's a simple function on our rng
object. Assuming that succeeds (and doesn't raise an
exception, which it shouldn't, because we already read the
documentation and know that it should work) we reply to say
that everything worked fine. That's what
<varname>conf.replySuccess</varname> says. By default, it has
the very dry (and appropriately robot-like) "The operation
succeeded." but you're perfectly welcome to customize it
yourself -- <filename>conf.py</filename> was written to be
modified!
</para>
<para>
So that's a bit more complicated command. But we still
haven't dealt with multiple arguments. Let's do that
next.
</para>
<para>
So these random numbers are useful, but they're not the kind
of random numbers we usually want in Real Life. In Real Life,
we like to tell someone to "pick a number between 1 and 10."
So let's write a function that does that. Of course, we won't
hardcode the 1 or the 10 into the function, but we'll take
them as arguments. First the function:
</para>
<programlisting>
def range(self, irc, msg, args):
"""&lt;start&gt; &lt;end&gt;
Returns a number between &lt;start&gt; and &lt;end&gt;, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, required=2)
try:
end = int(end)
start = int(start)
except ValueError:
irc.error(msg, '&lt;start&gt; and &lt;end&gt; must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(msg, str(self.rng.randrange(start, end+1)))
</programlisting>
<para>
Pretty simple. This is becoming old hat by now. The only new
thing here is the call to
<function>privmsgs.getArgs</function>. We have to make sure,
since we want two values, to pass a keyword parameter
"required" into <function>privmsgs.getArgs</function>. Of
course, <function>privmsgs.getArgs</function> handles all the
checking for missing arguments and whatnot so we don't have
to.
</para>
<para>
The <classname>Random</classname> object we're using offers us
a "sample" method that takes a sequence and a number (we'll
call it <varname>N</varname>) and returns a list of
<varname>N</varname> items taken randomly from the sequence.
So I'll show you an example that takes advantage of multiple
arguments but doesn't use
<function>privmsgs.getArgs</function> (and thus has to handle
its own errors if the number of arguments isn't right).
Here's the code:
</para>
<programlisting>
def sample(self, irc, msg, args):
"""&lt;number of items&gt; [&lt;text&gt; ...]
Returns a sample of the &lt;number of items&gt; taken from the remaining
arguments. Obviously &lt;number of items&gt; must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error(msg, '&lt;number of items&gt; must be an integer.')
return
if n &gt; len(args):
irc.error(msg, '&lt;number of items&gt; must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(msg, utils.commaAndify(map(repr, sample)))
</programlisting>
<para>
Most everything here is familiar. The difference between this
and the previous examples is that we're dealing with
<varname>args</varname> directly, rather than through
<function>getArgs</function>. Since we already have the
arguments in a list, it doesn't make any sense to have
<function>privmsgs.getArgs</function> smush them all together
into a big long string that we'll just have to re-split. But
we still want the nice error handling of
<function>privmsgs.getArgs</function>. So what do we do? We
raise <classname>callbacks.ArgumentError</classname>! That's
the secret juju that <function>privmsgs.getArgs</function> is
doing; now we're just doing it ourself. Someone up our
callchain knows how to handle it so a neat error message is
returned. So in this function, if
<function>.pop(0)</function> fails, we weren't given enough
arguments and thus need to tell the user how to call us.
</para>
<para>
So we have the args, we have the number, we do a simple call
to <function>random.sample</function> and then we do this
funky <function>utils.commaAndify</function> to it. Yeah, so
I was running low on useful names :) Anyway, what it does is
take a list of strings and return a string with them joined by
a comma, the last one being joined with a comma and "and". So
the list ['foo', 'bar', 'baz'] becomes "foo, bar, and baz".
It's pretty useful for showing the user lists in a useful
form. We map the strings with <function>repr()</function>
first just to surround them with quotes.
</para>
<para>
So we have one more example. Yes, I hear your groans, but
it's pedagogically useful :) This time we're going to write a
command that makes the bot roll a die. It'll take one
argument (the number of sides on the die) and will respond
with the equivalent of "/me rolls a __" where __ is the number
the bot rolled. So here's the code:
</para>
<programlisting>
def diceroll(self, irc, msg, args):
"""[&lt;number of sides&gt;]
Rolls a die with &lt;number of sides&gt; sides. The default number
of sides is 6.
"""
try:
n = privmsgs.getArgs(args, required=0, optional=1)
if not n:
n = 6
n = int(n)
except ValueError:
irc.error(msg, 'Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.queueMsg(ircmsgs.action(ircutils.replyTo(msg), s))
raise callbacks.CannotNest
</programlisting>
<para>
There's a lot of stuff you haven't seen before in there. The
most important, though, is the first thing you'll notice
that's different: the <function>privmsg.getArgs</function>
call. Here we're offering a default argument in case the user
is too lazy to supply one (or just wants a nice, standard
six-sided die :)) <function>privmsgs.getArgs</function>
supports that; we'll just tell it that we don't
<emphasis>need</emphasis> any arguments (via
<varname>required=0</varname>) and that we <emphasis>might
like</emphasis> one argument (<varname>optional=1</varname>).
If the user provides an argument, we'll get it -- if they
don't, we'll just get an empty string. Hence the "if not n: n
= 6", where we provide the default.
</para>
<para>
Later, though, you'll see something other than
<function>irc.reply</function>. This is
<function>irc.queueMsg</function>, the general interface for
sending messages to the server. It's what
<function>irc.reply</function> is using under the covers. It
takes an <classname>IrcMsg</classname> object. Fortunately,
that's exactly what's returned by
<function>ircmsgs.action</function>. An action message, just
in case you don't know, is a /me kind of message.
<function>ircmsgs.action</function> is a helper function that
takes a target (a place to send the message, either a channel
or a person) and a payload (the thing to /me) and returns the
appropriate <classname>IrcMsg</classname> object.
<function>ircutils.replyTo</function> simply takes an
<classname>IrcMsg</classname> and returns where we should
reply to; if the message was originally sent to a channel,
we'll reply to there, if it was originally sent to us
privately, we'll reply in private.
</para>
<para>
At the end, you might be surprised by the "raise
callbacks.CannotNest". That's used simply because at the
moment you can't nest actions (just like you can't nest
anything that doesn't go through
<function>irc.reply</function>). That raise just makes sure
the user finds this out if he tries to nest this like "@rot13
[diceroll]".
</para>
<para>
So that's our plugin. 5 commands, each building in
complexity. You should now be able to write most anything you
want to do in Supybot. Except regexp-based plugins, but
that's a story for another day (and those aren't nearly as
cool as these command-based callbacks anyway :)). Now we need
to flesh it out to make it a full-fledged plugin.
</para>
</sect2>
<sect2>
<title>Finishing touches</title>
<para>
Let's take a look at that <function>configure</function>
function <script>scripts/newplugin.py</script> made
for us. Here it is, in case you've forgotten:
</para>
<programlisting>
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
</programlisting>
<para>
You remember when you first started running supybot and ran
<script>scripts/setup.py</script> and it asked you
all those questions? Well, now's your chance to ask other
users some questions of your own. In our case, with our
<plugin>Random</plugin> plugin, it might be nice to offer
the user the ability to specify a seed to use whenever the
plugin is loaded. So let's ask him if he wants to do that,
and if so, let's ask him what the seed should be.
</para>
<programlisting>
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
if yn('Do you want to specify a seed to be used for the RNG')=='y':
seed = something('What seed? It must be an int or long.')
while not seed.isdigit():
print 'That\'s not a valid seed.'
seed = something('What seed?')
onStart.append('seed %s' % seed)
</programlisting>
<para>
As you can see, what the <module>questions</module> module
does is fairly self-evident: <function>yn</function> returns
either 'y' or 'n'; <function>something</function> returns
<emphasis>something</emphasis> (but not nothing; for nothing,
you'd want <function>anything</function>). So basically we
ask some questions until we get a good seed. Then we do this
"onStart.append('seed %s' % seed)" doohickey.
<varname>onStart</varname> is a list of the commands to run
when the bot starts; we're just throwing our little piece into
it. These commands will then be written into the template
<script>scripts/setup.py</script> creates for the bot.
</para>
<para>
We've written our own plugin from scratch (well, from the
boilerplate that we got from
<script>scripts/newplugin.py</script> :)) and
survived! Now go write more plugins for supybot, and send
them to me so I can use them too :)
</para>
</sect2>
</sect1>
</article>

284
docs/DocBook/faq.sgml Normal file
View File

@ -0,0 +1,284 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article class="faq">
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot Frequently Asked Questions</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>26 Feb 2004</date>
<revremark>Changed to Supybot DTD</revremark>
</revision>
</revhistory>
</articleinfo>
<qandaset defaultlabel="qanda">
<qandaentry>
<question>
<para>
Why does my bot not recognize me or tell me that I don't
have the <capability>owner</capability> capability?
</para>
</question>
<answer>
<para>
Because you're not given it anything to recognize you
from! You'll need to identify with the bot
(<botcommand>help identify</botcommand> to see how that
works) or add your hostmask to your user record
(<botcommand>help addhostmask</botcommand> to see how that
works) for it to know that you're you. You may wish to
note that <botcommand>addhostmask</botcommand> can accept
a password; rather than identify, you can send the command
<botcommand>addhostmask myOwnerUser [hostmask]
myOwnerUserPassword</botcommand> and the bot will add your
current hostmask to your owner user (of course, you should
change <literal>myOwnerUser</literal> and
<literal>myOwnerUserPassword</literal> appropriately for
your bot).
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
How do I make Supybot op my users?
</para>
</question>
<answer>
<para>
First, you'll have to make sure that your users register
with the bot. They can do this with the
<botcommand>register</botcommand> command. After they do
so, you'll want to add the
<capability>#channel,op</capability> capability to their
user. Use the <botcommand>channel
addcapability</botcommand> command to do this. After
that, your users should be able to use the
<botcommand>op</botcommand> command to get ops.
</para>
<para>
If you want your users to be auto-opped when they join the
channel, you'll need to load the <plugin>Enforcer</plugin>
plugin and turn its <registrygroup>autoOp</registrygroup>
configuration variable on. Use the
<botcommand>config</botcommand> command to do so. Here's
an example of how to do these steps:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; I'm going to make an example session for giving
you auto-ops, for our FAQ.
&lt;dunk1&gt; ah ok ;]
&lt;jemfinch|lambda&gt; First, I need you to register with supybot, using
the "register" command (remember to send it in private).
&lt;dunk1&gt; done
&lt;jemfinch|lambda&gt; what name are you registered under?
&lt;dunk1&gt; dunk1
&lt;jemfinch|lambda&gt; ok, cool.
&lt;jemfinch|lambda&gt; @channel addcapability dunk1 op
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; now use the "op" command to get ops.
&lt;dunk1&gt; @op
&mdash; supybot gives channel operator status to dunk1
&lt;dunk1&gt; works!
&lt;dunk1&gt; ;]
&lt;jemfinch|lambda&gt; @load Enforcer
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; @config supybot.plugins.Enforcer.autoOp.#supybot On
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; ok, now cycle the channel (part and then rejoin)
&lt;&ndash; dunk1 (dunker@freebsd.nl) has left #supybot
&ndash;&gt; dunk1 (dunker@freebsd.nl) has joined #supybot
&mdash; supybot gives channel operator status to dunk1
&lt;jemfinch|lambda&gt; cool, thanks :)
</ircsession>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can users with the <capability>admin</capability>
capability change configuration variables?
</para>
</question>
<answer>
<para>
Currently, no. Since this is the first release of Supybot
that uses the registry, we wanted to stay on the
conservative side and require the
<capability>owner</capability> capability for changing all
non-channel-related configuration variables. Feel free to
make your case to us as to why a certain configuration
variable should only require the
<capability>admin</capability> capability instead of the
<capability>owner</capability> capability, and if we agree
with you, we'll change it for the next release.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
How do I make my Supybot connect to multiple servers?
</para>
</question>
<answer>
<para>
You'll need to use the <plugin>Relay</plugin> plugin. As
long as you don't call the <botcommand>relay
join</botcommand> command, it won't actually do any
relaying between channels (even if the bot is on the same
channel on different networks). In order to use the Relay
plugin, you'll want to first call the <botcommand>relay
start</botcommand> command, followed by the
<botcommand>relay connect</botcommand> command. These
commands are (unfortunately) not persistent at this time,
so you'll need to give them to the bot anytime you start
it up. We'll probably have this lack of persistence
rectified before the next release.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can Supybot do factoids?
</para>
</question>
<answer>
<para>
Supybot most certainly can! In fact, we offer two
full-fledged factoids-related plugins!
</para>
<para>
<plugin>Factoids</plugin> (written by
<nick>jemfinch</nick>) is Supybot's original
factoids-related plugin. It offers full integration with
Supybot's nested commands as well as a complete 1:n key to
factoid ratio, with lookup by individual number.
<plugin>Factoids</plugin> also uses a channel-specific
database instead of a global database, although in the
future it will likely be a configuration option whether to
use channel-specific or global databases for such plugins.
</para>
<para>
<plugin>MoobotFactoids</plugin> (written by
<nick>Strike</nick>) is much more full-featured, offering
users the ability to define factoids in a slightly more
user-friendly way, as well as parsing factoids to handle
&lt;reply&gt;, &lt;action&gt;, "see", and alternations
(defining a factoid "test" as "&lt;reply&gt;(foo|bar|baz)"
will make the bot send "foo" or "bar" or "baz" to the
channel (without the normal "test is " at the beginning)).
If you're accustomed to Moobot's factoids or Blootbot's
factoids, then this is the Factoids plugin for you.
Unfortunately, due to the more natural definition syntax
(required to be compatible with Moobot) you can't define
Factoids with nested commands; you'll have to evaluate the
command first and then copy the result into your factoid
definition. <plugin>MoobotFactoids</plugin> uses a global
database, so the factoids are the same for all channels.
</para>
<para>
In the future, we plan to have a compatibility plugin for
Infobot, but as of present we've not yet written one.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can I import my Infobot/Blootbot/Moobot factoids into
Supybot?
</para>
</question>
<answer>
<para>
As of present, we have no automated way to do so.
<nick>Strike</nick> has written a few scripts for
importing a Moobot database into
<plugin>MoobotFactoids</plugin>, however, so you'll want
to talk to him about helping you with that. We're
certainly happy to help you convert such databases; if you
can provide us with such a database exported to a flat
file, we can probably do the rest of the work to write a
script that imports it into a database for one of our
factoids-related plugins.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
I found a bug, what do I do?
</para>
</question>
<answer>
<para>
Submit it on Sourceforge through our Sourceforge project
page:
<ulink
url="http://sourceforge.net/tracker/?group_id=58965&amp;atid=489447">
http://sourceforge.net/tracker/?group_id=58965&amp;atid=489447
</ulink>. If Sourceforge happens to be down when you try
to submit your bug, then post it in the "Supybot Developer
Discussion" forum at our forums at
<ulink url="http://forums.supybot.org">
http://forums.supybot.org/
</ulink>. If that doesn't work, email
<email>supybot-bugs@lists.sourceforge.net</email>. If
that doesn't work, email
<email>jemfinch@supybot.org</email>. If that doesn't
work, find yourself some carrier pigeons and &hellip; hah!
You thought I was serious!
</para>
<para>
Anyway, when you submit your bug, we'll need several
things. If the bug involved an uncaught exception, we
need the traceback (basically the stuff from
&ldquo;Uncaught exception in &hellip;&rdquo; to the next
log entry). We'd also like to see the commands that
caused the bug, or happened around the time you saw the
bug. If the bug involved a database, we'd love to see the
database. Remember, it's always worse to send us too much
information in a bug report than too little.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Karma doesn't seem to work for me.
</para>
</question>
<answer>
<para>
<plugin>Karma</plugin> by default doesn't acknowledge
karma updates. If you check the karma of whatever you
increased/decreased, you'll note that your increment or
decrement still took place. If you'd rather
<plugin>Karma</plugin> acknowledge karma updates, change
the
<registrygroup>supybot.plugins.Karma.response</registrygroup>
configuration variable to <literal>On</literal>.
</para>
</answer>
</qandaentry>
</qandaset>
</article>

View File

@ -0,0 +1,297 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Getting started with Supybot</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>18 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Introduction</title>
<para>
Ok, so you've decided to try out Supybot. That's great! The more
people who use Supybot, the more people can submit bugs and help
us to make it the best IRC bot in the world :)
</para>
<para>
First things first: Supybot <emphasis>requires</emphasis> Python
2.3. There ain't no getting around it. 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, <emphasis>we're</emphasis>
Python developers and we like banana splits.
</para>
</sect1>
<sect1>
<title>Installing the bot and its utilities</title>
<para>
So what do you do? First thing you'll want to do is run (with
root/admin privileges) <application>python setup.py
install</application>. This will install Supybot globally. If
you need to install locally for whatever reason, see this <ulink
url="http://tinyurl.com/2tb37">forum post</ulink> on how to do so.
You'll then have several new programs installed where Python
scripts are normally installed on your system
(<filename>/usr/bin</filename> or
<filename>/usr/local/bin</filename> are common on UNIX systems;
<filename>C:\Python23\Scripts</filename> is a common place on
Windows; and (watch out, this is a long one :))
<filename>/System/Library/Frameworks/Python.framework/Versions/2.3/bin</filename>
is a common place on MacOS X.). The two that might be of
particular interest to you, the new user, are
<script>supybot</script> and
<script>supybot-wizard</script> The former
(<script>supybot</script> is the script to run an actual
bot; the latter (<script>supybot-wizard</script> is an
in-depth wizard that provides a nice user interface for creating
configuration files for your bot. We'd prefer you to the use
<script>supybot-wizard</script>, but if you're in a
hurry or don't feel like being asked many questions, just run
supybot with no arguments and it'll ask you only the questions
necessary ")to run a bot.
</para>
</sect1>
<sect1>
<title>Firing up the bot for the first time</title>
<para>
So after running either of those two programs, 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
<script>supybot</script> 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.
</para>
</sect1>
<sect1>
<title>Your first interactions with the bot</title>
<para>
Ok, so let's assume your bot connected to the server fine and
joined the channels you told it to join. For now we'll assume you
named your bot <nick>supybot</nick> (you probably didn't,
but it'll make it much clearer in the examples that follow to
assume that you did). We'll also assume that you told it to join
<channel>#channel</channel> (a nice generic name for a channel,
isn't it? :)) So what do you do with this bot that you just made
to join your channel? Try this in the channel:
</para>
<ircsession>
supybot: list
</ircsession>
<para>
Replacing <nick>supybot</nick> with the actual name you
picked for your bot, of course. Your bot should reply with a list
of the plugins he currently has loaded. At least
<plugin>Admin</plugin>, <plugin>Channel</plugin>,
<plugin>Config</plugin>, <plugin>Misc</plugin>,
<plugin>Owner</plugin>, and <plugin>User</plugin> should be
there; if you used <script>supybot-wizard</script> to
create your configuration file you may have many more plugins
loaded. The <botcommand>list</botcommand> command can also be used to
list the commands in a given plugin:
</para>
<ircsession>
supybot: list Misc
</ircsession>
<para>
Will list all the commands in the <plugin>Misc</plugin> plugin.
</para>
<sect2>
<title>Accessing the bot's online help</title>
<para>
If you want to see the help for any command, just use
the <botcommand>help</botcommand> command:
</para>
<ircsession>
supybot: help help
supybot: help list
supybot: help load
</ircsession>
</sect2>
<sect2>
<title>Dealing with ambiguous commands</title>
<para>
Sometimes more than one plugin will have a given command; for
instance, the <botcommand>list</botcommand> command exists in both
the <plugin>Misc</plugin> and <plugin>Config</plugin>
plugins (both loaded by default). <plugin>List</plugin>, in
this case, defaults to the <plugin>Misc</plugin> plugin, but
you may want to get the help for the
<botcommand>list</botcommand>
command in the <plugin>Config</plugin> plugin. In that
case, you'll want to give your command like this:
</para>
<ircsession>
supybot: help config list
</ircsession>
<para>
Anytime your bot tells you that a given command is defined in
several plugins, you'll want to use this syntax
(<botcommand>plugin command</botcommand>) to disambiguate which
plugin's command you wish to call. For instance, if you
wanted to call the <plugin>Config</plugin> plugin's
<botcommand>list</botcommand> command, then you'd need to say:
</para>
<ircsession>
supybot: config list
</ircsession>
<para>
Rather than just <botcommand>list</botcommand>.
</para>
</sect2>
<sect2>
<title>Loading plugins</title>
<para>
Now that you know how to deal with plugins having commands
with the same name, let's take a look at loading other
plugins. If you didn't use
<script>supybot-wizard</script>, though, you might
do well to try it before playing around with loading plugins
yourself: each plugin has its own
<function>configure</function> function that the wizard uses
to setup the appropriate registry entries if the plugin
requires any.
</para>
<sect3>
<title>Identifying yourself as the bot owner</title>
<para>
Now, if you do want to play around with loading plugins,
you're going to need to have the
<capability>owner</capability>
capability. If you ran the wizard, then chances are you
already added an owner user for yourself. If not,
however, you can add one via the handy-dandy
<script>supybot-adduser</script> script. You'll
want to run it while the bot is not running (otherwise it
could overwrite
<script>supybot-adduser</script>'s changes to
your user database before you get a chance to reload
them). Just follow the prompts, and when it asks if you
want to give the user any capabilities, say yes and then
give yourself the <capability>owner</capability> capability
(without the quotes), restart the bot and you'll be ready
to load some plugins!
</para>
<para>
Now, in order for the bot to recognize you as your owner
user, you'll have to identify with the bot. Open up a
query window in your irc client (/query should do it; if
not, just know that you can't identify in a channel
because it requires sending your password to the bot).
Then type this:
</para>
<ircsession>
help identify
</ircsession>
<para>
And follow the instructions; the command you send will
probably look like this, with your owner user and password
replaced:
</para>
<ircsession>
identify myowneruser myuserpassword
</ircsession>
<para>
The bot will tell you that &ldquo;The operation
succeeded&rdquo; if you got the right name and password.
Now that you're identified, you can do anything that
requires any privilege: that includes all the commands in
the <plugin>Owner</plugin> and <plugin>Admin</plugin>
plugins, which you may want to take a look at (using the
<botcommand>list</botcommand> and
<botcommand>help</botcommand>
commands, of course). One command in particular that you
might want to use (it's from the <plugin>User</plugin>
plugin) is the <botcommand>addhostmask</botcommand> command: it
lets you add a hostmask to your user record so the bot
recognizes you by your hostmask instead of requiring you
to always identify with it before it recognizes you. Use
the <botcommand>help</botcommand> command to see how this
command works. Here's how I often use it:
</para>
<ircsession>
addhostmask myuser [hostmask] mypassword
</ircsession>
<para>
You may not have seen that "[hostmask]" syntax before.
Supybot allows nested commands, which means that any
command's output can be nested as an argument to another
command. The hostmask command from the
<plugin>Misc</plugin> plugin returns the hostmask of a
given nick, but if given no arguments, it returns the
hostmask of the person giving the command. So the command
above adds the hostmask I'm currently using to my user's
list of recognized hostmasks. I'm only required to give
<literal>mypassword</literal> if I'm not already
identified with the bot.
</para>
</sect3>
</sect2>
<sect2>
<title>The <botcommand>more</botcommand> command</title>
<para>
Another command you might find yourself needing somewhat often
is the <botcommand>more</botcommand> command. The IRC protocol
limits messages to 512 bytes, 60 or so of which must be
devoted to some bookkeeping. Sometimes, however, Supybot
wants to send a message that's longer than that. What it
does, then, is break it into "chunks" and send the first one,
following it with "(X more messages)" where X is how many more
chunks there are. To get to these chunks, use the more
command. One way to try is to look at the listing of
configuration groups for the bot (more on this in the
CONFIGURATION document) by giving the command "config list
supybot". Last I checked, it'll overflow into a second chunk.
When you invoke this command, you should see output like:
</para>
<ircsession>
&lt;supybot&gt; nick, ident, user, server, password, channels, prefixChars,
defaultCapabilities, defaultAllow, defaultIgnore,
humanTimestampFormat, externalIP, bracketSyntax, pipeSyntax,
followIdentificationThroughNickChanges, alwaysJoinOnInvite,
showSimpleSyntax, maxHistoryLength, nickmods, throttleTime,
snarfThrottle, threadAllCommands, pingServer, pingInterval,
upkeepInterval, flush, (1 more message)
</ircsession>
<para>
Now, to see the rest of the output, simply give the command
<botcommand>more</botcommand>, and it will show you the rest:
</para>
<ircsession>
&lt;jemfinch&gt; more
&lt;supybot&gt; httpPeekSize, and defaultSocketTimeout
</ircsession>
</sect2>
</sect1>
<sect1>
<title>You're ready!</title>
<para>
You should now have a solid foundation for using Supybot. Be sure
to check the help that is built-in to the bot itself if you have
any questions, and enjoy using Supybot!
</para>
</sect1>
</article>

View File

@ -0,0 +1,585 @@
<!DOCTYPE article SYSTEM "supybot.dtd">
<article>
<articleinfo>
<authorgroup>
<author>
<firstname>Jeremiah</firstname>
<surname>Fincher</surname>
</author>
<editor>
<firstname>Daniel</firstname>
<surname>DiPaolo</surname>
<contrib>DocBook translator</contrib>
</editor>
</authorgroup>
<title>Supybot developer interfaces</title>
<revhistory>
<revision>
<revnumber>0.1</revnumber>
<date>19 Feb 2004</date>
<revremark>Initial Docbook translation</revremark>
</revision>
<revision>
<revnumber>0.2</revnumber>
<date>26 Feb 2004</date>
<revremark>Converted to Supybot DTD</revremark>
</revision>
</revhistory>
</articleinfo>
<sect1>
<title>Available interfaces</title>
<para>
These are the interfaces for some of the objects you'll deal with
if you code for Supybot.
</para>
<sect2>
<title><classname>ircmsgs.IrcMsg</classname>
<para>
This is the object that represents an IRC message. It has
several methods and attributes. The most important thing
about this class, however, is that it <emphasis>is</emphasis>
hashable, and thus <emphasis>cannot</emphasis> be modified.
Do not change any attributes; any code that modifies an IRC
message is <emphasis>broken</emphasis> and should not exist.
</para>
<variablelist>
<title>Interesting methods</title>
<varlistentry>
<term>__init__</term>
<listitem>
<para>
One of the more complex initializers in a class.
It can be used in three different ways:
</para>
<orderedlist numeration="arabic" spacing="normal">
<listitem>
<para>
It can be given a string, as one received
from the server, which it will then parse
into its separate components and
instantiate the class with those
components as attributes.
</para>
</listitem>
<listitem>
<para>
It can be given a command, some (optional)
arguments, and a (optional) prefix, and
will instantiate the class with those
components as attributes.
</para>
</listitem>
<listitem>
<para>
It can be given, in addition to any of the
above arguments, a <varname>msg</varname>
keyword argument that will use the
attributes of msg as defaults. This
exists to make it easier to copy messages,
since the class is immutable.
</para>
</listitem>
</orderedlist>
</listitem>
</varlistentry>
<varlistentry>
<term>__str__</term>
<listitem>
<para>
This returns the message in a string form suitable
for sending to a server.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>__repr__</term>
<listitem>
<para>
This returns the message in a form suitable for
<function>eval()</function>, assuming the name
<varname>IrcMsg</varname> is in your namespace and
is bound to this class.
</para>
</listitem>
</varlistentry>
</variablelist>
<para>
The following attributes are the meat of this class. These
are generally what you'll be looking at with
<varname>IrcMsg</varname>s.
</para>
<variablelist>
<title>Interesting attributes</title>
<varlistentry>
<term>command</term>
<listitem>
<para>
This is the command of the
<varname>IrcMsg</varname> &ndash;
<literal>PRIVMSG</literal>,
<literal>NOTICE</literal>,
<literal>WHOIS</literal>,
etc.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>args</term>
<listitem>
<para>
This is a tuple of the arguments to the
<varname>IrcMsg</varname>. Some messages have
arguments, some don't, depending on what command
they are. You are, of course, always assured that
<varname>args</varname> exists and is a tuple,
though it might be empty.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>prefix</term>
<listitem>
<para>
This is the hostmask of the person/server the
message is from. In general, you won't be setting
this on your outgoing messages, but incoming
messages will always have one. This is the whole
hostmask; if the message was received from a
server, it'll be the server's hostmask; if the
message was received from a user, it'll be the
whole user hostmask. In that case, however, it's
also parsed out into the
<varname>nick</varname>/<varname>user</varname>/<varname>host</varname>
attributes, which are probably more useful to
check for many purposes.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>nick</term>
<listitem>
<para>
If the message was sent by a user, this will be
the nick of the user. If it was sent by a server,
this will be the server's name (something like
<literal>calvino.freenode.net</literal> or
similar).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>user</term>
<listitem>
<para>
If the message was sent by a user, this will be
the user string of the user &ndash; what they put
into their IRC client for their "full name." If
it was sent by a server, it'll be the server's
name, again.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>host</term>
<listitem>
<para>
If the message was sent by a user, this will be
the host portion of their hostmask. If it was
sent by a server, it'll be the server's name (yet
again :))
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title><classname>irclib.Irc</classname>
<para>
This is the object to handle everything about IRC except the
actual connection to the server itself.
(<emphasis>NOTE</emphasis> that the object actually received
by commands in subclasses of
<classname>callbacks.Privmsg</classname> is an
<classname>IrcObjectProxy</classname>, which is described
later. It augments the following interface with several
methods of its own to help plugin authors.)
</para>
<variablelist>
<title>Interesting methods</title>
<varlistentry>
<term>queueMsg</term>
<listitem>
<para>
Queues a message for sending to the server. The
queue is generally FIFO, but it does prioritize
messages based on their command.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>sendMsg</term>
<listitem>
<para>
Queues a message for sending to the server prior
to any messages in the normal queue. This is
exactly a FIFO queue, no reordering is done at
all.
</para>
</listitem>
</varlistentry>
<!--<note>
<para>
The following two methods are the most important for
people writing new <varname>IrcDriver</varname>s.
Otherwise, you really don't need to pay attention to
them.
</para>
</note>-->
<varlistentry>
<term>feedMsg</term>
<listitem>
<para>
Feeds the <varname>Irc</varname> object a message
for it handle appropriately, as well as passing it
on to callbacks.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>takeMsg</term>
<listitem>
<para>
If the <varname>Irc</varname> object has a message
it's ready to send to the server, this will return
it. Otherwise, it will return
<literal>None</literal>.
</para>
</listitem>
</varlistentry>
<!--<note>
<para>
The next several methods are of far more marginal
utility. But someone may need them, so they're
documented here.
</para>
</note>-->
<varlistentry>
<term>addCallback</term>
<listitem>
<para>
Takes a callback to add to the list of callbacks
in the <varname>Irc</varname> object. See the
interface for <varname>IrcCallback</varname> for
more information.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>getCallback</term>
<listitem>
<para>
Gets a callback by name, if it is in the
<varname>Irc</varname> object's list of callbacks.
If it it isn't, returns <literal>None</literal>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>removeCallback</term>
<listitem>
<para>
Removes a callback by name. Returns a list of the
callbacks removed (since it is technically
possible to have multiple callbacks with the same
name. This list may be empty.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>__init__</term>
<listitem>
<para>
Requires a <varname>nick</varname>. Optional
arguments include <varname>user</varname> and
<varname>ident</varname>, which default to the
nick given, <varname>password</varname>, which
defaults to the empty password, and
<varname>callbacks</varname>, a list of callbacks
(which defaults to nothing, an empty list).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>reset</term>
<listitem>
<para>
Resets the <varname>Irc</varname> object to its
original state, as well as sends a
<function>reset()</function> to every callbacks.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>die</term>
<listitem>
<para>
Kills the IRC object and all its callbacks.
</para>
</listitem>
</varlistentry>
</variablelist>
<variablelist>
<title>Interesting attributes</title>
<varlistentry>
<term>nick</term>
<listitem>
<para>
The current nick of the bot.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>prefix</term>
<listitem>
<para>
The current prefix of the bot.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>server</term>
<listitem>
<para>
The current server the bot is connected to.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>network</term>
<listitem>
<para>
The current network name the bot is connected to.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>afterConnect</term>
<listitem>
<para>
<literal>False</literal> until the bot has
received a command sent after the connection is
finished &ndash; 376, 377, or 422.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>state</term>
<listitem>
<para>
An <varname>IrcState</varname> object for this
particular connection. See the interface for the
<varname>IrcState</varname> object for more
information.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title><classname>irclib.IrcCallback</classname></title>
<variablelist>
<title>Interesting Methods</title>
<varlistentry>
<term>name</term>
<listitem>
<para>
Returns the name of the callback. The default
implementation simply returns the name of the
class.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>__call__</term>
<listitem>
<para>
Called by the <varname>Irc</varname> object with
itself and the message whenever a message is fed
to the <varname>Irc</varname> object. Nothing is
done with the return value.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>inFilter</term>
<listitem>
<para>
Called by the <varname>Irc</varname> object with
itself and the message whenever a message is fed
to the <varname>Irc</varname> object. The return
value should be an <varname>IrcMsg</varname>
object to be passed to the next callback in the
<varname>Irc</varname>'s list of callbacks. If
<literal>None</literal> is returned, all
processing stops. This gives callbacks an
oppurtunity to "filter" incoming messages before
general callbacks are given them.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>outFilter</term>
<listitem>
<para>
Basically equivalent to
<varname>inFilter</varname>, except instead of
being called on messages as they enter the
<varname>Irc</varname> object, it's called on
messages as they leave the <varname>Irc</varname>
object.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>die</term>
<listitem>
<para>
Called when the parent <varname>Irc</varname> is
told to die. This gives callbacks an oppurtunity
to close open files, network connections, or
databases before they're deleted.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>reset</term>
<listitem>
<para>
Called when the parent <varname>Irc</varname> is
told to reset (which is generally when
reconnecting to the server). Most callbacks don't
need to define this.
</para>
</listitem>
</varlistentry>
</variablelist>
<variablelist>
<title>Interesting attributes</title>
<varlistentry>
<term>priority</term>
<listitem>
<para>
Determines the priority of the callback in the
<varname>Irc</varname> object's list of callbacks.
Defaults to <literal>99</literal>; the valid range
includes <literal>0</literal> through
<literal>sys.maxint-1</literal> (don't use
<literal>sys.maxint</literal> itself, that's
reserved for the <varname>Misc</varname> plugin).
The lower the number, the higher the priority.
High priority callbacks are called earlier in the
<varname>inFilter</varname> cycle, earlier in the
<varname>__call__</varname> cycle, and later in
the <varname>outFilter</varname> cycle &ndash;
basically, they're given the first chances on the
way in and the last chances on the way out.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
<sect2>
<title><classname>callbacks.IrcObjectProxy</classname></title>
<para>
<classname>IrcObjectProxy</classname> is a proxy for an
<classname>irclib.Irc</classname> instance that serves to
provide a much fuller interface for handling replies and
errors as well as to handle the nesting of commands. This is
what you'll be dealing with almost all the time when writing
commands; when writing <function>doCommand</function> methods
(the kind you read about in the interface description of
<classname>irclib.IrcCallback</classname>) you'll be dealing
with plain old <classname>irclib.Irc</classname> objects.
</para>
<variablelist>
<title>Interesting methods</title>
<varlistentry>
<term>reply</term>
<listitem>
<para>
Called to reply to the current message with a
string that is to be the reply. Uses the
<function>queueMsg</function> command discussed in
the <classname>irclib.Irc</classname> section.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>replySuccess</term>
<term>replyError</term>
<listitem>
<para>
These reply with the configured responses for
success and generic error, respectively. If an
additional argument is given, it's (intelligently)
appended to the generic message to be more
specific.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>error</term>
<listitem>
<para>
Called to send an error reply to the current
message; not only does the response indicate an
error, but commands that error out break the
nested-command chain, which is generally useful
for not confusing the user :)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>errorNoCapability</term>
<listitem>
<para>
Like <function>error</function>, except it accepts
the capability that's missing and integrates it
into the configured error message for such things.
Also accepts an additional string for a more
descriptive message, if that's what you want.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>errorPossibleBug</term>
<term>errorNotRegistered</term>
<term>errorNoUser</term>
<term>errorRequiresPrivacy</term>
<listitem>
<para>
These methods reply with the appropriate
configured error message for the conditions in
their names; they all take an additional arguments
to be more specific about the conditions they
indicate, but this argument is very rarely
necessary.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>getRealIrc</term>
<listitem>
<para>
Returns the actual <classname>Irc</classname>
object being proxied for.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect2>
</sect1>
</article>

View File

@ -0,0 +1,46 @@
(define %stylesheet% "../stylesheets/supybot.css")
(element botcommand
(make element gi: "span"
attributes: '(("class" "botcommand"))
(process-children)))
(element plugin
(make element gi: "span"
attributes: '(("class" "plugin"))
(process-children)))
(element flag
(make element gi: "span"
attributes: '(("class" "flag"))
(process-children)))
(element nick
(make element gi: "span"
attributes: '(("class" "nick"))
(process-children)))
(element capability
(make element gi: "span"
attributes: '(("class" "capability"))
(process-children)))
(element registrygroup
(make element gi: "span"
attributes: '(("class" "registrygroup"))
(process-children)))
(element ircsession
(make element gi: "pre"
attributes: '(("class" "ircsession"))
(process-children)))
(element script
(make element gi: "span"
attributes: '(("class" "script"))
(process-children)))
(element channel
(make element gi: "span"
attributes: '(("class" "channel"))
(process-children)))

View File

@ -0,0 +1,43 @@
(define %mono-font-family% "Courier New")
(element botcommand
(make sequence
font-family-name: %mono-font-family%))
(element plugin
(make sequence
font-weight: 'bold))
(element flag
(make sequence
font-posture: 'italic))
(element nick
(make sequence
font-family-name: %mono-font-family%))
(element capability
(make sequence
font-weight: 'bold))
(element registrygroup
(make sequence
font-weight: 'bold))
(element ircsession
(make paragraph
font-family-name: %mono-font-family%
space-before: 12pt
space-after: 12pt
start-indent: 6pt
lines: 'asis
input-whitespace-treatment: 'preserve))
(element script
(make sequence
font-family-name: %mono-font-family%))
(element channel
(make sequence
font-weight: 'bold))

41
docs/DocBook/supybot.css Normal file
View File

@ -0,0 +1,41 @@
.channel {
font-weight: bold;
}
.botcommand {
font-family: monospace;
}
.flag {
font-family: monospace;
}
.nick {
font-style: italic;
}
.plugin {
font-family: monospace;
}
.capability {
font-family: monospace;
}
.registrygroup {
font-family: monospace;
}
.ircsession {
font-family: monospace;
display: block;
background-color: #666666;
}
.script {
font-family: monospace;
}
.channel {
font-weight: bold;
}

23
docs/DocBook/supybot.dsl Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE style-sheet PUBLIC "-//James Clark//DTD DSSSL Style Sheet//EN" [
<!ENTITY print-ss PUBLIC
"-//Norman Walsh//DOCUMENT DocBook Print Stylesheet//EN" CDATA DSSSL>
<!ENTITY html-ss PUBLIC
"-//Norman Walsh//DOCUMENT DocBook HTML Stylesheet//EN" CDATA DSSSL>
<!ENTITY supybot-print SYSTEM "supybot-print.dsl">
<!ENTITY supybot-html SYSTEM "supybot-html.dsl">
]>
<style-sheet>
<style-specification id="print" use="print-stylesheet">
<style-specification-body>
&supybot-print;
</style-specification-body>
</style-specification>
<style-specification id="html" use="html-stylesheet">
<style-specification-body>
&supybot-html;
</style-specification-body>
</style-specification>
<external-specification id="print-stylesheet" document="print-ss">
<external-specification id="html-stylesheet" document="html-ss">
</style-sheet>

139
docs/DocBook/supybot.dtd Normal file
View File

@ -0,0 +1,139 @@
<!-- Segregate all of our stuff into its own class for possible extension
later and just because I wanted to write my own class :) -->
<!ENTITY % local.supybot.tech.char.class "">
<!ENTITY % supybot.tech.char.class "BotCommand|Plugin|Flag|Nick|Capability
|RegistryGroup|Registry|Script
|Channel %local.supybot.tech.char.class;">
<!-- Stuff that isn't supybot-specific, but it's python-related and no
suitable element exists in the DocBook DTD -->
<!ENTITY % local.python.tech.char.class "">
<!ENTITY % python.tech.char.class "Module|Keyword
%local.python.tech.char.class;">
<!-- Pretty much all of our stuff fits where stuff in the tech.char class
goes, so we simply add our stuff using the local extension -->
<!ENTITY % local.tech.char.class "|%supybot.tech.char.class;
|%python.tech.char.class;">
<!-- linespecific is the same class as things like screen and programlisting,
so it's added here to fit with the DocBook stuff (i.e., so putting an
ircsession in where one of those previous two elements would be is a
valid operation -->
<!ENTITY % local.linespecific.class "|IrcSession">
<!-- Source the original DocBook DTD -->
<!ENTITY % DocBookDTD PUBLIC "-//OASIS//DTD DocBook V4.1//EN">
%DocBookDTD;
<!ELEMENT BotCommand - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.botcommand.attrib "">
<!ENTITY % botcommand.role.attrib "%role.attrib;">
<!ATTLIST BotCommand
%common.attrib;
%local.botcommand.attrib;
%botcommand.role.attrib;
>
<!ELEMENT Plugin - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.plugin.attrib "">
<!ENTITY % plugin.role.attrib "%role.attrib;">
<!ATTLIST Plugin
%common.attrib;
%local.plugin.attrib;
%plugin.role.attrib;
>
<!ELEMENT Flag - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.flag.attrib "">
<!ENTITY % flag.role.attrib
"
flagtype (arg|noarg) #IMPLIED
%role.attrib;"
>
<!ATTLIST Flag
%common.attrib;
%local.flag.attrib;
%flag.role.attrib;
>
<!ELEMENT Nick - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.nick.attrib "">
<!ENTITY % nick.role.attrib "%role.attrib;">
<!ATTLIST Nick
%common.attrib;
%local.nick.attrib;
%nick.role.attrib;
>
<!ELEMENT Capability - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.capability.attrib "">
<!ENTITY % capability.role.attrib "%role.attrib;">
<!ATTLIST Capability
%common.attrib;
%local.capability.attrib;
%capability.role.attrib;
>
<!ELEMENT Comment - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.comment.attrib "">
<!ENTITY % comment.role.attrib "%role.attrib;">
<!ATTLIST Comment
%common.attrib;
%local.comment.attrib;
%comment.role.attrib;
>
<!ELEMENT RegistryGroup - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.registrygroup.attrib "">
<!ENTITY % registrygroup.role.attrib "%role.attrib;">
<!ATTLIST RegistryGroup
%common.attrib;
%local.registrygroup.attrib;
%registrygroup.role.attrib;
>
<!ELEMENT Registry - - ((RegistryGroup|Comment)+)>
<!ENTITY % local.registry.attrib "">
<!ENTITY % registry.role.attrib "%role.attrib;">
<!ATTLIST Registry
%common.attrib;
%local.registry.attrib;
%registry.role.attrib;
>
<!ELEMENT IrcSession - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.ircsession.attrib "">
<!ENTITY % ircsession.role.attrib "%role.attrib;">
<!ATTLIST IrcSession
%common.attrib;
%local.ircsession.attrib;
%ircsession.role.attrib;
>
<!ELEMENT Script - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.script.attrib "">
<!ENTITY % script.role.attrib "%role.attrib;">
<!ATTLIST Script
%common.attrib;
%local.script.attrib;
%script.role.attrib;
>
<!ELEMENT Channel - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.channel.attrib "">
<!ENTITY % channel.role.attrib "%role.attrib;">
<!ATTLIST Channel
%common.attrib;
%local.channel.attrib;
%channel.role.attrib;
>
<!ELEMENT Module - - ((%smallcptr.char.mix)+)>
<!ENTITY % local.module.attrib "">
<!ENTITY % module.role.attrib "%role.attrib;">
<!ATTLIST Module
%common.attrib;
%local.module.attrib;
%module.role.attrib;
>

439
docs/EXAMPLE Normal file
View File

@ -0,0 +1,439 @@
Ok, so you want to write a callback for Supybot. Good, then this is
the place to be. We're going to start from the top (the highest
level, where Supybot code does the most work for you) and move lower
after that.
So have you used Supybot? If not, you need to go use it, get a feel
for it, see how the various commands work and such.
So now that we know you've used Supybot, we'll start getting into
details.
First, the easiest way to start writing a module is to use the wizard
provided, scripts/newplugin.py. Here's an example session:
-----
functor% supybot-newplugin
What should the name of the plugin be? Random
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 :)
Do you want a command-based plugin or a regexp-based plugin? [command/
regexp] command
Sometimes you'll want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a signficant amount
of time to run, you'll want to thread them so they don't block
the entire bot.
Does your plugin need to be threaded? [y/n] n
Your new plugin template is in plugins/Random.py
functor%
-----
So that's what it looks like. Now let's look at the source code (if
you'd like to look at it in your programming editor, the whole plugin
is available as examples/Random.py):
-----
#!/usr/bin/env python
###
# 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.
###
"""
Add the module docstring here. This will be used by the setup.py script.
"""
import plugins
import conf
import utils
import privmsgs
import callbacks
def configure(advanced):
# This will be called by setup.py 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 questions import expect, anything, something, yn
conf.registerPlugin('Random', True)
class Random(callbacks.Privmsg):
pass
Class = Random
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
-----
So a few notes, before we customize it.
You'll probably want to change the copyright notice to be your name.
It wouldn't stick even if you kept my name, so you might as well :)
Describe what you want the plugin to do in the docstring. This is
used in supybot-wizard in order to explain to the user the purpose of
the module. It's also returned when someone asks the bot for help for
a given module (instead of help for a certain command). We'll change
this one to "Lots of stuff relating to random numbers."
Then there are the imports. The callbacks module is used (the class
you're given subclasses callbacks.Privmsg) but the privmsgs module
isn't used. That's alright; we can almost guarantee you'll use it, so
we go ahead and add the import to the template.
Then you see a "configure" function. This is the function that's
called when users decide to add your module in supybot-wizard. You'll
note that by default it simply registers the plugin to be
automatically loaded on startup. For many plugins this is all you
need; for more complex plugins, you might need to ask questions and
add commands based on the answers.
Now comes the meat of the plugin: the plugin class.
What you're given is a skeleton: a simple subclass of
callbacks.Privmsg for you to start with. Now let's add a command.
I don't know what you know about random number generators, but the
short of it is that they start at a certain number (a seed) and they
continue (via some somewhat complicated/unpredictable algorithm) from
there. This seed (and the rest of the sequence, really) is all nice
and packaged up in Python's random module, the Random object. So the
first thing we're going to have to do is give our plugin a Random
object.
Normally, when we want to give instances of a class an object, we'll
do so in the __init__ method. And that works great for plugins, too.
The one thing you have to be careful of is that you call the
superclass __init__ method at the end of your own __init__. So to add
this random.Random object to our plugin, we can replace the "pass"
statement with this:
def __init__(self):
self.rng = random.Random()
callbacks.Privmsg.__init__(self)
(rng is an abbreviation for "random number generator," in case you
were curious)
Do be careful not to give your __init__ any arguments (other than
self, of course). There's no way anything will ever get to them! If
you have some sort of initial values you need to get to your plugin
before it can do anything interesting, add a command that gets those
values. By convention, those commands begin with "start" -- check out
the Relay plugin for an example of such a command.
There's an easier way to get our plugin to have its own rng than to
define an __init__. Plugins are unique among classes because we're
always certain that there will only be one instance -- supybot doesn't
allow us to load multiple instances of a single plugin. So instead of
adding the rng in __init__, we can just add it as a attribute to the
class itself. Like so (replacing the "pass" statement again):
rng = random.Random()
And we save two lines of code and make our code a little more clear :)
Now that we have an RNG, we need some way to get random numbers. So
first, we'll add a command that simply gets the next random number and
gives it back to the user. It takes no arguments, of course (what
would you give it?). Here's the command, and I'll follow that with
the explanation of what each part means.
def random(self, irc, msg, args):
"""takes no arguments
Returns the next random number generated by the random number
generator.
"""
irc.reply(str(self.rng.random()))
And that's it! Pretty simple, huh? Anyway, you're probably wondering
what all that *means*. We'll start with the def statement:
def random(self, irc, msg, args):
What that does is define a command "random". You can call it by
saying "@random" (or whatever prefix character your specific bot
uses). The arguments are a bit less obvious. Self is self-evident
(hah!). irc is the Irc object passed to the command; msg is the
original IrcMsg object. But you're really not going to have to deal
with either of these too much (with the exception of calling irc.reply
or irc.error). What you're *really* interested in is the args arg.
That if a list of all the arguments passed to your command, pre-parsed
and already evaluated (i.e., you never have to worry about nested
commands, or handling double quoted strings, or splitting on
whitespace -- the work has already been done for you). You can read
about the Irc object in irclib.py (you won't find .reply or .error
there, though, because you're actually getting an IrcObjectProxy, but
that's beyond the level we want to describe here :)). You can read
about the msg object in ircmsgs.py. But again, you'll very rarely be
using these objects.
(In case you're curious, the answer is yes, you *must* name your
arguments (self, irc, msg, args). The names of those arguments is one
of the ways that supybot uses to determine which methods in a plugin
class are commands and which aren't. And while we're talking about
naming restrictions, all your commands should be named in
all-lowercase with no underscores. Before calling a command, supybot
always converts the command name to lowercase and removes all dashes
and underscores. On the other hand, you now know an easy way to make
sure a method is never called (even if its arguments are (self, irc,
msg, args), however unlikely that may be). Just name it with an
underscore or an uppercase letter in it :))
You'll also note that the docstring is odd. The wonderful thing about
the supybot framework is that it's easy to write complete commands
with help and everything: the docstring *IS* the help! Given the
above docstring, this is what a supybot does:
<jemfinch> @help random
<angryman> jemfinch: (random takes no arguments) -- Returns the
next random number from the random number generator.
Now on to the actual body of the function:
irc.reply(str(self.rng.random()))
irc.reply takes one simple argument: a string. The string is the
reply to be sent. Don't worry about length restrictions or anything
-- if the string you want to send is too big for an IRC message (and
oftentimes that turns out to be the case :)) the Supybot framework
handles that entirely transparently to you. Do make sure, however,
that you give irc.reply a string. It doesn't take anything else
(sometimes even unicode fails!). That's why we have
"str(self.rng.random())" instead of simply "self.rng.random()" -- we
had to give irc.reply a string.
Anyway, now that we have an RNG, we have a need for seed! Of course,
Python gives us a good seed already (it uses the current time as a
seed if we don't give it one) but users might want to be able to
repeat "random" sequences, so letting them set the seed is a good
thing. So we'll add a seed command to give the RNG a specific seed:
def seed(self, irc, msg, args):
"""<seed>
Sets the seed of the random number generator. <seed> must be
an int or a long.
"""
seed = privmsgs.getArgs(args)
try:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error('<seed> must be a valid int or long.')
return
self.rng.seed(seed)
irc.replySuccess()
So this one's a bit more complicated. But it's still pretty simple.
The method name is "seed" so that'll be the command name. The
arguments are the same, the docstring is of the same form, so we don't
need to go over that again. The body of the function, however, is
significantly different.
privmsgs.getArgs is a function you're going to be seeing a lot of when
you write plugins for Supybot. What it does is basically give you the
right number of arguments for your comamnd. In this case, we want one
argument. But we might have been given any number of arguments by the
user. So privmsgs.getArgs joins them appropriately, leaving us with
one single "seed" argument (by default, it returns one argument as a
single value; more arguments are returned in a tuple/list). Yes, we
could've just said "seed = args[0]" and gotten the first argument, but
what if the user didn't pass us an argument at all? Then we've got to
catch the IndexError from args[0] and complain to the user about it.
privmsgs.getArgs, on the other hand, handles all that for us. If the
user didn't give us enough arguments, it'll reply with the help string
for the command, thus saving us the effort.
So we have the seed from privmsgs.getArgs. But it's a string. The
next three lines are pretty darn obvious: we're just converting the
string to a int of some sort. But if it's not, that's when we're
going to call irc.error. It has the same interface as we saw before
in irc.reply, but it makes sure to remind the user that an error has
been encountered (currently, that means it puts "Error: " at the
beginning of the message). After erroring, we return. It's important
to remember this return here; otherwise, we'll just keep going down
through the function and try to use this "seed" variable that never
got assigned. A good general rule of thumb is that any time you use
irc.error, you'll want to return immediately afterwards.
Then we set the seed -- that's a simple function on our rng object.
Assuming that succeeds (and doesn't raise an exception, which it
shouldn't, because we already read the documentation and know that it
should work) we reply to say that everything worked fine. That's what
conf.replySuccess says. By default, it has the very dry (and
appropriately robot-like) "The operation succeeded." but you're
perfectly welcome to customize it yourself -- conf.py was written to
be modified!
So that's a bit more complicated command. But we still haven't dealt
with multiple arguments. Let's do that next.
So these random numbers are useful, but they're not the kind of random
numbers we usually want in Real Life. In Real Life, we like to tell
someone to "pick a number between 1 and 10." So let's write a
function that does that. Of course, we won't hardcode the 1 or the 10
into the function, but we'll take them as arguments. First the
function:
def range(self, irc, msg, args):
"""<start> <end>
Returns a number between <start> and <end>, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, required=2)
try:
end = int(end)
start = int(start)
except ValueError:
irc.error('<start> and <end> must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(str(self.rng.randrange(start, end+1)))
Pretty simple. This is becoming old hat by now. The only new thing
here is the call to privmsgs.getArgs. We have to make sure, since we
want two values, to pass a keyword parameter "required" into
privmsgs.getArgs. Of course, privmsgs.getArgs handles all the
checking for missing arguments and whatnot so we don't have to.
The Random object we're using offers us a "sample" method that takes a
sequence and a number (we'll call it N) and returns a list of N items
taken randomly from the sequence. So I'll show you an example that
takes advantage of multiple arguments but doesn't use privmsgs.getArgs
(and thus has to handle its own errors if the number of arguments
isn't right). Here's the code:
def sample(self, irc, msg, args):
"""<number of items> [<text> ...]
Returns a sample of the <number of items> taken from the remaining
arguments. Obviously <number of items> must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error('<number of items> must be an integer.')
return
if n > len(args):
irc.error('<number of items> must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(utils.commaAndify(map(repr, sample)))
Most everything here is familiar. The difference between this and the
previous examples is that we're dealing with args directly, rather
than through getArgs. Since we already have the arguments in a list,
it doesn't make any sense to have privmsgs.getArgs smush them all
together into a big long string that we'll just have to re-split. But
we still want the nice error handling of privmsgs.getArgs. So what do
we do? We raise callbacks.ArgumentError! That's the secret juju that
privmsgs.getArgs is doing; now we're just doing it ourself. Someone
up our callchain knows how to handle it so a neat error message is
returned. So in this function, if .pop(0) fails, we weren't given
enough arguments and thus need to tell the user how to call us.
So we have the args, we have the number, we do a simple call to
random.sample and then we do this funky utils.commaAndify to it.
Yeah, so I was running low on useful names :) Anyway, what it does is
take a list of strings and return a string with them joined by a
comma, the last one being joined with a comma and "and". So the list
['foo', 'bar', 'baz'] becomes "foo, bar, and baz". It's pretty useful
for showing the user lists in a useful form. We map the strings with
repr() first just to surround them with quotes.
So we have one more example. Yes, I hear your groans, but it's
pedagogically useful :) This time we're going to write a command that
makes the bot roll a die. It'll take one argument (the number of
sides on the die) and will respond with the equivalent of "/me rolls a
__" where __ is the number the bot rolled. So here's the code:
def diceroll(self, irc, msg, args):
"""[<number of sides>]
Rolls a die with <number of sides> sides. The default number
of sides is 6.
"""
try:
n = privmsgs.getArgs(args, required=0, optional=1)
if not n:
n = 6
n = int(n)
except ValueError:
irc.error('Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.reply(s, action=True)
There's a lot of stuff you haven't seen before in there. The most
important, though, is the first thing you'll notice that's different:
the privmsg.getArgs call. Here we're offering a default argument in
case the user is too lazy to supply one (or just wants a nice,
standard six-sided die :)) privmsgs.getArgs supports that; we'll just
tell it that we don't *need* any arguments (via required=0) and that
we *might like* one argument (optional=1). If the user provides an
argument, we'll get it -- if they don't, we'll just get an empty
string. Hence the "if not n: n = 6", where we provide the default.
You'll also note that irc.reply was given a keyword argument here,
"action". This means that the reply is to be made as an action rather
than a normal reply.
So that's our plugin. 5 commands, each building in complexity. You
should now be able to write most anything you want to do in Supybot.
Except regexp-based plugins, but that's a story for another day (and
those aren't nearly as cool as these command-based callbacks anyway
:)). Now we need to flesh it out to make it a full-fledged plugin.
TODO: Describe the registry and how to write a proper plugin configure
function.
We've written our own plugin from scratch (well, from the boilerplate
that we got from scripts/newplugin.py :)) and survived! Now go write
more plugins for supybot, and send them to me so I can use them too :)

169
docs/FAQ Normal file
View File

@ -0,0 +1,169 @@
Q: Why does my bot not recognize me or tell me that I don't have the
"owner" capability?
A: Because you're not given it anything to recognize you from!
You'll need to identify with the bot ("help identify" to see how
that works) or add your hostmask to your user record ("help
addhostmask" to see how that works) for it to know that you're you.
You may wish to note that addhostmask can accept a password; rather
than identify, you can send the command "addhostmask myOwnerUser
[hostmask] myOwnerUserPassword" and the bot will add your current
hostmask to your owner user (of course, you should change
myOwnerUser and myOwnerUserPassword appropriately for your bot).
Q: How do I make my Supybot op my users?
A: First, you'll have to make sure that your users register with the
bot. They can do this with the "register" command. After they do
so, you'll want to add the #channel,op capability to their user.
Use the "channel addcapability" command to do this. After that,
your users should be able to use the "op" command to get ops.
If you want your users to be auto-opped when they join the channel,
you'll need to load the Enforcer plugin and turn its autoOp
configuration variable on. Use the "config" command to do so.
Here's an example of how to do these steps:
<jemfinch|lambda> I'm going to make an example session for giving
you auto-ops, for our FAQ.
<dunk1> ah ok ;]
<jemfinch|lambda> First, I need you to register with supybot, using
the "register" command (remember to send it in
private).
<dunk1> done
<jemfinch|lambda> what name are you registered under?
<dunk1> dunk1
<jemfinch|lambda> ok, cool.
<jemfinch|lambda> @channel addcapability dunk1 op
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> now use the "op" command to get ops.
<dunk1> @op
--- supybot gives channel operator status to dunk1
<dunk1> works!
<dunk1> ;]
<jemfinch|lambda> @load Enforcer
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> @config supybot.plugins.Enforcer.autoOp.#supybot On
<supybot> jemfinch|lambda: The operation succeeded.
<jemfinch|lambda> ok, now cycle the channel (part and then rejoin)
<-- dunk1 (dunker@freebsd.nl) has left #supybot
--> dunk1 (dunker@freebsd.nl) has joined #supybot
--- supybot gives channel operator status to dunk1
<jemfinch|lambda> cool, thanks :)
Q: Can users with the "admin" capability change configuration
variables?
A: Currently, no. Since this is the first release of Supybot that
uses the registry, we wanted to stay on the conservative side and
require the "owner" capability for changing all non-channel-related
configuration variables. Feel free to make your case to us as to
why a certain configuration variable should only require the
"admin" capability instead of the "owner" capability, and if we
agree with you, we'll change it for the next release.
Q: How do I make my Supybot connect to multiple servers?
A: You'll need to use the Relay plugin. As long as you don't call
the "relay join" command, it won't actually do any relaying between
channels (even if the bot is on the same channel on different
networks). In order to use the Relay plugin, you'll want to first
call the "relay start" command, followed by the "relay connect"
command. These commands are (unfortunately) not persistent at this
time, so you'll need to give them to the bot anytime you start it
up. We'll probably have this lack of persistence rectified before
the next release.
Q: Can Supybot do factoids?
A: Supybot most certainly can! In fact, we offer two full-fledged
factoids-related plugins!
Factoids (written by jemfinch) is Supybot's original
factoids-related plugin. It offers full integration with Supybot's
nested commands as well as a complete 1:n key to factoid ratio,
with lookup by individual number. Factoids also uses a
channel-specific database instead of a global database, although in
the future it will likely be a configuration option whether to use
channel-specific or global databases for such plugins.
MoobotFactoids (written by Strike) is much more full-featured,
offering users the ability to define factoids in a slightly more
user-friendly way, as well as parsing factoids to handle <reply>,
<action>, "see", and altnerations (defining a factoid "test" as
"<reply>(foo|bar|baz)" will make the bot send "foo" or "bar" or
"baz" to the channel (without the normal "test is " at the
beginning)). If you're accustomed to Moobot's factoids or
Blootbot's factoids, then this is the Factoids plugin for you.
Unfortunately, due to the more natural definition syntax (required
to be compatible with Moobot) you can't define Factoids with nested
commands; you'll have to evaluate the command first and then copy
the result into your factoid definition. MoobotFactoids uses a
global database, so the factoids are the same for all channels.
In the future, we plan to have a compatibility plugin for Infobot,
but as of present we've not yet written one.
Q: Can I import my Infobot/Blootbot/Moobot factoids into Supybot?
A: As of present, we have no automated way to do so. Strike has
written a few scripts for importing a Moobot database into
MoobotFactoids, however, so you'll want to talk to him about
helping you with that. We're certainly happy to help you convert
such databases; if you can provide us with such a database exported
to a flat file, we can probably do the rest of the work to write a
script that imports it into a database for one of our
factoids-related plugins.
Q: I found a bug, what do I do?
A: Submit it on Sourceforge through our Sourceforge project page:
<http://sourceforge.net/tracker/?group_id=58965&atid=489447>. If
Sourceforge happens to be down when you try to submit your bug,
then post it in the "Supybot Developer Discussion" forum at our
forums at <http://forums.supybot.org/>. If that doesn't work,
email supybot-bugs@lists.sourceforge.net. If that doesn't work,
email jemfinch@supybot.org. If that doesn't work, find yourself
some carrier pigeons and ... hah! You thought I was serious!
Anyway, when you submit your bug, we'll need several things. If
the bug involved an uncaught exception, we need the traceback
(basically the stuff from "Uncaught exception in ..." to the next
log entry). We'd also like to see the commands that caused the
bug, or happened around the time you saw the bug. If the bug
involved a database, we'd love to see the database. Remember, it's
always worse to send us too little information in a bug report than
too much.
Q: Karma doesn't seem to work for me.
A: Karma by default doesn't acknowledge karma updates. If you check
the karma of whatever you increased/decreased, you'll note that
your increment or decrement still took place. If you'd rather
Karma acknowledge karma updates, change the
supybot.plugins.Karma.response configuration variable to On.
Q: I added an alias, but it doesn't work!
A: Take a look at "help <alias you added>". If the alias the bot has
listed doesn't match what you're giving it, chances are you need
to quote your alias in order for the brackets not to be
evaluated. For instance, if you're adding an alias to give you a
link to your homepage, you need to say:
alias add mylink "strconcat http://my.host.com/ [urlquote $1]"
and not:
alias add mylink strconcat http://my.host.com/ [urlquote $1]
The first version works; the second version will always return the
same url.

173
docs/GETTING_STARTED Normal file
View File

@ -0,0 +1,173 @@
Ok, so you've decided to try out Supybot. That's great! The more
people who use Supybot, the more people can submit bugs and help us to
make it the best IRC bot in the world :)
First things first: Supybot *requires* Python 2.3. There ain't no
getting around it. 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 what do you do? First thing you'll want to do is run (with
root/admin privileges) "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; C:\Python23\Scripts is a common place on Windows; and
(watch out, this is a long one :))
/System/Library/Frameworks/Python.framework/Versions/2.3/bin is a
common place on MacOS X.). 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 configuration files for your bot. We'd prefer
you to the use supybot-wizard, but if you're in a hurry or don't feel
like being asked many questions, just run supybot with no arguments
and it'll ask you only the questions necessary to run a bot.
So after running either of those two programs, 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.
Ok, so let's assume your bot connected to the server fine and joined
the channels you told it to join. For now we'll assume you named your
bot "supybot" (you probably didn't, but it'll make it much clearer in
the examples that follow to assume that you did). We'll also assume
that you told it to join #channel (a nice generic name for a channel,
isn't it? :)) So what do you do with this bot that you just made to
join your channel? Try this in the channel:
supybot: list
Replacing "supybot" with the actual name you picked for your bot, of
course. Your bot should reply with a list of the plugins he currently
has loaded. At least Admin, Channel, Config, Misc, Owner, and User
should be there; if you used supybot-wizard to create your
configuration file you may have many more plugins loaded. The list
command can also be used to list the commands in a given plugin:
supybot: list Misc
Will list all the commands in the Misc plugin. If you want to see the
help for any command, just use the help command:
supybot: help help
supybot: help list
supybot: help load
Sometimes more than one plugin will have a given command; for
instance, the "list" command exists in both the Misc and Config
plugins (both loaded by default). List, in this case, defaults to the
Misc plugin, but you may want to get the help for the list command in
the Config plugin. In that case, you'll want to give your command
like this:
supybot: help config list
Anytime your bot tells you that a given command is defined in several
plugins, you'll want to use this syntax ("plugin command") to
disambiguate which plugin's command you wish to call. For instance,
if you wanted to call the Config plugin's list command, then you'd
need to say:
supybot: config list
Rather than just "list".
Now that you know how to deal with plugins having commands with the
same name, let's take a look at loading other plugins. If you didn't
use supybot-wizard, though, you might do well to try it before playing
around with loading plugins yourself: each plugin has its own
configure function that the wizard uses to setup the appropriate
registry entries if the plugin requires any.
Now, if you do want to play around with loading plugins, you're going
to need to have the owner capability. If you ran the wizard, then
chances are you already added an owner user for yourself. If not,
however, you can add one via the handy-dandy supybot-adduser script.
You'll want to run it while the bot is not running (otherwise it could
overwrite supybot-adduser's changes to your user database before you
get a chance to reload them). Just follow the prompts, and when it
asks if you want to give the user any capabilities, say yes and then
give yourself the "owner" capability (without the quotes), restart the
bot and you'll be ready to load some plugins!
Now, in order for the bot to recognize you as your owner user, you'll
have to identify with the bot. Open up a query window in your irc
client (/query should do it; if not, just know that you can't identify
in a channel because it requires sending your password to the bot).
Then type this:
help identify
And follow the instructions; the command you send will probably look
like this, with your owner user and password replaced:
identify myowneruser myuserpassword
The bot will tell you that "The operation succeeded" if you got the
right name and password. Now that you're identified, you can do
anything that requires any privilege: that includes all the commands
in the Owner and Admin plugins, which you may want to take a look at
(using the list and help commands, of course). One command in
particular that you might want to use (it's from the User plugin) is
the addhostmask command: it lets you add a hostmask to your user
record so the bot recognizes you by your hostmask instead of requiring
you to always identify with it before it recognizes you. Use the help
command to see how this command works. Here's how I often use it:
addhostmask myuser [hostmask] mypassword
You may not have seen that "[hostmask]" syntax before. Supybot allows
nested commands, which means that any command's output can be nested
as an argument to another command. The hostmask command from the Misc
plugin returns the hostmask of a given nick, but if given no
arguments, it returns the hostmask of the person giving the command.
So the command above adds the hostmask I'm currently using to my
user's list of recognized hostmasks. I'm only required to give
mypassword if I'm not already identified with the bot.
Another command you might find yourself needing somewhat often is the
"more" command. The IRC protocol limits messages to 512 bytes, 60 or
so of which must be devoted to some bookkeeping. Sometimes, however,
Supybot wants to send a message that's longer than that. What it
does, then, is break it into "chunks" and send the first one,
following it with "(X more messages)" where X is how many more chunks
there are. To get to these chunks, use the more command. One way to
try is to look at the listing of configuration groups for the bot
(more on this in the CONFIGURATION document) by giving the command
"config list supybot". Last I checked, it'll overflow into a second
chunk. When you invoke this command, you should see output like:
<supybot> nick, ident, user, server, password, channels, prefixChars,
defaultCapabilities, defaultAllow, defaultIgnore,
humanTimestampFormat, externalIP, bracketSyntax, pipeSyntax,
followIdentificationThroughNickChanges, alwaysJoinOnInvite,
showSimpleSyntax, maxHistoryLength, nickmods, throttleTime,
snarfThrottle, threadAllCommands, pingServer, pingInterval,
upkeepInterval, flush, (1 more message)
Now, to see the rest of the output, simply give the command "more",
and it will show you the rest:
<jemfinch> more
<supybot> httpPeekSize, and defaultSocketTimeout
You should now have a solid foundation for using Supybot. Be sure to
check the help that is built-in to the bot itself if you have any
questions, and enjoy using Supybot!
###
# Addenda
###
Local installs: See this forum post: http://tinyurl.com/2tb37

259
docs/INTERFACES Normal file
View File

@ -0,0 +1,259 @@
These are the interfaces for some of the objects you'll deal with if
you code for Supybot.
ircmsgs.IrcMsg:
This is the object that represents an IRC message. It has
several methods and attributes. The most important thing
about this class, however, is that it *is* hashable, and thus
*cannot* be modified. Do not change any attributes; any code
that modifies an IRC message is *broken* and should not
exist.
Interesting Methods:
__init__: One of the more complex initializers in
a class. It can be used in three different ways:
1) It can be given a string, as one received from
the server, which it will then parse into its
separate components and instantiate the class
with those components as attributes.
2) It can be given a command, some (optional)
arguments, and a (optional) prefix, and will
instantiate the class with those components as
attributes.
3) It can be given, in addition to any of the
above arguments, a 'msg' keyword argument that
will use the attributes of msg as defaults.
This exists to make it easier to copy
messages, since the class is immutable.
__str__: This returns the message in a string form
suitable for sending to a server.
__repr__: This returns the message in a form
suitable for eval(), assuming the name "IrcMsg" is
in your namespace and is bound to this class.
Interesting Attributes:
This is the meat of this class. These are
generally what you'll be looking at with IrcMsgs.
command: This is the command of the IrcMsg --
PRIVMSG, NOTICE, WHOIS, etc.
args: This is a tuple of the arguments to the
IrcMsg. Some messages have arguments, some don't,
depending on what command they are. You are, of
course, always assured that args exists and is a
tuple, though it might be empty.
prefix: This is the hostmask of the person/server
the message is from. In general, you won't be
setting this on your outgoing messages, but
incoming messages will always have one. This is
the whole hostmask; if the message was received
from a server, it'll be the server's hostmask; if
the message was received from a user, it'll be the
whole user hostmask. In that case, however, it's
also parsed out into the nick/user/host
attributes, which are probably more useful to
check for many purposes.
nick: If the message was sent by a user, this will
be the nick of the user. If it was sent by a
server, this will be the server's name (something
like calvino.freenode.net or similar).
user: If the message was sent by a user, this will
be the user string of the user -- what they put
into their IRC client for their "full name." If
it was sent by a server, it'll be the server's
name, again.
host: If the message was sent by a user, this will
be the host portion of their hostmask. If it was
sent by a server, it'll be the server's name (yet
again :))
irclib.Irc:
This is the object to handle everything about IRC except the
actual connection to the server itself. (*NOTE* that the
object actually received by commands in subclasses of
callbacks.Privmsg is an IrcObjectProxy, which is described
later. It augments the following interface with several
methods of its own to help plugin authors.)
Interesting Methods:
The two following messages (queueMsg and
sendMsg) are the methods by far most commonly
called by plugin authors. They're generally
the only methods you need to pay attention to
if you're writing plugins.
queueMsg: Queues a message for sending to the
server. The queue is generally FIFO, but it
does prioritize messages based on their command.
sendMsg: Queues a message for sending to the
server prior to any messages in the normal
queue. This is exactly a FIFO queue, no
reordering is done at all.
The following two methods are the most important
for people writing new IrcDrivers. Otherwise,
you really don't need to pay attention to them.
feedMsg: Feeds the Irc object a message for it
handle appropriately, as well as passing it on
to callbacks.
takeMsg: If the Irc object has a message it's
ready to send to the server, this will return
it. Otherwise, it will return None.
The next several methods are of far more marginal
utility. But someone may need them, so they're
documented here.
addCallback: Takes a callback to add to the list
of callbacks in the Irc object. See the
interface for IrcCallback for more information.
getCallback: Gets a callback by name, if it is
in the Irc object's list of callbacks. If it
it isn't, returns None.
removeCallback: Removes a callback by name.
Returns a list of the callbacks removed (since
it is technically possible to have multiple
callbacks with the same name. This list may
be empty.
__init__: Requires a nick. Optional arguments
include user and ident, which default to the
nick given, password, which defaults to the empty
password, and callbacks, a list of callbacks
(which defaults to nothing, an empty list).
reset: Resets the Irc object to its original
state, as well as sends a reset() to every
callbacks.
die: Kills the IRC object and all its callbacks.
Interesting attributes:
nick: The current nick of the bot.
prefix: The current prefix of the bot.
server: The current server the bot is connected to.
network: The current network name the bot is connected to.
afterConnect: False until the bot has received a
command sent after the connection is finished --
376, 377, or 422.
state: An IrcState object for this particular
connection. See the interface for the IrcState
object for more information.
irclib.IrcCallback:
Interesting Methods:
name: Returns the name of the callback. The
default implementation simply returns the name
of the class.
__call__: Called by the Irc object with itself
and the message whenever a message is fed to
the Irc object. Nothing is done with the return
value.
inFilter: Called by the Irc object with itself
and the message whenever a message is fed to
the Irc object. The return value should be an
IrcMsg object to be passed to the next callback
in the Irc's list of callbacks. If None is
returned, all processing stops. This gives
callbacks an oppurtunity to "filter" incoming
messages before general callbacks are given
them.
outFilter: Basically equivalent to inFilter,
except instead of being called on messages
as they enter the Irc object, it's called on
messages as they leave the Irc object.
die: Called when the parent Irc is told to
die. This gives callbacks an oppurtunity to
close open files, network connections, or
databases before they're deleted.
reset: Called when the parent Irc is told to
reset (which is generally when reconnecting
to the server). Most callbacks don't need
to define this.
Interesting attributes:
priority: Determines the priority of the
callback in the Irc object's list of
callbacks. Defaults to 99; the valid range
includes 0 through sys.maxint-1 (don't use
sys.maxint itself, that's reserved for the
Misc plugin). The lower the number, the
higher the priority. High priority
callbacks are called earlier in the
inFilter cycle, earlier in the __call__
cycle, and later in the outFilter cycle --
basically, they're given the first chances
on the way in and the last chances on the
way out.
callbacks.IrcObjectProxy:
IrcObjectProxy is a proxy for an irclib.Irc instance that
serves to provide a much fuller interface for handling
replies and errors as well as to handle the nesting of
commands. This is what you'll be dealing with almost all the
time when writing commands; when writing doCommand methods
(the kind you read about in the interface description of
irclib.IrcCallback) you'll be dealing with plain old
irclib.Irc objects.
Interesting methods:
reply: Called to reply to the current message
with a string that is to be the reply.
replySuccess, replyError: These reply with the
configured responses for success and generic
error, respectively. If an additional argument
is given, it's (intelligently) appended to the
generic message to be more specific.
error: Called to send an error reply to the
current message; not only does the response
indicate an error, but commands that error out
break the nested-command chain, which is
generally useful for not confusing the user :)
errorNoCapability: Like error, except it accepts
the capability that's missing and integrates it
into the configured error message for such
things. Also accepts an additional string for a
more descriptive message, if that's what you
want.
errorPossibleBug, errorNotRegistered,
errorNoUser, errorRequiresPrivacy: These methods
reply with the appropriate configured error
message for the conditions in their names; they
all take an additional arguments to be more
specific about the conditions they indicate, but
this argument is very rarely necessary.
getRealIrc: Returns the actual Irc object being
proxied for.

64
docs/OVERVIEW Normal file
View File

@ -0,0 +1,64 @@
So here's a general *programming* introduction to what the different
modules do and what services they provide. It is, however, only an
introduction. Read the modules themselves for a much more detailed
explanation :)
fix.py: Stuff that Python should (but doesn't) include by default.
cdb.py: A constant database library, translated from C (and my O'Caml
version) More information available at
http://cr.yp.to/cdb.html. Not currently used since we
switched to PySQLite.
ansi.py: Contains different ANSI color sequences.
Mostly used by the debug module.
conf.py: The configuration file for the bot -- it sets a lot of
variables that other modules check for when they have
questions about what they need to do.
world.py: Just a dropping off place for some globals that need to be
shared among all the modules. It's obviously not used *a
lot*, but some things seem to fit better here than anywhere
else.
template.py: A template used by setup.py to create customized runnable
Python scripts for individual bots.
privmsgs.py: Basic stuff relating to callbacks.Privmsg, the base class
for most plugins.
callbacks.py: A few basic callbacks providing significant
functionality. You'll likely be inheriting from Privmsg
quite a bit.
ircdb.py: The users and channels databases are here, as well as the
IrcUser and IrcChannel classes. Look here when you want to
restrict a command to only certain users.
irclib.py: Provides the most important class in the irclib, Irc. It
represents a connection to an IRC server, but it's entirely
separate from the network implementation. It's connected
to the network (or whatever else drives it) by a "driver"
module which uses its feedMsg/takeMsg functions.
drivers.py: The baseclass (IrcDriver) for various drivers to drive
irclib.Irc classes. Also, the driving mechanism.
asyncoreDrivers.py: The asyncore-based drivers for use with the bot.
socketDrivers.py: The plain old socket-based drivers for use with the
bot.
twistedDrivers.py: The Twisted <http://www.twistedmatrix.com/> drivers
for use with the bot.
ircmsgs.py: The IrcMsg class (get to know it :)) and various functions
for making the creation of IrcMsgs easier.
ircutils.py: Various utility functions for Irc -- read the module to
see what goodies are there :)
schedule.py: A schedule driver (which is automatically registered with
the drivers module) to run things at a particular time,
or at specified periods of time.

86
docs/STYLE Normal file
View File

@ -0,0 +1,86 @@
Read PEP 8 (Guido's Style Guide) and know that we use almost all the
same style guidelines.
Maximum line length is 79 characters. 78 is a safer bet, though.
Identation is 4 spaces per level. No tabs.
Single quotes are used for all string literals that aren't docstrings.
They're just easier to type.
Triple double quotes (""") are always used for docstrings.
Spaces go around all operators (except around '=' in default arguments
to functions) and after all commas (unless doing so keeps a line
within the 79 character limit).
Class names are StudlyCaps. Method and function names are camelCaps
(StudlyCaps with an initial lowercase letter). If variable and
attribute names can maintain readability without being camelCaps, then
they should be entirely in lowercase, otherwise they should also use
camelCaps. Plugin names are StudlyCaps.
Imports should always happen at the top of the module, one import per
line (so if imports need to be added or removed later, it can be done
easily).
A blank line should be between all consecutive method declarations in
a class definition. Two blank lines should be between all consecutive
class definitions in a file. Comments are even better than blank
lines for separating classes.
Database filenames should generally begin with the name of the plugin
and the extension should be 'db'. baseplugin.DBHandler does this
already.
Whenever creating a file descriptor or socket, keep a reference around
and be sure to close it. There should be no code like this:
s = urllib2.urlopen('url').read()
Instead, do this:
fd = urllib2.urlopen('url')
s = fd.read()
fd.close()
This is to be sure the bot doesn't leak file descriptors.
All plugins should include a docstring decsribing what the plugin
does. This docstring will be returned when the user wants help on a
plugin.
Method docstrings in classes deriving from callbacks.Privmsg should
include an argument list as their first line, and after that a blank
line followed by a longer description of what the command does. The
argument list is used by the 'syntax' command, and the longer
description is used by the 'help' command.
Whenever joining more than two strings, use string interpolation, not
addition:
s = x + y + z # Bad.
s = '%s%s%s' % (x, y, z) # Good.
s = ''.join([x, y, z]) # Best, but not as general.
This has to do with efficiency; the intermediate string x+y is made
(and thus copied) before x+y+z is made, so it's less efficient.
When writing strings that have formatting characters in them, don't
use anything but %s unless you absolutely must. In particular, %d
should never be used, it's less general than %s and serves no useful
purpose.
Use the log module to its fullest; when you need to print some values
to debug, use self.log.debug to do so, and leave those print
statements in the code (commented out) so they can later be
re-enabled. Remember that once code is buggy, it tends to have more
bugs, and you'll probably need those print statements again.
SQL table names should be all-lowercase and include underscores to
separate words. This is because SQL itself is case-insensitive.
SQL statements in code should put SQL words in ALL CAPS:
SELECT quote FROM quotes ORDER BY random() LIMIT 1
All plugins should have test cases written for them. Even if it
doesn't actually test anything but just exists, it's good to have the
test there so there's a place to add more tests later (and so we can
be sure that all plugins are adequately documented.
All uses of eval() that expect to get integrated in Supybot must be
approved by jemfinch, no exceptions.

154
examples/Random.py Normal file
View File

@ -0,0 +1,154 @@
#!/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.
###
"""
Lots of stuff relating to random numbers.
"""
import plugins
import random
import conf
import utils
import ircmsgs
import ircutils
import privmsgs
import registry
import callbacks
def configure(advanced):
from questions import expect, anything, something, yn
conf.registerPlugin('Random', True)
if yn('Do you want to specify a seed to be used for the RNG'):
seed = something('What seed? It must be an integer or long.')
while not seed.isdigit():
print 'That\'s not a valid seed.'
seed = something('What seed?')
conf.supybot.plugins.Random.seed.setValue(seed)
class Seed(registry.Value):
def set(self, s):
try:
self.setValue(long(s))
except ValueError:
raise registry.InvalidRegistryValue, 'Value must be an integer.'
conf.registerPlugin('Random')
conf.registerGlobalValue(conf.supybot.plugins.Random, 'seed', Seed(0, """
Sets the seed of the random number generator. The seen must be a valid
Python integer or long."""))
class Random(callbacks.Privmsg):
rng = random.Random()
def random(self, irc, msg, args):
"""takes no arguments
Returns the next random number from the random number
generator.
"""
irc.reply(str(self.rng.random()))
def seed(self, irc, msg, args):
"""<seed>
Sets the seed of the random number generator. <seed> must be an int
or a long.
"""
seed = privmsgs.getArgs(args)
try:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error('<seed> must be a valid int or long.')
return
self.rng.seed(seed)
irc.replySuccess()
def range(self, irc, msg, args):
"""<start> <end>
Returns a number between <start> and <end>, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, required=2)
try:
end = int(end)
start = int(start)
except ValueError:
irc.error('<start> and <end> must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(str(self.rng.randrange(start, end+1)))
def sample(self, irc, msg, args):
"""<number of items> [<text> ...]
Returns a sample of the <number of items> taken from the remaining
arguments. Obviously <number of items> must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error('<number of items> must be an integer.')
return
if n > len(args):
irc.error('<number of items> must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(utils.commaAndify(map(repr, sample)))
def diceroll(self, irc, msg, args):
"""[<number of sides>]
Rolls a die with <number of sides> sides. The default number of
sides is 6.
"""
try:
n = privmsgs.getArgs(args, required=0, optional=1)
if not n:
n = 6
n = int(n)
except ValueError:
irc.error('Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n)
irc.queueMsg(ircmsgs.action(ircutils.replyTo(msg), s))
raise callbacks.CannotNest
Class = Random
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,30 +0,0 @@
---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ site.title | xml_escape }}</title>
<description>{{ site.description | xml_escape }}</description>
<link>{{ site.url }}{{ site.baseurl }}/</link>
<atom:link href="{{ "/feed.xml" | prepend: site.baseurl | prepend: site.url }}" rel="self" type="application/rss+xml"/>
<pubDate>{{ site.time | date_to_rfc822 }}</pubDate>
<lastBuildDate>{{ site.time | date_to_rfc822 }}</lastBuildDate>
<generator>Jekyll v{{ jekyll.version }}</generator>
{% for post in site.posts limit:10 %}
<item>
<title>{{ post.title | xml_escape }}</title>
<description>{{ post.content | xml_escape }}</description>
<pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
<link>{{ post.url | prepend: site.baseurl | prepend: site.url }}</link>
<guid isPermaLink="true">{{ post.url | prepend: site.baseurl | prepend: site.url }}</guid>
{% for tag in post.tags %}
<category>{{ tag | xml_escape }}</category>
{% endfor %}
{% for cat in post.categories %}
<category>{{ cat | xml_escape }}</category>
{% endfor %}
</item>
{% endfor %}
</channel>
</rss>

View File

@ -1,26 +0,0 @@
---
layout: default
---
<!-- @format -->
**_WARNING: most of the content on this site originates from 2014!_**
Welcome to Mikaela's Supybot pages.
This site isn't official and won't help with most of issues. In case you are
looking for the official sites, they are here:
- [Limnoria's website](https://limnoria.net/)
- [Limnoria official documentation](https://docs.limnoria.net/)
- [Supybook](https://hoxu.github.io/supybook/devel/)
- [Gribble Wiki](https://sourceforge.net/p/gribble/wiki/Main_Page/)
I also have
[something in my gist repo](https://gitea.blesmrt.net/mikaela/gist/src/branch/master/irc/limnoria/)
at 2021-06-11 16:07 UTC
[opinionated titlefetching instructions](https://gitea.blesmrt.net/mikaela/gist/src/branch/master/irc/limnoria/titlefetching.md).
If you cannot find what you are looking for from them, please come to IRC and
ask. The Support channels are
[#supybot,#limnoria on irc.libera.chat](ircs://irc.libera.chat:6697/%23supybot%2c%23limnoria)

3978
others/SOAP.py Normal file

File diff suppressed because it is too large Load Diff

0
others/__init__.py Normal file
View File

301
others/amazon.py Normal file
View File

@ -0,0 +1,301 @@
"""Python wrapper
for Amazon web APIs
This module allows you to access Amazon's web APIs,
to do things like search Amazon and get the results programmatically.
Described here:
http://www.amazon.com/webservices
You need a Amazon-provided license key to use these services.
Follow the link above to get one. These functions will look in
several places (in this order) for the license key:
- the "license_key" argument of each function
- the module-level LICENSE_KEY variable (call setLicense once to set it)
- an environment variable called AMAZON_LICENSE_KEY
- a file called ".amazonkey" in the current directory
- a file called "amazonkey.txt" in the current directory
- a file called ".amazonkey" in your home directory
- a file called "amazonkey.txt" in your home directory
- a file called ".amazonkey" in the same directory as amazon.py
- a file called "amazonkey.txt" in the same directory as amazon.py
Sample usage:
>>> import amazon
>>> amazon.setLicense('...') # must get your own key!
>>> pythonBooks = amazon.searchByKeyword('Python')
>>> pythonBooks[0].ProductName
u'Learning Python (Help for Programmers)'
>>> pythonBooks[0].URL
...
>>> pythonBooks[0].OurPrice
...
Other available functions:
- browseBestSellers
- searchByASIN
- searchByUPC
- searchByAuthor
- searchByArtist
- searchByActor
- searchByDirector
- searchByManufacturer
- searchByListMania
- searchSimilar
- searchByWishlist
Other usage notes:
- Most functions can take product_line as well, see source for possible values
- All functions can take type="lite" to get less detail in results
- All functions can take page=N to get second, third, fourth page of results
- All functions can take license_key="XYZ", instead of setting it globally
- All functions can take http_proxy="http://x/y/z" which overrides your system setting
"""
__author__ = "Mark Pilgrim (f8dy@diveintomark.org)"
__version__ = "0.61"
__cvsversion__ = "$Revision$"[11:-2]
__date__ = "$Date$"[7:-2]
__copyright__ = "Copyright (c) 2002 Mark Pilgrim"
__license__ = "Python"
# Powersearch and return object type fix by Joseph Reagle <geek@goatee.net>
from xml.dom import minidom
import os, sys, getopt, cgi, urllib
try:
import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
timeoutsocket.setDefaultSocketTimeout(10)
except ImportError:
pass
LICENSE_KEY = None
HTTP_PROXY = None
# don't touch the rest of these constants
class AmazonError(Exception): pass
class NoLicenseKey(Exception): pass
_amazonfile1 = ".amazonkey"
_amazonfile2 = "amazonkey.txt"
_licenseLocations = (
(lambda key: key, 'passed to the function in license_key variable'),
(lambda key: LICENSE_KEY, 'module-level LICENSE_KEY variable (call setLicense to set it)'),
(lambda key: os.environ.get('AMAZON_LICENSE_KEY', None), 'an environment variable called AMAZON_LICENSE_KEY'),
(lambda key: _contentsOf(os.getcwd(), _amazonfile1), '%s in the current directory' % _amazonfile1),
(lambda key: _contentsOf(os.getcwd(), _amazonfile2), '%s in the current directory' % _amazonfile2),
(lambda key: _contentsOf(os.environ.get('HOME', ''), _amazonfile1), '%s in your home directory' % _amazonfile1),
(lambda key: _contentsOf(os.environ.get('HOME', ''), _amazonfile2), '%s in your home directory' % _amazonfile2),
(lambda key: _contentsOf(_getScriptDir(), _amazonfile1), '%s in the amazon.py directory' % _amazonfile1),
(lambda key: _contentsOf(_getScriptDir(), _amazonfile2), '%s in the amazon.py directory' % _amazonfile2)
)
## administrative functions
def version():
print """PyAmazon %(__version__)s
%(__copyright__)s
released %(__date__)s
""" % globals()
## utility functions
def setLicense(license_key):
"""set license key"""
global LICENSE_KEY
LICENSE_KEY = license_key
def getLicense(license_key = None):
"""get license key
license key can come from any number of locations;
see module docs for search order"""
for get, location in _licenseLocations:
rc = get(license_key)
if rc: return rc
raise NoLicenseKey, 'get a license key at http://www.amazon.com/webservices'
def setProxy(http_proxy):
"""set HTTP proxy"""
global HTTP_PROXY
HTTP_PROXY = http_proxy
def getProxy(http_proxy = None):
"""get HTTP proxy"""
return http_proxy or HTTP_PROXY
def getProxies(http_proxy = None):
http_proxy = getProxy(http_proxy)
if http_proxy:
proxies = {"http": http_proxy}
else:
proxies = None
return proxies
def _contentsOf(dirname, filename):
filename = os.path.join(dirname, filename)
if not os.path.exists(filename): return None
fsock = open(filename)
contents = fsock.read()
fsock.close()
return contents
def _getScriptDir():
if __name__ == '__main__':
return os.path.abspath(os.path.dirname(sys.argv[0]))
else:
return os.path.abspath(os.path.dirname(sys.modules[__name__].__file__))
class Bag: pass
def unmarshal(element):
rc = Bag()
if isinstance(element, minidom.Element) and (element.tagName == 'Details'):
rc.URL = element.attributes["url"].value
childElements = [e for e in element.childNodes if isinstance(e, minidom.Element)]
if childElements:
for child in childElements:
key = child.tagName
if hasattr(rc, key):
if type(getattr(rc, key)) <> type([]):
setattr(rc, key, [getattr(rc, key)])
setattr(rc, key, getattr(rc, key) + [unmarshal(child)])
elif isinstance(child, minidom.Element) and (child.tagName == 'Details'):
# make the first Details element a key
setattr(rc,key,[unmarshal(child)])
#dbg: because otherwise 'hasattr' only tests
#dbg: on the second occurence: if there's a
#dbg: single return to a query, it's not a
#dbg: list. This module should always
#dbg: return a list of Details objects.
else:
setattr(rc, key, unmarshal(child))
else:
rc = "".join([e.data for e in element.childNodes if isinstance(e, minidom.Text)])
if element.tagName == 'SalesRank':
rc = int(rc.replace(',', ''))
return rc
def buildURL(search_type, keyword, product_line, type, page, license_key):
url = "http://xml.amazon.com/onca/xml?v=1.0&f=xml&t=webservices-20"
url += "&dev-t=%s" % license_key.strip()
url += "&type=%s" % type
if page:
url += "&page=%s" % page
if product_line:
url += "&mode=%s" % product_line
url += "&%s=%s" % (search_type, urllib.quote(keyword))
return url
## main functions
def search(search_type, keyword, product_line, type="heavy", page=None,
license_key = None, http_proxy = None):
"""search Amazon
You need a license key to call this function; see
http://www.amazon.com/webservices
to get one. Then you can either pass it to
this function every time, or set it globally; see the module docs for details.
Parameters:
keyword - keyword to search
search_type - in (KeywordSearch, BrowseNodeSearch, AsinSearch, UpcSearch, AuthorSearch, ArtistSearch, ActorSearch, DirectorSearch, ManufacturerSearch, ListManiaSearch, SimilaritySearch)
product_line - type of product to search for. restrictions based on search_type
UpcSearch - in (music, classical)
AuthorSearch - must be "books"
ArtistSearch - in (music, classical)
ActorSearch - in (dvd, vhs, video)
DirectorSearch - in (dvd, vhs, video)
ManufacturerSearch - in (electronics, kitchen, videogames, software, photo, pc-hardware)
http_proxy (optional) - address of HTTP proxy to use for sending and receiving SOAP messages
Returns: list of Bags, each Bag may contain the following attributes:
Asin - Amazon ID ("ASIN" number) of this item
Authors - list of authors
Availability - "available", etc.
BrowseList - list of related categories
Catalog - catalog type ("Book", etc)
CollectiblePrice - ?, format "$34.95"
ImageUrlLarge - URL of large image of this item
ImageUrlMedium - URL of medium image of this item
ImageUrlSmall - URL of small image of this item
Isbn - ISBN number
ListPrice - list price, format "$34.95"
Lists - list of ListMania lists that include this item
Manufacturer - manufacturer
Media - media ("Paperback", "Audio CD", etc)
NumMedia - number of different media types in which this item is available
OurPrice - Amazon price, format "$24.47"
ProductName - name of this item
ReleaseDate - release date, format "09 April, 1999"
Reviews - reviews (AvgCustomerRating, plus list of CustomerReview with Rating, Summary, Content)
SalesRank - sales rank (integer)
SimilarProducts - list of Product, which is ASIN number
ThirdPartyNewPrice - ?, format "$34.95"
URL - URL of this item
"""
license_key = getLicense(license_key)
url = buildURL(search_type, keyword, product_line, type, page, license_key)
proxies = getProxies(http_proxy)
u = urllib.FancyURLopener(proxies)
usock = u.open(url)
xmldoc = minidom.parse(usock)
# from xml.dom.ext import PrettyPrint
# PrettyPrint(xmldoc)
usock.close()
data = unmarshal(xmldoc).ProductInfo
if hasattr(data, 'ErrorMsg'):
raise AmazonError, data.ErrorMsg
else:
return data.Details
def searchByKeyword(keyword, product_line="books", type="heavy", page=1, license_key=None, http_proxy=None):
return search("KeywordSearch", keyword, product_line, type, page, license_key, http_proxy)
def browseBestSellers(browse_node, product_line="books", type="heavy", page=1, license_key=None, http_proxy=None):
return search("BrowseNodeSearch", browse_node, product_line, type, page, license_key, http_proxy)
def searchByASIN(ASIN, type="heavy", license_key=None, http_proxy=None):
return search("AsinSearch", ASIN, None, type, None, license_key, http_proxy)
def searchByUPC(UPC, type="heavy", license_key=None, http_proxy=None):
return search("UpcSearch", UPC, None, type, None, license_key, http_proxy)
def searchByAuthor(author, type="heavy", page=1, license_key=None, http_proxy=None):
return search("AuthorSearch", author, "books", type, page, license_key, http_proxy)
def searchByArtist(artist, product_line="music", type="heavy", page=1, license_key=None, http_proxy=None):
if product_line not in ("music", "classical"):
raise AmazonError, "product_line must be in ('music', 'classical')"
return search("ArtistSearch", artist, product_line, type, page, license_key, http_proxy)
def searchByActor(actor, product_line="dvd", type="heavy", page=1, license_key=None, http_proxy=None):
if product_line not in ("dvd", "vhs", "video"):
raise AmazonError, "product_line must be in ('dvd', 'vhs', 'video')"
return search("ActorSearch", actor, product_line, type, page, license_key, http_proxy)
def searchByDirector(director, product_line="dvd", type="heavy", page=1, license_key=None, http_proxy=None):
if product_line not in ("dvd", "vhs", "video"):
raise AmazonError, "product_line must be in ('dvd', 'vhs', 'video')"
return search("DirectorSearch", director, product_line, type, page, license_key, http_proxy)
def searchByManufacturer(manufacturer, product_line="pc-hardware", type="heavy", page=1, license_key=None, http_proxy=None):
if product_line not in ("electronics", "kitchen", "videogames", "software", "photo", "pc-hardware"):
raise AmazonError, "product_line must be in ('electronics', 'kitchen', 'videogames', 'software', 'photo', 'pc-hardware')"
return search("ManufacturerSearch", manufacturer, product_line, type, page, license_key, http_proxy)
def searchByListMania(listManiaID, type="heavy", page=1, license_key=None, http_proxy=None):
return search("ListManiaSearch", listManiaID, None, type, page, license_key, http_proxy)
def searchSimilar(ASIN, type="heavy", page=1, license_key=None, http_proxy=None):
return search("SimilaritySearch", ASIN, None, type, page, license_key, http_proxy)
def searchByWishlist(wishlistID, type="heavy", page=1, license_key=None, http_proxy=None):
return search("WishlistSearch", wishlistID, None, type, page, license_key, http_proxy)
def searchByPower(keyword, product_line="books", type="heavy", page=1, license_key=None, http_proxy=None):
return search("PowerSearch", keyword, product_line, type, page, license_key, http_proxy)
# >>> RecentKing = amazon.searchByPower('author:Stephen King and pubdate:2003')
# >>> SnowCrash = amazon.searchByPower('title:Snow Crash')

294
others/asynchat.py Normal file
View File

@ -0,0 +1,294 @@
# -*- Mode: Python; tab-width: 4 -*-
# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing <rushing@nightmare.com>
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
r"""A class supporting chat-style (command/response) protocols.
This class adds support for 'chat' style protocols - where one side
sends a 'command', and the other sends a response (examples would be
the common internet protocols - smtp, nntp, ftp, etc..).
The handle_read() method looks at the input stream for the current
'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n'
for multi-line output), calling self.found_terminator() on its
receipt.
for example:
Say you build an async nntp client using this class. At the start
of the connection, you'll have self.terminator set to '\r\n', in
order to process the single-line greeting. Just before issuing a
'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST
command will be accumulated (using your own 'collect_incoming_data'
method) up to the terminator, and then control will be returned to
you - by calling your self.found_terminator() method.
"""
import socket
import asyncore
class async_chat (asyncore.dispatcher):
"""This is an abstract class. You must derive from this class, and add
the two methods collect_incoming_data() and found_terminator()"""
# these are overridable defaults
ac_in_buffer_size = 4096
ac_out_buffer_size = 4096
def __init__ (self, conn=None):
self.ac_in_buffer = ''
self.ac_out_buffer = ''
self.producer_fifo = fifo()
asyncore.dispatcher.__init__ (self, conn)
def set_terminator (self, term):
"Set the input delimiter. Can be a fixed string of any length, an integer, or None"
self.terminator = term
def get_terminator (self):
return self.terminator
# grab some more data from the socket,
# throw it to the collector method,
# check for the terminator,
# if found, transition to the next state.
def handle_read (self):
try:
data = self.recv (self.ac_in_buffer_size)
except socket.error, _:
self.handle_error()
return
self.ac_in_buffer = self.ac_in_buffer + data
# Continue to search for self.terminator in self.ac_in_buffer,
# while calling self.collect_incoming_data. The while loop
# is necessary because we might read several data+terminator
# combos with a single recv(1024).
while self.ac_in_buffer:
lb = len(self.ac_in_buffer)
terminator = self.get_terminator()
if terminator is None:
# no terminator, collect it all
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = ''
elif type(terminator) == type(0):
# numeric terminator
n = terminator
if lb < n:
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = ''
self.terminator = self.terminator - lb
else:
self.collect_incoming_data (self.ac_in_buffer[:n])
self.ac_in_buffer = self.ac_in_buffer[n:]
self.terminator = 0
self.found_terminator()
else:
# 3 cases:
# 1) end of buffer matches terminator exactly:
# collect data, transition
# 2) end of buffer matches some prefix:
# collect data to the prefix
# 3) end of buffer does not match any prefix:
# collect data
terminator_len = len(terminator)
index = self.ac_in_buffer.find(terminator)
if index != -1:
# we found the terminator
if index > 0:
# don't bother reporting the empty string (source of subtle bugs)
self.collect_incoming_data (self.ac_in_buffer[:index])
self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
# This does the Right Thing if the terminator is changed here.
self.found_terminator()
else:
# check for a prefix of the terminator
index = find_prefix_at_end (self.ac_in_buffer, terminator)
if index:
if index != lb:
# we found a prefix, collect up to the prefix
self.collect_incoming_data (self.ac_in_buffer[:-index])
self.ac_in_buffer = self.ac_in_buffer[-index:]
break
else:
# no prefix, collect it all
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = ''
def handle_write (self):
self.initiate_send ()
def handle_close (self):
self.close()
def push (self, data):
self.producer_fifo.push (simple_producer (data))
self.initiate_send()
def push_with_producer (self, producer):
self.producer_fifo.push (producer)
self.initiate_send()
def readable (self):
"predicate for inclusion in the readable for select()"
return (len(self.ac_in_buffer) <= self.ac_in_buffer_size)
def writable (self):
"predicate for inclusion in the writable for select()"
# return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected)
# this is about twice as fast, though not as clear.
return not (
(self.ac_out_buffer == '') and
self.producer_fifo.is_empty() and
self.connected
)
def close_when_done (self):
"automatically close this channel once the outgoing queue is empty"
self.producer_fifo.push (None)
# refill the outgoing buffer by calling the more() method
# of the first producer in the queue
def refill_buffer (self):
_string_type = type('')
while 1:
if len(self.producer_fifo):
p = self.producer_fifo.first()
# a 'None' in the producer fifo is a sentinel,
# telling us to close the channel.
if p is None:
if not self.ac_out_buffer:
self.producer_fifo.pop()
self.close()
return
elif type(p) is _string_type:
self.producer_fifo.pop()
self.ac_out_buffer = self.ac_out_buffer + p
return
data = p.more()
if data:
self.ac_out_buffer = self.ac_out_buffer + data
return
else:
self.producer_fifo.pop()
else:
return
def initiate_send (self):
obs = self.ac_out_buffer_size
# try to refill the buffer
if (len (self.ac_out_buffer) < obs):
self.refill_buffer()
if self.ac_out_buffer and self.connected:
# try to send the buffer
try:
num_sent = self.send (self.ac_out_buffer[:obs])
if num_sent:
self.ac_out_buffer = self.ac_out_buffer[num_sent:]
except socket.error, _:
self.handle_error()
return
def discard_buffers (self):
# Emergencies only!
self.ac_in_buffer = ''
self.ac_out_buffer = ''
while self.producer_fifo:
self.producer_fifo.pop()
class simple_producer:
def __init__ (self, data, buffer_size=512):
self.data = data
self.buffer_size = buffer_size
def more (self):
if len (self.data) > self.buffer_size:
result = self.data[:self.buffer_size]
self.data = self.data[self.buffer_size:]
return result
else:
result = self.data
self.data = ''
return result
class fifo:
def __init__ (self, list=None):
if not list:
self.list = []
else:
self.list = list
def __len__ (self):
return len(self.list)
def is_empty (self):
return self.list == []
def first (self):
return self.list[0]
def push (self, data):
self.list.append (data)
def pop (self):
if self.list:
result = self.list[0]
del self.list[0]
return (1, result)
else:
return (0, None)
# Given 'haystack', see if any prefix of 'needle' is at its end. This
# assumes an exact match has already been checked. Return the number of
# characters matched.
# for example:
# f_p_a_e ("qwerty\r", "\r\n") => 1
# f_p_a_e ("qwerty\r\n", "\r\n") => 2
# f_p_a_e ("qwertydkjf", "\r\n") => 0
# this could maybe be made faster with a computed regex?
# [answer: no; circa Python-2.0, Jan 2001]
# python: 18307/s
# re: 12820/s
# regex: 14035/s
def find_prefix_at_end (haystack, needle):
nl = len(needle)
result = 0
for i in range (1,nl):
if haystack[-(nl-i):] == needle[:(nl-i)]:
result = nl-i
break
return result
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

494
others/asyncore.py Normal file
View File

@ -0,0 +1,494 @@
# -*- Mode: Python -*-
# Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing <rushing@nightmare.com>
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
"""Basic infrastructure for asynchronous socket service clients and servers.
There are only two ways to have a program on a single processor do "more
than one thing at a time". Multi-threaded programming is the simplest and
most popular way to do it, but there is another very different technique,
that lets you have nearly all the advantages of multi-threading, without
actually using multiple threads. it's really only practical if your program
is largely I/O bound. If your program is CPU bound, then pre-emptive
scheduled threads are probably what you really need. Network servers are
rarely CPU-bound, however.
If your operating system supports the select() system call in its I/O
library (and nearly all do), then you can use it to juggle multiple
communication channels at once; doing other work while your I/O is taking
place in the "background." Although this strategy can seem strange and
complex, especially at first, it is in many ways easier to understand and
control than multi-threaded programming. The module documented here solves
many of the difficult problems for you, making the task of building
sophisticated high-performance network servers and clients a snap.
"""
import exceptions
import select
import socket
import sys
import time
import os
from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, \
ENOTCONN, ESHUTDOWN, EINTR, EISCONN
try:
socket_map
except NameError:
socket_map = {}
class ExitNow(exceptions.Exception):
pass
def read(obj):
try:
obj.handle_read_event()
except ExitNow:
raise
except:
obj.handle_error()
def write(obj):
try:
obj.handle_write_event()
except ExitNow:
raise
except:
obj.handle_error()
def readwrite(obj, flags):
try:
if flags & select.POLLIN:
obj.handle_read_event()
if flags & select.POLLOUT:
obj.handle_write_event()
except ExitNow:
raise
except:
obj.handle_error()
def poll(timeout=0.0, map=None):
if map is None:
map = socket_map
if map:
r = []; w = []; e = []
for fd, obj in map.items():
if obj.readable():
r.append(fd)
if obj.writable():
w.append(fd)
if [] == r == w == e:
time.sleep(timeout)
else:
try:
r, w, e = select.select(r, w, e, timeout)
except select.error, err:
if err[0] != EINTR:
raise
else:
return
for fd in r:
obj = map.get(fd)
if obj is None:
continue
read(obj)
for fd in w:
obj = map.get(fd)
if obj is None:
continue
write(obj)
def poll2(timeout=0.0, map=None):
import poll
if map is None:
map = socket_map
if timeout is not None:
# timeout is in milliseconds
timeout = int(timeout*1000)
if map:
l = []
for fd, obj in map.items():
flags = 0
if obj.readable():
flags = poll.POLLIN
if obj.writable():
flags = flags | poll.POLLOUT
if flags:
l.append((fd, flags))
r = poll.poll(l, timeout)
for fd, flags in r:
obj = map.get(fd)
if obj is None:
continue
readwrite(obj, flags)
def poll3(timeout=0.0, map=None):
# Use the poll() support added to the select module in Python 2.0
if map is None:
map = socket_map
if timeout is not None:
# timeout is in milliseconds
timeout = int(timeout*1000)
pollster = select.poll()
if map:
for fd, obj in map.items():
flags = 0
if obj.readable():
flags = select.POLLIN
if obj.writable():
flags = flags | select.POLLOUT
if flags:
pollster.register(fd, flags)
try:
r = pollster.poll(timeout)
except select.error, err:
if err[0] != EINTR:
raise
r = []
for fd, flags in r:
obj = map.get(fd)
if obj is None:
continue
readwrite(obj, flags)
def loop(timeout=30.0, use_poll=0, map=None):
if map is None:
map = socket_map
if use_poll:
if hasattr(select, 'poll'):
poll_fun = poll3
else:
poll_fun = poll2
else:
poll_fun = poll
while map:
poll_fun(timeout, map)
class dispatcher:
debug = 0
connected = 0
accepting = 0
closing = 0
addr = None
def __init__(self, sock=None, map=None):
if sock:
self.set_socket(sock, map)
# I think it should inherit this anyway
self.socket.setblocking(0)
self.connected = 1
# XXX Does the constructor require that the socket passed
# be connected?
try:
self.addr = sock.getpeername()
except socket.error:
# The addr isn't crucial
pass
else:
self.socket = None
def __repr__(self):
status = [self.__class__.__module__+"."+self.__class__.__name__]
if self.accepting and self.addr:
status.append('listening')
elif self.connected:
status.append('connected')
if self.addr is not None:
try:
status.append('%s:%d' % self.addr)
except TypeError:
status.append(repr(self.addr))
return '<%s at %#x>' % (' '.join(status), id(self))
def add_channel(self, map=None):
#self.log_info('adding channel %s' % self)
if map is None:
map = socket_map
map[self._fileno] = self
def del_channel(self, map=None):
fd = self._fileno
if map is None:
map = socket_map
if map.has_key(fd):
#self.log_info('closing channel %d:%s' % (fd, self))
del map[fd]
def create_socket(self, family, type):
self.family_and_type = family, type
self.socket = socket.socket(family, type)
self.socket.setblocking(0)
self._fileno = self.socket.fileno()
self.add_channel()
def set_socket(self, sock, map=None):
self.socket = sock
## self.__dict__['socket'] = sock
self._fileno = sock.fileno()
self.add_channel(map)
def set_reuse_addr(self):
# try to re-use a server port if possible
try:
self.socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR,
self.socket.getsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR) | 1
)
except socket.error:
pass
# ==================================================
# predicates for select()
# these are used as filters for the lists of sockets
# to pass to select().
# ==================================================
def readable(self):
return True
if os.name == 'mac':
# The macintosh will select a listening socket for
# write if you let it. What might this mean?
def writable(self):
return not self.accepting
else:
def writable(self):
return True
# ==================================================
# socket object methods.
# ==================================================
def listen(self, num):
self.accepting = 1
if os.name == 'nt' and num > 5:
num = 1
return self.socket.listen(num)
def bind(self, addr):
self.addr = addr
return self.socket.bind(addr)
def connect(self, address):
self.connected = 0
err = self.socket.connect_ex(address)
# XXX Should interpret Winsock return values
if err in (EINPROGRESS, EALREADY, EWOULDBLOCK):
return
if err in (0, EISCONN):
self.addr = address
self.connected = 1
self.handle_connect()
else:
raise socket.error, err
def accept(self):
# XXX can return either an address pair or None
try:
conn, addr = self.socket.accept()
return conn, addr
except socket.error, why:
if why[0] == EWOULDBLOCK:
pass
else:
raise socket.error, why
def send(self, data):
try:
result = self.socket.send(data)
return result
except socket.error, why:
if why[0] == EWOULDBLOCK:
return 0
else:
raise socket.error, why
return 0
def recv(self, buffer_size):
try:
data = self.socket.recv(buffer_size)
if not data:
# a closed connection is indicated by signaling
# a read condition, and having recv() return 0.
self.handle_close()
return ''
else:
return data
except socket.error, why:
# winsock sometimes throws ENOTCONN
if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]:
self.handle_close()
return ''
else:
raise socket.error, why
def close(self):
self.del_channel()
self.socket.close()
# cheap inheritance, used to pass all other attribute
# references to the underlying socket object.
def __getattr__(self, attr):
return getattr(self.socket, attr)
# log and log_info may be overridden to provide more sophisticated
# logging and warning methods. In general, log is for 'hit' logging
# and 'log_info' is for informational, warning and error logging.
def log(self, message):
sys.stderr.write('log: %s\n' % str(message))
def log_info(self, message, type='info'):
if __debug__ or type != 'info':
print '%s: %s' % (type, message)
def handle_read_event(self):
if self.accepting:
# for an accepting socket, getting a read implies
# that we are connected
if not self.connected:
self.connected = 1
self.handle_accept()
elif not self.connected:
self.handle_connect()
self.connected = 1
self.handle_read()
else:
self.handle_read()
def handle_write_event(self):
# getting a write implies that we are connected
if not self.connected:
self.handle_connect()
self.connected = 1
self.handle_write()
def handle_expt_event(self):
self.handle_expt()
def handle_error(self):
nil, t, v, tbinfo = compact_traceback()
# sometimes a user repr method will crash.
try:
self_repr = repr(self)
except:
self_repr = '<__repr__(self) failed for object at %0x>' % id(self)
self.log_info(
'uncaptured python exception, closing channel %s (%s:%s %s)' % (
self_repr,
t,
v,
tbinfo
),
'error'
)
self.close()
def handle_expt(self):
self.log_info('unhandled exception', 'warning')
def handle_read(self):
self.log_info('unhandled read event', 'warning')
def handle_write(self):
self.log_info('unhandled write event', 'warning')
def handle_connect(self):
self.log_info('unhandled connect event', 'warning')
def handle_accept(self):
self.log_info('unhandled accept event', 'warning')
def handle_close(self):
self.log_info('unhandled close event', 'warning')
self.close()
# ---------------------------------------------------------------------------
# adds simple buffered output capability, useful for simple clients.
# [for more sophisticated usage use asynchat.async_chat]
# ---------------------------------------------------------------------------
class dispatcher_with_send(dispatcher):
def __init__(self, sock=None):
dispatcher.__init__(self, sock)
self.out_buffer = ''
def initiate_send(self):
num_sent = 0
num_sent = dispatcher.send(self, self.out_buffer[:512])
self.out_buffer = self.out_buffer[num_sent:]
def handle_write(self):
self.initiate_send()
def writable(self):
return (not self.connected) or len(self.out_buffer)
def send(self, data):
if self.debug:
self.log_info('sending %s' % repr(data))
self.out_buffer = self.out_buffer + data
self.initiate_send()
# ---------------------------------------------------------------------------
# used for debugging.
# ---------------------------------------------------------------------------
def compact_traceback():
t, v, tb = sys.exc_info()
tbinfo = []
assert tb # Must have a traceback
while tb:
tbinfo.append((
tb.tb_frame.f_code.co_filename,
tb.tb_frame.f_code.co_name,
str(tb.tb_lineno)
))
tb = tb.tb_next
# just to be safe
del tb
file, function, line = tbinfo[-1]
info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo])
return (file, function, line), t, v, info
def close_all(map=None):
if map is None:
map = socket_map
for x in map.values():
x.socket.close()
map.clear()

167
others/babelfish.py Normal file
View File

@ -0,0 +1,167 @@
# babelizer.py - API for simple access to babelfish.altavista.com.
# Requires python 2.0 or better.
#
# See it in use at http://babel.MrFeinberg.com/
"""API for simple access to babelfish.altavista.com.
Summary:
import babelizer
print ' '.join(babelizer.available_languages)
print babelizer.translate( 'How much is that doggie in the window?',
'English', 'French' )
def babel_callback(phrase):
print phrase
sys.stdout.flush()
babelizer.babelize( 'I love a reigning knight.',
'English', 'German',
callback = babel_callback )
available_languages
A list of languages available for use with babelfish.
translate( phrase, from_lang, to_lang )
Uses babelfish to translate phrase from from_lang to to_lang.
babelize(phrase, from_lang, through_lang, limit = 12, callback = None)
Uses babelfish to translate back and forth between from_lang and
through_lang until either no more changes occur in translation or
limit iterations have been reached, whichever comes first. Takes
an optional callback function which should receive a single
parameter, being the next translation. Without the callback
returns a list of successive translations.
It's only guaranteed to work if 'english' is one of the two languages
given to either of the translation methods.
Both translation methods throw exceptions which are all subclasses of
BabelizerError. They include
LanguageNotAvailableError
Thrown on an attempt to use an unknown language.
BabelfishChangedError
Thrown when babelfish.altavista.com changes some detail of their
layout, and babelizer can no longer parse the results or submit
the correct form (a not infrequent occurance).
BabelizerIOError
Thrown for various networking and IO errors.
Version: $Id$
Author: Jonathan Feinberg <jdf@pobox.com>
"""
import re, string, urllib
"""
Various patterns I have encountered in looking for the babelfish result.
We try each of them in turn, based on the relative number of times I've
seen each of these patterns. $1.00 to anyone who can provide a heuristic
for knowing which one to use. This includes AltaVista employees.
"""
__where = [ re.compile(r'lang=..>([^<]*)</div'),
re.compile(r'name=\"q\" value=\"([^\"]*)\">'),
]
__languages = { 'english' : 'en',
'chinese' : 'zh',
'french' : 'fr',
'german' : 'de',
'italian' : 'it',
'japanese' : 'ja',
'korean' : 'ko',
'spanish' : 'es',
'portuguese' : 'pt',
}
"""
All of the available language names.
"""
available_languages = [ x.title() for x in __languages.keys() ]
"""
Calling translate() or babelize() can raise a BabelizerError
"""
class BabelizerError(Exception):
pass
class LanguageNotAvailableError(BabelizerError):
pass
class BabelfishChangedError(BabelizerError):
pass
class BabelizerIOError(BabelizerError):
pass
def clean(text):
return ' '.join(string.replace(text.strip(), "\n", ' ').split())
def translate(phrase, from_lang, to_lang):
phrase = clean(phrase)
try:
from_code = __languages[from_lang.lower()]
to_code = __languages[to_lang.lower()]
except KeyError, lang:
raise LanguageNotAvailableError(lang)
params = urllib.urlencode( { 'BabelFishFrontPage' : 'yes',
'doit' : 'done',
'tt' : 'urltext',
'intl' : '1',
'urltext' : phrase,
'lp' : from_code + '_' + to_code } )
try:
response = urllib.urlopen('http://babelfish.altavista.com/babelfish/tr', params)
except IOError, what:
raise BabelizerIOError("Couldn't talk to server: %s" % what)
except:
print "Unexpected error:", sys.exc_info()[0]
html = response.read()
for regex in __where:
match = regex.search(html)
if match:
break
if not match:
raise BabelfishChangedError("Can't recognize translated string.")
return clean(match.group(1))
def babelize(phrase, from_language, through_language, limit = 12, callback = None):
phrase = clean(phrase)
seen = { phrase: 1 }
if callback:
callback(phrase)
else:
results = [ phrase ]
flip = { from_language: through_language, through_language: from_language }
next = from_language
for i in range(limit):
phrase = translate(phrase, next, flip[next])
if seen.has_key(phrase):
break
seen[phrase] = 1
if callback:
callback(phrase)
else:
results.append(phrase)
next = flip[next]
if next != from_language:
phrase = translate(phrase, next, flip[next])
results.append(phrase)
if not callback:
return results
if __name__ == '__main__':
import sys
def printer(x):
print x
sys.stdout.flush();
babelize("I won't take that sort of treatment from you, or from your doggie!",
'english', 'french', callback = printer)

1183
others/convertcore.py Executable file

File diff suppressed because it is too large Load Diff

313
others/dictclient.py Normal file
View File

@ -0,0 +1,313 @@
# Client for the DICT protocol (RFC2229)
#
# Copyright (C) 2002 John Goerzen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import socket, re
version = '1.0'
def dequote(s):
"""Will remove single or double quotes from the start and end of a string
and return the result."""
return s.strip('\'"')
def enquote(s):
"""This function will put a string in double quotes, properly
escaping any existing double quotes with a backslash. It will
return the result."""
return '"' + s.replace('"', "\\\"") + '"'
class Connection:
"""This class is used to establish a connection to a database server.
You will usually use this as the first call into the dictclient library.
Instantiating it takes two optional arguments: a hostname (a string)
and a port (an int). The hostname defaults to localhost
and the port to 2628, the port specified in RFC."""
def __init__(self, hostname='localhost', port=2628, timeout=10):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
self.sock.connect((hostname, port))
self.rfile = self.sock.makefile("rt")
self.wfile = self.sock.makefile("wt", 0)
self.saveconnectioninfo()
def getresultcode(self):
"""Generic function to get a result code. It will return a list
consisting of two items: the integer result code and the text
following. You will not usually use this function directly."""
line = self.rfile.readline().strip()
code, text = line.split(' ', 1)
return [int(code), text]
def get200result(self):
"""Used when expecting a single line of text -- a 200-class
result. Returns [intcode, remaindertext]"""
code, text = self.getresultcode()
if code < 200 or code >= 300:
raise Exception, "Got '%s' when 200-class response expected" % \
line
return [code, text]
def get100block(self):
"""Used when expecting multiple lines of text -- gets the block
part only. Does not get any codes or anything! Returns a string."""
data = []
while 1:
line = self.rfile.readline().strip()
if line == '.':
break
data.append(line)
return "\n".join(data)
def get100result(self):
"""Used when expecting multiple lines of text, terminated by a period
and a 200 code. Returns: [initialcode, [bodytext_1lineperentry],
finalcode]"""
code, text = self.getresultcode()
if code < 100 or code >= 200:
raise Exception, "Got '%s' when 100-class response expected" % \
code
bodylines = self.get100block().split("\n")
code2 = self.get200result()[0]
return [code, bodylines, code2]
def get100dict(self):
"""Used when expecting a dictionary of results. Will read from
the initial 100 code, to a period and the 200 code."""
dict = {}
for line in self.get100result()[1]:
key, val = line.split(' ', 1)
dict[key] = dequote(val)
return dict
def saveconnectioninfo(self):
"""Called by __init__ to handle the initial connection. Will
save off the capabilities and messageid."""
code, string = self.get200result()
assert code == 220
capstr, msgid = re.search('<(.*)> (<.*>)$', string).groups()
self.capabilities = capstr.split('.')
self.messageid = msgid
def getcapabilities(self):
"""Returns a list of the capabilities advertised by the server."""
return self.capabilities
def getmessageid(self):
"""Returns the message id, including angle brackets."""
return self.messageid
def getdbdescs(self):
"""Gets a dict of available databases. The key is the db name
and the value is the db description. This command may generate
network traffic!"""
if hasattr(self, 'dbdescs'):
return self.dbdescs
self.sendcommand("SHOW DB")
self.dbdescs = self.get100dict()
return self.dbdescs
def getstratdescs(self):
"""Gets a dict of available strategies. The key is the strat
name and the value is the strat description. This call may
generate network traffic!"""
if hasattr(self, 'stratdescs'):
return self.stratdescs
self.sendcommand("SHOW STRAT")
self.stratdescs = self.get100dict()
return self.stratdescs
def getdbobj(self, dbname):
"""Gets a Database object corresponding to the database name passed
in. This function explicitly will *not* generate network traffic.
If you have not yet run getdbdescs(), it will fail."""
if not hasattr(self, 'dbobjs'):
self.dbobjs = {}
if self.dbobjs.has_key(dbname):
return self.dbobjs[dbname]
# We use self.dbdescs explicitly since we don't want to
# generate net traffic with this request!
if dbname != '*' and dbname != '!' and \
not dbname in self.dbdescs.keys():
raise Exception, "Invalid database name '%s'" % dbname
self.dbobjs[dbname] = Database(self, dbname)
return self.dbobjs[dbname]
def sendcommand(self, command):
"""Takes a command, without a newline character, and sends it to
the server."""
self.wfile.write(command + "\n")
def define(self, database, word):
"""Returns a list of Definition objects for each matching
definition. Parameters are the database name and the word
to look up. This is one of the main functions you will use
to interact with the server. Returns a list of Definition
objects. If there are no matches, an empty list is returned.
Note: database may be '*' which means to search all databases,
or '!' which means to return matches from the first database that
has a match."""
self.getdbdescs() # Prime the cache
if database != '*' and database != '!' and \
not database in self.getdbdescs():
raise Exception, "Invalid database '%s' specified" % database
self.sendcommand("DEFINE " + enquote(database) + " " + enquote(word))
code = self.getresultcode()[0]
retval = []
if code == 552:
# No definitions.
return []
if code != 150:
raise Exception, "Unknown code %d" % code
while 1:
code, text = self.getresultcode()
if code != 151:
break
resultword, resultdb = re.search('^"(.*)" (\S+)', text).groups()
defstr = self.get100block()
retval.append(Definition(self, self.getdbobj(resultdb),
resultword, defstr))
return retval
def match(self, database, strategy, word):
"""Gets matches for a query. Arguments are database name,
the strategy (see available ones in getstratdescs()), and the
pattern/word to look for. Returns a list of Definition objects.
If there is no match, an empty list is returned.
Note: database may be '*' which means to search all databases,
or '!' which means to return matches from the first database that
has a match."""
self.getstratdescs() # Prime the cache
self.getdbdescs() # Prime the cache
if not strategy in self.getstratdescs().keys():
raise Exception, "Invalid strategy '%s'" % strategy
if database != '*' and database != '!' and \
not database in self.getdbdescs().keys():
raise Exception, "Invalid database name '%s'" % database
self.sendcommand("MATCH %s %s %s" % (enquote(database),
enquote(strategy),
enquote(word)))
code = self.getresultcode()[0]
if code == 552:
# No Matches
return []
if code != 152:
raise Exception, "Unexpected code %d" % code
retval = []
for matchline in self.get100block().split("\n"):
matchdict, matchword = matchline.split(" ", 1)
retval.append(Definition(self, self.getdbobj(matchdict),
dequote(matchword)))
if self.getresultcode()[0] != 250:
raise Exception, "Unexpected end-of-list code %d" % code
return retval
class Database:
"""An object corresponding to a particular database in a server."""
def __init__(self, dictconn, dbname):
"""Initialize the object -- requires a Connection object and
a database name."""
self.conn = dictconn
self.name = dbname
def getname(self):
"""Returns the short name for this database."""
return self.name
def getdescription(self):
if hasattr(self, 'description'):
return self.description
if self.getname() == '*':
self.description = 'All Databases'
elif self.getname() == '!':
self.description = 'First matching database'
else:
self.description = self.conn.getdbdescs()[self.getname()]
return self.description
def getinfo(self):
"""Returns a string of info describing this database."""
if hasattr(self, 'info'):
return self.info
if self.getname() == '*':
self.info = "This special database will search all databases on the system."
elif self.getname() == '!':
self.info = "This special database will return matches from the first matching database."
else:
self.conn.sendcommand("SHOW INFO " + self.name)
self.info = "\n".join(self.conn.get100result()[1])
return self.info
def define(self, word):
"""Get a definition from within this database.
The argument, word, is the word to look up. The return value is the
same as from Connection.define()."""
return self.conn.define(self.getname(), word)
def match(self, strategy, word):
"""Get a match from within this database.
The argument, word, is the word to look up. The return value is
the same as from Connection.define()."""
return self.conn.match(self.getname(), strategy, word)
class Definition:
"""An object corresponding to a single definition."""
def __init__(self, dictconn, db, word, defstr = None):
"""Instantiate the object. Requires: a Connection object,
a Database object (NOT corresponding to '*' or '!' databases),
a word. Optional: a definition string. If not supplied,
it will be fetched if/when it is requested."""
self.conn = dictconn
self.db = db
self.word = word
self.defstr = defstr
def getdb(self):
"""Get the Database object corresponding to this definition."""
return self.db
def getdefstr(self):
"""Get the definition string (the actual content) of this
definition."""
if not self.defstr:
self.defstr = self.conn.define(self.getdb().getname(), self.word)[0].getdefstr()
return self.defstr
def getword(self):
"""Get the word this object describes."""
return self.word

433
others/google.py Normal file
View File

@ -0,0 +1,433 @@
"""Python wrapper for Google web APIs
This module allows you to access Google's web APIs through SOAP,
to do things like search Google and get the results programmatically.
Described here:
http://www.google.com/apis/
You need a Google-provided license key to use these services.
Follow the link above to get one. These functions will look in
several places (in this order) for the license key:
- the "license_key" argument of each function
- the module-level LICENSE_KEY variable (call setLicense once to set it)
- an environment variable called GOOGLE_LICENSE_KEY
- a file called ".googlekey" in the current directory
- a file called "googlekey.txt" in the current directory
- a file called ".googlekey" in your home directory
- a file called "googlekey.txt" in your home directory
- a file called ".googlekey" in the same directory as google.py
- a file called "googlekey.txt" in the same directory as google.py
Sample usage:
>>> import google
>>> google.setLicense('...') # must get your own key!
>>> data = google.doGoogleSearch('python')
>>> data.meta.searchTime
0.043221000000000002
>>> data.results[0].URL
'http://www.python.org/'
>>> data.results[0].title
'<b>Python</b> Language Website'
See documentation of SearchResultsMetaData and SearchResult classes
for other available attributes.
"""
__author__ = "Mark Pilgrim (f8dy@diveintomark.org)"
__version__ = "0.5.2"
__cvsversion__ = "$Revision$"[11:-2]
__date__ = "$Date$"[7:-2]
__copyright__ = "Copyright (c) 2002 Mark Pilgrim"
__license__ = "Python"
__credits__ = """David Ascher, for the install script
Erik Max Francis, for the command line interface
Michael Twomey, for HTTP proxy support"""
import SOAP
import os, sys, getopt
LICENSE_KEY = None
HTTP_PROXY = None
# don't touch the rest of these constants
class NoLicenseKey(Exception): pass
_url = 'http://api.google.com/search/beta2'
_namespace = 'urn:GoogleSearch'
_false = SOAP.booleanType(0)
_true = SOAP.booleanType(1)
_googlefile1 = ".googlekey"
_googlefile2 = "googlekey.txt"
_licenseLocations = (
(lambda key: key, 'passed to the function in license_key variable'),
(lambda key: LICENSE_KEY, 'module-level LICENSE_KEY variable (call setLicense to set it)'),
(lambda key: os.environ.get('GOOGLE_LICENSE_KEY', None), 'an environment variable called GOOGLE_LICENSE_KEY'),
(lambda key: _contentsOf(os.getcwd(), _googlefile1), '%s in the current directory' % _googlefile1),
(lambda key: _contentsOf(os.getcwd(), _googlefile2), '%s in the current directory' % _googlefile2),
(lambda key: _contentsOf(os.environ.get('HOME', ''), _googlefile1), '%s in your home directory' % _googlefile1),
(lambda key: _contentsOf(os.environ.get('HOME', ''), _googlefile2), '%s in your home directory' % _googlefile2),
(lambda key: _contentsOf(_getScriptDir(), _googlefile1), '%s in the google.py directory' % _googlefile1),
(lambda key: _contentsOf(_getScriptDir(), _googlefile2), '%s in the google.py directory' % _googlefile2)
)
## administrative functions
def version():
print """PyGoogle %(__version__)s
%(__copyright__)s
released %(__date__)s
Thanks to:
%(__credits__)s""" % globals()
def usage():
program = os.path.basename(sys.argv[0])
print """Usage: %(program)s [options] [querytype] query
options:
-k, --key= <license key> Google license key (see important note below)
-1, -l, --lucky show only first hit
-m, --meta show meta information
-r, --reverse show results in reverse order
-x, --proxy= <url> use HTTP proxy
-h, --help print this help
-v, --version print version and copyright information
-t, --test run test queries
querytype:
-s, --search= <query> search (default)
-c, --cache= <url> retrieve cached page
-p, --spelling= <word> check spelling
IMPORTANT NOTE: all Google functions require a valid license key;
visit http://www.google.com/apis/ to get one. %(program)s will look in
these places (in order) and use the first license key it finds:
* the key specified on the command line""" % vars()
for get, location in _licenseLocations[2:]:
print " *", location
## utility functions
def setLicense(license_key):
"""set license key"""
global LICENSE_KEY
LICENSE_KEY = license_key
def getLicense(license_key = None):
"""get license key
license key can come from any number of locations;
see module docs for search order"""
for get, location in _licenseLocations:
rc = get(license_key)
if rc: return rc
#usage()
raise NoLicenseKey, 'get a license key at http://www.google.com/apis/'
def setProxy(http_proxy):
"""set HTTP proxy"""
global HTTP_PROXY
HTTP_PROXY = http_proxy
def getProxy(http_proxy = None):
"""get HTTP proxy"""
return http_proxy or HTTP_PROXY
def _contentsOf(dirname, filename):
filename = os.path.join(dirname, filename)
if not os.path.exists(filename): return None
fsock = open(filename)
contents = fsock.read()
fsock.close()
return contents
def _getScriptDir():
if __name__ == '__main__':
return os.path.abspath(os.path.dirname(sys.argv[0]))
else:
return os.path.abspath(os.path.dirname(sys.modules[__name__].__file__))
def _marshalBoolean(value):
if value:
return _true
else:
return _false
## output formatters
def makeFormatter(outputFormat):
classname = "%sOutputFormatter" % outputFormat.capitalize()
return globals()[classname]()
def output(results, params):
formatter = makeFormatter(params.get("outputFormat", "text"))
outputmethod = getattr(formatter, params["func"])
outputmethod(results, params)
class OutputFormatter:
def boil(self, data):
if type(data) == type(u""):
return data.encode("utf-8", "replace")
else:
return data
class TextOutputFormatter(OutputFormatter):
def common(self, data, params):
if params.get("showMeta", 0):
meta = data.meta
for category in meta.directoryCategories:
print "directoryCategory: %s" % self.boil(category["fullViewableName"])
for attr in [node for node in dir(meta) if node <> "directoryCategories" and node[:2] <> '__']:
print "%s:" % attr, self.boil(getattr(meta, attr))
def doGoogleSearch(self, data, params):
results = data.results
if params.get("feelingLucky", 0):
results = results[:1]
if params.get("reverseOrder", 0):
results.reverse()
for result in results:
for attr in dir(result):
if attr == "directoryCategory":
print "directoryCategory:", self.boil(result.directoryCategory["fullViewableName"])
elif attr[:2] <> '__':
print "%s:" % attr, self.boil(getattr(result, attr))
print
self.common(data, params)
def doGetCachedPage(self, data, params):
print data
self.common(data, params)
doSpellingSuggestion = doGetCachedPage
## search results classes
class _SearchBase:
def __init__(self, params):
for k, v in params.items():
if isinstance(v, SOAP.structType):
v = v._asdict
try:
if isinstance(v[0], SOAP.structType):
v = [node._asdict for node in v]
except:
pass
self.__dict__[str(k)] = v
class SearchResultsMetaData(_SearchBase):
"""metadata of search query results
Available attributes:
documentFiltering - flag indicates whether duplicate page filtering was perfomed in this search
searchComments - human-readable informational message (example: "'the' is a very common word
and was not included in your search")
estimatedTotalResultsCount - estimated total number of results for this query
estimateIsExact - flag indicates whether estimatedTotalResultsCount is an exact value
searchQuery - search string that initiated this search
startIndex - index of first result returned (zero-based)
endIndex - index of last result returned (zero-based)
searchTips - human-readable informational message on how to use Google bette
directoryCategories - list of dictionaries like this:
{'fullViewableName': Open Directory category,
'specialEncoding': encoding scheme of this directory category}
searchTime - total search time, in seconds
"""
pass
class SearchResult(_SearchBase):
"""search result
Available attributes:
URL - URL
title - title (HTML)
snippet - snippet showing query context (HTML)
cachedSize - size of cached version of this result, (KB)
relatedInformationPresent - flag indicates that the "related:" keyword is supported for this URL
hostName: When filtering occurs, a maximum of two results from any given host is returned.
When this occurs, the second resultElement that comes from that host contains
the host name in this parameter.
directoryCategory: dictionary like this:
{'fullViewableName': Open Directory category,
'specialEncoding': encoding scheme of this directory category}
directoryTitle: Open Directory title of this result (or blank)
summary - Open Directory summary for this result (or blank)
"""
pass
class SearchReturnValue:
"""complete search results for a single query
Available attributes:
meta - SearchResultsMetaData
results - list of SearchResult
"""
def __init__(self, metadata, results):
self.meta = metadata
self.results = results
## main functions
def doGoogleSearch(q, start=0, maxResults=10, filter=1, restrict='',
safeSearch=0, language='', inputencoding='', outputencoding='',
license_key = None, http_proxy = None):
"""search Google
You need a license key to call this function; see
http://www.google.com/apis/ to get one. Then you can either pass it to
this function every time, or set it globally; see the module docs for details.
Parameters:
q - search string. Anything you could type at google.com, you can pass here.
See http://www.google.com/help/features.html for examples of advanced features.
start (optional) - zero-based index of first desired result (for paging through
multiple pages of results)
maxResults (optional) - maximum number of results, currently capped at 10
filter (optional) - set to 1 to filter out similar results, set to 0 to see everything
restrict (optional) - restrict results by country or topic. Examples:
Ukraine - search only sites located in Ukraine
linux - search Linux sites only
mac - search Mac sites only
bsd - search FreeBSD sites only
See the APIs_reference.html file in the SDK (http://www.google.com/apis/download.html)
for more advanced examples and a full list of country codes and topics.
safeSearch (optional) - set to 1 to filter results with SafeSearch (no adult material)
language (optional) - restricts search to documents in one or more languages. Example:
lang_en - only return pages in English
lang_fr - only return pages in French
See the APIs_reference.html file in the SDK (http://www.google.com/apis/download.html)
for more advanced examples and a full list of language codes.
inputencoding (optional) - sets the character encoding of q parameter
outputencoding (optional) - sets the character encoding of the returned results
See the APIs_reference.html file in the SDK (http://www.google.com/apis/download.html)
for a full list of encodings.
http_proxy (optional) - address of HTTP proxy to use for sending and receiving SOAP messages
Returns: SearchReturnValue
.meta - SearchMetaData
.results - list of SearchResult
See documentation of these individual classes for list of available attributes
"""
http_proxy = getProxy(http_proxy)
remoteserver = SOAP.SOAPProxy(_url, namespace=_namespace, http_proxy=http_proxy)
license_key = getLicense(license_key)
filter = _marshalBoolean(filter)
safeSearch = _marshalBoolean(safeSearch)
data = remoteserver.doGoogleSearch(license_key, q, start, maxResults, filter, restrict,
safeSearch, language, inputencoding, outputencoding)
metadata = data._asdict
del metadata["resultElements"]
metadata = SearchResultsMetaData(metadata)
results = [SearchResult(node._asdict) for node in data.resultElements]
return SearchReturnValue(metadata, results)
def doGetCachedPage(url, license_key = None, http_proxy = None):
"""get page from Google cache
You need a license key to call this function; see
http://www.google.com/apis/ to get one. Then you can either pass it to
this function every time, or set it globally; see the module docs for details.
Parameters:
url - address of page to get
license_key (optional) - Google license key
http_proxy (optional) - address of HTTP proxy to use for sending and receiving SOAP messages
Returns: string, text of cached page
"""
http_proxy = getProxy(http_proxy)
remoteserver = SOAP.SOAPProxy(_url, namespace=_namespace, http_proxy=http_proxy)
license_key = getLicense(license_key)
return remoteserver.doGetCachedPage(license_key, url)
def doSpellingSuggestion(phrase, license_key = None, http_proxy = None):
"""get spelling suggestions from Google
You need a license key to call this function; see
http://www.google.com/apis/ to get one. Then you can either pass it to
this function every time, or set it globally; see the module docs for details.
Parameters:
phrase - word or phrase to spell-check
http_proxy (optional) - address of HTTP proxy to use for sending and receiving SOAP messages
Returns: text of suggested replacement, or None
"""
http_proxy = getProxy(http_proxy)
remoteserver = SOAP.SOAPProxy(_url, namespace=_namespace, http_proxy=http_proxy)
license_key = getLicense(license_key)
return remoteserver.doSpellingSuggestion(license_key, phrase)
## functional test suite (see googletest.py for unit test suite)
def test():
try:
getLicense(None)
except NoLicenseKey:
return
print "Searching for Python at google.com..."
data = doGoogleSearch("Python")
output(data, {"func": "doGoogleSearch"})
print "\nSearching for 5 _French_ pages about Python, encoded in ISO-8859-1..."
data = doGoogleSearch("Python", language='lang_fr', outputencoding='ISO-8859-1', maxResults=5)
output(data, {"func": "doGoogleSearch"})
phrase = "Pyhton programming languager"
print "\nTesting spelling suggetions for '%s'..." % phrase
data = doSpellingSuggestion(phrase)
output(data, {"func": "doSpellingSuggestion"})
## main driver for command-line use
def main(argv):
if not argv:
usage()
return
q = None
func = None
http_proxy = None
license_key = None
feelingLucky = 0
showMeta = 0
reverseOrder = 0
runTest = 0
outputFormat = "text"
try:
opts, args = getopt.getopt(argv, "s:c:p:k:lmrx:hvt1",
["search=", "cache=", "spelling=", "key=", "lucky", "meta", "reverse", "proxy=", "help", "version", "test"])
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-s", "--search"):
q = arg
func = "doGoogleSearch"
elif opt in ("-c", "--cache"):
q = arg
func = "doGetCachedPage"
elif opt in ("-p", "--spelling"):
q = arg
func = "doSpellingSuggestion"
elif opt in ("-k", "--key"):
license_key = arg
elif opt in ("-l", "-1", "--lucky"):
feelingLucky = 1
elif opt in ("-m", "--meta"):
showMeta = 1
elif opt in ("-r", "--reverse"):
reverseOrder = 1
elif opt in ("-x", "--proxy"):
http_proxy = arg
elif opt in ("-h", "--help"):
usage()
elif opt in ("-v", "--version"):
version()
elif opt in ("-t", "--test"):
runTest = 1
if runTest:
setLicense(license_key)
setProxy(http_proxy)
test()
if args and not q:
q = args[0]
func = "doGoogleSearch"
if func:
results = globals()[func](q, http_proxy=http_proxy, license_key=license_key)
output(results, locals())
if __name__ == '__main__':
main(sys.argv[1:])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

1013
others/rfc822.py Executable file

File diff suppressed because it is too large Load Diff

722
others/rssparser.py Normal file
View File

@ -0,0 +1,722 @@
#!/usr/bin/python
"""Ultra-liberal feed parser
Visit http://diveintomark.org/projects/feed_parser/ for the latest version
Handles RSS 0.9x, RSS 1.0, RSS 2.0, Pie/Atom/Echo feeds
RSS 0.9x/common elements:
- title, link, guid, description, webMaster, managingEditor, language
copyright, lastBuildDate, pubDate
Additional RSS 1.0/2.0 elements:
- dc:rights, dc:language, dc:creator, dc:date, dc:subject,
content:encoded, admin:generatorAgent, admin:errorReportsTo,
Addition Pie/Atom/Echo elements:
- subtitle, created, issued, modified, summary, id, content
Things it handles that choke other parsers:
- bastard combinations of RSS 0.9x and RSS 1.0
- illegal XML characters
- naked and/or invalid HTML in description
- content:encoded in item element
- guid in item element
- fullitem in item element
- non-standard namespaces
- inline XML in content (Pie/Atom/Echo)
- multiple content items per entry (Pie/Atom/Echo)
Requires Python 2.2 or later
"""
__version__ = "2.5.3"
__author__ = "Mark Pilgrim <http://diveintomark.org/>"
__copyright__ = "Copyright 2002-3, Mark Pilgrim"
__contributors__ = ["Jason Diamond <http://injektilo.org/>",
"John Beimler <http://john.beimler.org/>"]
__license__ = "Python"
__history__ = """
1.0 - 9/27/2002 - MAP - fixed namespace processing on prefixed RSS 2.0 elements,
added Simon Fell's test suite
1.1 - 9/29/2002 - MAP - fixed infinite loop on incomplete CDATA sections
2.0 - 10/19/2002
JD - use inchannel to watch out for image and textinput elements which can
also contain title, link, and description elements
JD - check for isPermaLink="false" attribute on guid elements
JD - replaced openAnything with open_resource supporting ETag and
If-Modified-Since request headers
JD - parse now accepts etag, modified, agent, and referrer optional
arguments
JD - modified parse to return a dictionary instead of a tuple so that any
etag or modified information can be returned and cached by the caller
2.0.1 - 10/21/2002 - MAP - changed parse() so that if we don't get anything
because of etag/modified, return the old etag/modified to the caller to
indicate why nothing is being returned
2.0.2 - 10/21/2002 - JB - added the inchannel to the if statement, otherwise its
useless. Fixes the problem JD was addressing by adding it.
2.1 - 11/14/2002 - MAP - added gzip support
2.2 - 1/27/2003 - MAP - added attribute support, admin:generatorAgent.
start_admingeneratoragent is an example of how to handle elements with
only attributes, no content.
2.3 - 6/11/2003 - MAP - added USER_AGENT for default (if caller doesn't specify);
also, make sure we send the User-Agent even if urllib2 isn't available.
Match any variation of backend.userland.com/rss namespace.
2.3.1 - 6/12/2003 - MAP - if item has both link and guid, return both as-is.
2.4 - 7/9/2003 - MAP - added preliminary Pie/Atom/Echo support based on Sam Ruby's
snapshot of July 1 <http://www.intertwingly.net/blog/1506.html>; changed
project name
2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree);
removed unnecessary urllib code -- urllib2 should always be available anyway;
return actual url, status, and full HTTP headers (as result['url'],
result['status'], and result['headers']) if parsing a remote feed over HTTP --
this should pass all the HTTP tests at <http://diveintomark.org/tests/client/http/>;
added the latest namespace-of-the-week for RSS 2.0
2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom
User-Agent (otherwise urllib2 sends two, which confuses some servers)
2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for
inline <xhtml:body> and <xhtml:div> as used in some RSS 2.0 feeds
2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or
textInput, and also to return the character encoding (if specified)
"""
try:
import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
timeoutsocket.setDefaultSocketTimeout(10)
except ImportError:
pass
import cgi, re, sgmllib, string, StringIO, gzip, urllib2
sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
USER_AGENT = "UltraLiberalFeedParser/%s +http://diveintomark.org/projects/feed_parser/" % __version__
def decodeEntities(data):
data = data or ''
data = data.replace('&lt;', '<')
data = data.replace('&gt;', '>')
data = data.replace('&quot;', '"')
data = data.replace('&apos;', "'")
data = data.replace('&amp;', '&')
return data
class FeedParser(sgmllib.SGMLParser):
namespaces = {"http://backend.userland.com/rss": "",
"http://blogs.law.harvard.edu/tech/rss": "",
"http://purl.org/rss/1.0/": "",
"http://example.com/newformat#": "",
"http://example.com/necho": "",
"http://purl.org/echo/": "",
"uri/of/echo/namespace#": "",
"http://purl.org/pie/": "",
"http://purl.org/rss/1.0/modules/textinput/": "ti",
"http://purl.org/rss/1.0/modules/company/": "co",
"http://purl.org/rss/1.0/modules/syndication/": "sy",
"http://purl.org/dc/elements/1.1/": "dc",
"http://webns.net/mvcb/": "admin",
"http://www.w3.org/1999/xhtml": "xhtml"}
def reset(self):
self.channel = {}
self.items = []
self.elementstack = []
self.inchannel = 0
self.initem = 0
self.incontent = 0
self.intextinput = 0
self.inimage = 0
self.contentmode = None
self.contenttype = None
self.contentlang = None
self.namespacemap = {}
sgmllib.SGMLParser.reset(self)
def push(self, element, expectingText):
self.elementstack.append([element, expectingText, []])
def pop(self, element):
if not self.elementstack: return
if self.elementstack[-1][0] != element: return
element, expectingText, pieces = self.elementstack.pop()
if not expectingText: return
output = "".join(pieces)
output = decodeEntities(output)
if self.incontent and self.initem:
if not self.items[-1].has_key(element):
self.items[-1][element] = []
self.items[-1][element].append({"language":self.contentlang, "type":self.contenttype, "value":output})
elif self.initem:
self.items[-1][element] = output
elif self.inchannel and (not self.intextinput) and (not self.inimage):
self.channel[element] = output
def _addNamespaces(self, attrs):
for prefix, value in attrs:
if not prefix.startswith("xmlns:"): continue
prefix = prefix[6:]
if prefix.find('backend.userland.com/rss') <> -1:
# match any backend.userland.com namespace
prefix = 'http://backend.userland.com/rss'
if self.namespaces.has_key(value):
self.namespacemap[prefix] = self.namespaces[value]
def _mapToStandardPrefix(self, name):
colonpos = name.find(':')
if colonpos <> -1:
prefix = name[:colonpos]
suffix = name[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
name = prefix + ':' + suffix
return name
def _getAttribute(self, attrs, name):
value = [v for k, v in attrs if self._mapToStandardPrefix(k) == name]
if value:
value = value[0]
else:
value = None
return value
def start_channel(self, attrs):
self.push('channel', 0)
self.inchannel = 1
def end_channel(self):
self.pop('channel')
self.inchannel = 0
def start_image(self, attrs):
self.inimage = 1
def end_image(self):
self.inimage = 0
def start_textinput(self, attrs):
self.intextinput = 1
def end_textinput(self):
self.intextinput = 0
def start_item(self, attrs):
self.items.append({})
self.push('item', 0)
self.initem = 1
def end_item(self):
self.pop('item')
self.initem = 0
def start_dc_language(self, attrs):
self.push('language', 1)
start_language = start_dc_language
def end_dc_language(self):
self.pop('language')
end_language = end_dc_language
def start_dc_creator(self, attrs):
self.push('creator', 1)
start_managingeditor = start_dc_creator
start_webmaster = start_dc_creator
def end_dc_creator(self):
self.pop('creator')
end_managingeditor = end_dc_creator
end_webmaster = end_dc_creator
def start_dc_rights(self, attrs):
self.push('rights', 1)
start_copyright = start_dc_rights
def end_dc_rights(self):
self.pop('rights')
end_copyright = end_dc_rights
def start_dc_date(self, attrs):
self.push('date', 1)
start_lastbuilddate = start_dc_date
start_pubdate = start_dc_date
def end_dc_date(self):
self.pop('date')
end_lastbuilddate = end_dc_date
end_pubdate = end_dc_date
def start_dc_subject(self, attrs):
self.push('category', 1)
def end_dc_subject(self):
self.pop('category')
def start_link(self, attrs):
self.push('link', self.inchannel or self.initem)
def end_link(self):
self.pop('link')
def start_guid(self, attrs):
self.guidislink = ('ispermalink', 'false') not in attrs
self.push('guid', 1)
def end_guid(self):
self.pop('guid')
if self.guidislink:
if not self.items[-1].has_key('link'):
# guid acts as link, but only if "ispermalink" is not present or is "true",
# and only if the item doesn't already have a link element
self.items[-1]['link'] = self.items[-1]['guid']
def start_title(self, attrs):
self.push('title', self.inchannel or self.initem)
def start_description(self, attrs):
self.push('description', self.inchannel or self.initem)
def start_content_encoded(self, attrs):
self.push('content_encoded', 1)
start_fullitem = start_content_encoded
def end_content_encoded(self):
self.pop('content_encoded')
end_fullitem = end_content_encoded
def start_admin_generatoragent(self, attrs):
self.push('generator', 1)
value = self._getAttribute(attrs, 'rdf:resource')
if value:
self.elementstack[-1][2].append(value)
self.pop('generator')
def start_feed(self, attrs):
self.inchannel = 1
def end_feed(self):
self.inchannel = 0
def start_entry(self, attrs):
self.items.append({})
self.push('item', 0)
self.initem = 1
def end_entry(self):
self.pop('item')
self.initem = 0
def start_subtitle(self, attrs):
self.push('subtitle', 1)
def end_subtitle(self):
self.pop('subtitle')
def start_summary(self, attrs):
self.push('summary', 1)
def end_summary(self):
self.pop('summary')
def start_modified(self, attrs):
self.push('modified', 1)
def end_modified(self):
self.pop('modified')
def start_created(self, attrs):
self.push('created', 1)
def end_created(self):
self.pop('created')
def start_issued(self, attrs):
self.push('issued', 1)
def end_issued(self):
self.pop('issued')
def start_id(self, attrs):
self.push('id', 1)
def end_id(self):
self.pop('id')
def start_content(self, attrs):
self.incontent = 1
if ('mode', 'escaped') in attrs:
self.contentmode = 'escaped'
elif ('mode', 'base64') in attrs:
self.contentmode = 'base64'
else:
self.contentmode = 'xml'
mimetype = [v for k, v in attrs if k=='type']
if mimetype:
self.contenttype = mimetype[0]
xmllang = [v for k, v in attrs if k=='xml:lang']
if xmllang:
self.contentlang = xmllang[0]
self.push('content', 1)
def end_content(self):
self.pop('content')
self.incontent = 0
self.contentmode = None
self.contenttype = None
self.contentlang = None
def start_body(self, attrs):
self.incontent = 1
self.contentmode = 'xml'
self.contenttype = 'application/xhtml+xml'
xmllang = [v for k, v in attrs if k=='xml:lang']
if xmllang:
self.contentlang = xmllang[0]
self.push('content', 1)
start_div = start_body
start_xhtml_body = start_body
start_xhtml_div = start_body
end_body = end_content
end_div = end_content
end_xhtml_body = end_content
end_xhtml_div = end_content
def unknown_starttag(self, tag, attrs):
if self.incontent and self.contentmode == 'xml':
self.handle_data("<%s%s>" % (tag, "".join([' %s="%s"' % t for t in attrs])))
return
self._addNamespaces(attrs)
colonpos = tag.find(':')
if colonpos <> -1:
prefix = tag[:colonpos]
suffix = tag[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
if prefix:
prefix = prefix + '_'
methodname = 'start_' + prefix + suffix
try:
method = getattr(self, methodname)
return method(attrs)
except AttributeError:
return self.push(prefix + suffix, 0)
return self.push(tag, 0)
def unknown_endtag(self, tag):
if self.incontent and self.contentmode == 'xml':
self.handle_data("</%s>" % tag)
return
colonpos = tag.find(':')
if colonpos <> -1:
prefix = tag[:colonpos]
suffix = tag[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
if prefix:
prefix = prefix + '_'
methodname = 'end_' + prefix + suffix
try:
method = getattr(self, methodname)
return method()
except AttributeError:
return self.pop(prefix + suffix)
return self.pop(tag)
def handle_charref(self, ref):
# called for each character reference, e.g. for "&#160;", ref will be "160"
# Reconstruct the original character reference.
if not self.elementstack: return
text = "&#%s;" % ref
if self.incontent and self.contentmode == 'xml':
text = cgi.escape(text)
self.elementstack[-1][2].append(text)
def handle_entityref(self, ref):
# called for each entity reference, e.g. for "&copy;", ref will be "copy"
# Reconstruct the original entity reference.
if not self.elementstack: return
text = "&%s;" % ref
if self.incontent and self.contentmode == 'xml':
text = cgi.escape(text)
self.elementstack[-1][2].append(text)
def handle_data(self, text):
# called for each block of plain text, i.e. outside of any tag and
# not containing any character or entity references
if not self.elementstack: return
if self.incontent and self.contentmode == 'xml':
text = cgi.escape(text)
self.elementstack[-1][2].append(text)
def handle_comment(self, text):
# called for each comment, e.g. <!-- insert message here -->
pass
def handle_pi(self, text):
# called for each processing instruction, e.g. <?instruction>
pass
def handle_decl(self, text):
# called for the DOCTYPE, if present, e.g.
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
# "http://www.w3.org/TR/html4/loose.dtd">
pass
_new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
def _scan_name(self, i, declstartpos):
rawdata = self.rawdata
n = len(rawdata)
if i == n:
return None, -1
m = self._new_declname_match(rawdata, i)
if m:
s = m.group()
name = s.strip()
if (i + len(s)) == n:
return None, -1 # end of buffer
return string.lower(name), m.end()
else:
self.updatepos(declstartpos, i)
self.error("expected name token")
def parse_declaration(self, i):
# override internal declaration handler to handle CDATA blocks
if self.rawdata[i:i+9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1: k = len(self.rawdata)
self.handle_data(cgi.escape(self.rawdata[i+9:k]))
return k+3
return sgmllib.SGMLParser.parse_declaration(self, i)
class FeedURLHandler(urllib2.HTTPRedirectHandler, urllib2.HTTPDefaultErrorHandler):
def http_error_default(self, req, fp, code, msg, headers):
if ((code / 100) == 3) and (code != 304):
return self.http_error_302(req, fp, code, msg, headers)
from urllib import addinfourl
infourl = addinfourl(fp, headers, req.get_full_url())
infourl.status = code
return infourl
# raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
def http_error_302(self, req, fp, code, msg, headers):
infourl = urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
infourl.status = code
return infourl
def http_error_301(self, req, fp, code, msg, headers):
infourl = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers)
infourl.status = code
return infourl
http_error_300 = http_error_302
http_error_307 = http_error_302
def open_resource(source, etag=None, modified=None, agent=None, referrer=None):
"""
URI, filename, or string --> stream
This function lets you define parsers that take any input source
(URL, pathname to local or network file, or actual data as a string)
and deal with it in a uniform manner. Returned object is guaranteed
to have all the basic stdio read methods (read, readline, readlines).
Just .close() the object when you're done with it.
If the etag argument is supplied, it will be used as the value of an
If-None-Match request header.
If the modified argument is supplied, it must be a tuple of 9 integers
as returned by gmtime() in the standard Python time module. This MUST
be in GMT (Greenwich Mean Time). The formatted date/time will be used
as the value of an If-Modified-Since request header.
If the agent argument is supplied, it will be used as the value of a
User-Agent request header.
If the referrer argument is supplied, it will be used as the value of a
Referer[sic] request header.
"""
if hasattr(source, "read"):
return source
if source == "-":
return sys.stdin
if not agent:
agent = USER_AGENT
# try to open with urllib2 (to use optional headers)
request = urllib2.Request(source)
if etag:
request.add_header("If-None-Match", etag)
if modified:
request.add_header("If-Modified-Since", format_http_date(modified))
request.add_header("User-Agent", agent)
if referrer:
request.add_header("Referer", referrer)
request.add_header("Accept-encoding", "gzip")
opener = urllib2.build_opener(FeedURLHandler())
opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent
try:
return opener.open(request)
except:
# source is not a valid URL, but it might be a valid filename
pass
# try to open with native open function (if source is a filename)
try:
return open(source)
except:
pass
# treat source as string
return StringIO.StringIO(str(source))
def get_etag(resource):
"""
Get the ETag associated with a response returned from a call to
open_resource().
If the resource was not returned from an HTTP server or the server did
not specify an ETag for the resource, this will return None.
"""
if hasattr(resource, "info"):
return resource.info().getheader("ETag")
return None
def get_modified(resource):
"""
Get the Last-Modified timestamp for a response returned from a call to
open_resource().
If the resource was not returned from an HTTP server or the server did
not specify a Last-Modified timestamp, this function will return None.
Otherwise, it returns a tuple of 9 integers as returned by gmtime() in
the standard Python time module().
"""
if hasattr(resource, "info"):
last_modified = resource.info().getheader("Last-Modified")
if last_modified:
return parse_http_date(last_modified)
return None
short_weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
long_weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def format_http_date(date):
"""
Formats a tuple of 9 integers into an RFC 1123-compliant timestamp as
required in RFC 2616. We don't use time.strftime() since the %a and %b
directives can be affected by the current locale (HTTP dates have to be
in English). The date MUST be in GMT (Greenwich Mean Time).
"""
return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (short_weekdays[date[6]], date[2], months[date[1] - 1], date[0], date[3], date[4], date[5])
rfc1123_match = re.compile(r"(?P<weekday>[A-Z][a-z]{2}), (?P<day>\d{2}) (?P<month>[A-Z][a-z]{2}) (?P<year>\d{4}) (?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2}) GMT").match
rfc850_match = re.compile(r"(?P<weekday>[A-Z][a-z]+), (?P<day>\d{2})-(?P<month>[A-Z][a-z]{2})-(?P<year>\d{2}) (?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2}) GMT").match
asctime_match = re.compile(r"(?P<weekday>[A-Z][a-z]{2}) (?P<month>[A-Z][a-z]{2}) ?(?P<day>\d\d?) (?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2}) (?P<year>\d{4})").match
def parse_http_date(date):
"""
Parses any of the three HTTP date formats into a tuple of 9 integers as
returned by time.gmtime(). This should not use time.strptime() since
that function is not available on all platforms and could also be
affected by the current locale.
"""
date = str(date)
year = 0
weekdays = short_weekdays
m = rfc1123_match(date)
if not m:
m = rfc850_match(date)
if m:
year = 1900
weekdays = long_weekdays
else:
m = asctime_match(date)
if not m:
return None
try:
year = year + int(m.group("year"))
month = months.index(m.group("month")) + 1
day = int(m.group("day"))
hour = int(m.group("hour"))
minute = int(m.group("minute"))
second = int(m.group("second"))
weekday = weekdays.index(m.group("weekday"))
a = int((14 - month) / 12)
julian_day = (day - 32045 + int(((153 * (month + (12 * a) - 3)) + 2) / 5) + int((146097 * (year + 4800 - a)) / 400)) - (int((146097 * (year + 4799)) / 400) - 31738) + 1
daylight_savings_flag = 0
return (year, month, day, hour, minute, second, weekday, julian_day, daylight_savings_flag)
except:
# the month or weekday lookup probably failed indicating an invalid timestamp
return None
def parse(uri, etag=None, modified=None, agent=None, referrer=None):
r = FeedParser()
f = open_resource(uri, etag=etag, modified=modified, agent=agent, referrer=referrer)
data = f.read()
if hasattr(f, "headers"):
if f.headers.get('content-encoding', '') == 'gzip':
try:
data = gzip.GzipFile(fileobj=StringIO.StringIO(data)).read()
except:
# some feeds claim to be gzipped but they're not, so we get garbage
data = ''
r.feed(data)
result = {"channel": r.channel, "items": r.items}
newEtag = get_etag(f)
if newEtag: result["etag"] = newEtag
elif etag: result["etag"] = etag
newModified = get_modified(f)
if newModified: result["modified"] = newModified
elif modified: result["modified"] = modified
if hasattr(f, "url"):
result["url"] = f.url
if hasattr(f, "headers"):
result["headers"] = f.headers.dict
if hasattr(f, "status"):
result["status"] = f.status
elif hasattr(f, "url"):
result["status"] = 200
# get the xml encoding
if result.get('encoding', '') == '':
xmlheaderRe = re.compile('<\?.*encoding="(.*)".*\?>')
match = xmlheaderRe.match(data)
if match:
result['encoding'] = match.groups()[0].lower()
f.close()
return result
TEST_SUITE = ('http://www.pocketsoap.com/rssTests/rss1.0withModules.xml',
'http://www.pocketsoap.com/rssTests/rss1.0withModulesNoDefNS.xml',
'http://www.pocketsoap.com/rssTests/rss1.0withModulesNoDefNSLocalNameClash.xml',
'http://www.pocketsoap.com/rssTests/rss2.0noNSwithModules.xml',
'http://www.pocketsoap.com/rssTests/rss2.0noNSwithModulesLocalNameClash.xml',
'http://www.pocketsoap.com/rssTests/rss2.0NSwithModules.xml',
'http://www.pocketsoap.com/rssTests/rss2.0NSwithModulesNoDefNS.xml',
'http://www.pocketsoap.com/rssTests/rss2.0NSwithModulesNoDefNSLocalNameClash.xml')
if __name__ == '__main__':
import sys
if sys.argv[1:]:
urls = sys.argv[1:]
else:
urls = TEST_SUITE
from pprint import pprint
for url in urls:
print url
print
result = parse(url)
pprint(result)
print
"""
TODO
- textinput/textInput
- image
- author
- contributor
- comments
"""

218
others/shlex.py Normal file
View File

@ -0,0 +1,218 @@
"""A lexical analyzer class for simple shell-like syntaxes."""
# Module and documentation by Eric S. Raymond, 21 Dec 1998
# Input stacking and error message cleanup added by ESR, March 2000
# push_source() and pop_source() made explicit by ESR, January 2001.
import os.path
import sys
__all__ = ["shlex"]
class shlex:
"A lexical analyzer class for simple shell-like syntaxes."
def __init__(self, instream=None, infile=None):
if instream is not None:
self.instream = instream
self.infile = infile
else:
self.instream = sys.stdin
self.infile = None
self.commenters = '#'
self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')
self.whitespace = ' \t\r\n'
self.quotes = '\'"'
self.state = ' '
self.pushback = []
self.lineno = 1
self.debug = 0
self.token = ''
self.backslash = False
self.filestack = []
self.source = None
if self.debug:
print 'shlex: reading from %s, line %d' \
% (self.instream, self.lineno)
def push_token(self, tok):
"Push a token onto the stack popped by the get_token method"
if self.debug >= 1:
print "shlex: pushing token " + `tok`
self.pushback = [tok] + self.pushback
def push_source(self, newstream, newfile=None):
"Push an input source onto the lexer's input source stack."
self.filestack.insert(0, (self.infile, self.instream, self.lineno))
self.infile = newfile
self.instream = newstream
self.lineno = 1
if self.debug:
if newfile is not None:
print 'shlex: pushing to file %s' % (self.infile,)
else:
print 'shlex: pushing to stream %s' % (self.instream,)
def pop_source(self):
"Pop the input source stack."
self.instream.close()
(self.infile, self.instream, self.lineno) = self.filestack[0]
self.filestack = self.filestack[1:]
if self.debug:
print 'shlex: popping to %s, line %d' \
% (self.instream, self.lineno)
self.state = ' '
def get_token(self):
"Get a token from the input stream (or from stack if it's nonempty)"
if self.pushback:
tok = self.pushback[0]
self.pushback = self.pushback[1:]
if self.debug >= 1:
print "shlex: popping token " + `tok`
return tok
# No pushback. Get a token.
raw = self.read_token()
# Handle inclusions
while raw == self.source:
spec = self.sourcehook(self.read_token())
if spec:
(newfile, newstream) = spec
self.push_source(newstream, newfile)
raw = self.get_token()
# Maybe we got EOF instead?
while raw == "":
if len(self.filestack) == 0:
return ""
else:
self.pop_source()
raw = self.get_token()
# Neither inclusion nor EOF
if self.debug >= 1:
if raw:
print "shlex: token=" + `raw`
else:
print "shlex: token=EOF"
return raw
def read_token(self):
"Read a token from the input stream (no pushback or inclusions)"
while 1:
nextchar = self.instream.read(1)
if nextchar == '\n':
self.lineno = self.lineno + 1
if self.debug >= 3:
print "shlex: in state", repr(self.state), \
"I see character:", repr(nextchar)
if self.state is None:
self.token = '' # past end of file
break
elif self.state == ' ':
if not nextchar:
self.state = None # end of file
break
elif nextchar in self.whitespace:
if self.debug >= 2:
print "shlex: I see whitespace in whitespace state"
if self.token:
break # emit current token
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno = self.lineno + 1
elif nextchar in self.wordchars:
self.token = nextchar
self.state = 'a'
elif nextchar in self.quotes:
self.token = nextchar
self.state = nextchar
else:
self.token = nextchar
if self.token:
break # emit current token
else:
continue
elif self.state in self.quotes:
self.token = self.token + nextchar
if nextchar == '\\':
if self.backslash:
self.backslash = False
else:
self.backslash = True
else:
if not self.backslash and nextchar == self.state:
self.state = ' '
break
elif self.backslash:
self.backslash = False
elif not nextchar: # end of file
if self.debug >= 2:
print "shlex: I see EOF in quotes state"
# XXX what error should be raised here?
raise ValueError, "No closing quotation"
elif self.state == 'a':
if not nextchar:
self.state = None # end of file
break
elif nextchar in self.whitespace:
if self.debug >= 2:
print "shlex: I see whitespace in word state"
self.state = ' '
if self.token:
break # emit current token
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno = self.lineno + 1
elif nextchar in self.wordchars or nextchar in self.quotes:
self.token = self.token + nextchar
else:
self.pushback = [nextchar] + self.pushback
if self.debug >= 2:
print "shlex: I see punctuation in word state"
self.state = ' '
if self.token:
break # emit current token
else:
continue
result = self.token
self.token = ''
if self.debug > 1:
if result:
print "shlex: raw token=" + `result`
else:
print "shlex: raw token=EOF"
return result
def sourcehook(self, newfile):
"Hook called on a filename to be sourced."
if newfile[0] == '"':
newfile = newfile[1:-1]
# This implements cpp-like semantics for relative-path inclusion.
if type(self.infile) == type("") and not os.path.isabs(newfile):
newfile = os.path.join(os.path.dirname(self.infile), newfile)
return (newfile, open(newfile, "r"))
def error_leader(self, infile=None, lineno=None):
"Emit a C-compiler-like, Emacs-friendly error-message leader."
if infile is None:
infile = self.infile
if lineno is None:
lineno = self.lineno
return "\"%s\", line %d: " % (infile, lineno)
if __name__ == '__main__':
if len(sys.argv) == 1:
lexer = shlex()
else:
file = sys.argv[1]
lexer = shlex(open(file), file)
while 1:
tt = lexer.get_token()
if tt:
print "Token: " + repr(tt)
else:
break

785
others/unittest.py Normal file
View File

@ -0,0 +1,785 @@
#!/usr/bin/env python
'''
Python unit testing framework, based on Erich Gamma's JUnit and Kent Beck's
Smalltalk testing framework.
This module contains the core framework classes that form the basis of
specific test cases and suites (TestCase, TestSuite etc.), and also a
text-based utility class for running the tests and reporting the results
(TextTestRunner).
Simple usage:
import unittest
class IntegerArithmenticTestCase(unittest.TestCase):
def testAdd(self): ## test method names begin 'test*'
self.assertEquals((1 + 2), 3)
self.assertEquals(0 + 1, 1)
def testMultiply(self):
self.assertEquals((0 * 10), 0)
self.assertEquals((5 * 8), 40)
if __name__ == '__main__':
unittest.main()
Further information is available in the bundled documentation, and from
http://pyunit.sourceforge.net/
Copyright (c) 1999, 2000, 2001 Steve Purcell
This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
'''
__author__ = "Steve Purcell"
__email__ = "stephen_purcell at yahoo dot com"
__version__ = "#Revision: 1.46 $"[11:-2]
import time
import sys
import traceback
import string
import os
import types
###
# Globals
###
asserts = 0
##############################################################################
# Test framework core
##############################################################################
# All classes defined herein are 'new-style' classes, allowing use of 'super()'
__metaclass__ = type
def _strclass(cls):
return "%s.%s" % (cls.__module__, cls.__name__)
class TestResult:
"""Holder for test result information.
Test results are automatically managed by the TestCase and TestSuite
classes, and do not need to be explicitly manipulated by writers of tests.
Each instance holds the total number of tests run, and collections of
failures and errors that occurred among those test runs. The collections
contain tuples of (testcase, exceptioninfo), where exceptioninfo is the
formatted traceback of the error that occurred.
"""
def __init__(self):
self.failures = []
self.errors = []
self.testsRun = 0
self.shouldStop = 0
def startTest(self, test):
"Called when the given test is about to be run"
self.testsRun = self.testsRun + 1
def stopTest(self, test):
"Called when the given test has been run"
pass
def addError(self, test, err):
"""Called when an error has occurred. 'err' is a tuple of values as
returned by sys.exc_info().
"""
self.errors.append((test, self._exc_info_to_string(err)))
def addFailure(self, test, err):
"""Called when an error has occurred. 'err' is a tuple of values as
returned by sys.exc_info()."""
self.failures.append((test, self._exc_info_to_string(err)))
def addSuccess(self, test):
"Called when a test has completed successfully"
pass
def wasSuccessful(self):
"Tells whether or not this result was a success"
return len(self.failures) == len(self.errors) == 0
def stop(self):
"Indicates that the tests should be aborted"
self.shouldStop = 1
def _exc_info_to_string(self, err):
"""Converts a sys.exc_info()-style tuple of values into a string."""
return string.join(traceback.format_exception(*err), '')
def __repr__(self):
return "<%s run=%i errors=%i failures=%i>" % \
(_strclass(self.__class__), self.testsRun, len(self.errors),
len(self.failures))
class TestCase:
"""A class whose instances are single test cases.
By default, the test code itself should be placed in a method named
'runTest'.
If the fixture may be used for many test cases, create as
many test methods as are needed. When instantiating such a TestCase
subclass, specify in the constructor arguments the name of the test method
that the instance is to execute.
Test authors should subclass TestCase for their own tests. Construction
and deconstruction of the test's environment ('fixture') can be
implemented by overriding the 'setUp' and 'tearDown' methods respectively.
If it is necessary to override the __init__ method, the base class
__init__ method must always be called. It is important that subclasses
should not change the signature of their __init__ method, since instances
of the classes are instantiated automatically by parts of the framework
in order to be run.
"""
# This attribute determines which exception will be raised when
# the instance's assertion methods fail; test methods raising this
# exception will be deemed to have 'failed' rather than 'errored'
failureException = AssertionError
def __init__(self, methodName='runTest'):
"""Create an instance of the class that will use the named test
method when executed. Raises a ValueError if the instance does
not have a method with the specified name.
"""
try:
self.__testMethodName = methodName
testMethod = getattr(self, methodName)
self.__testMethodDoc = testMethod.__doc__
except AttributeError:
raise ValueError, "no such test method in %s: %s" % \
(self.__class__, methodName)
def setUp(self):
"Hook method for setting up the test fixture before exercising it."
pass
def tearDown(self):
"Hook method for deconstructing the test fixture after testing it."
pass
def countTestCases(self):
return 1
def defaultTestResult(self):
return TestResult()
def shortDescription(self):
"""Returns a one-line description of the test, or None if no
description has been provided.
The default implementation of this method returns the first line of
the specified test method's docstring.
"""
doc = self.__testMethodDoc
return doc and string.strip(string.split(doc, "\n")[0]) or None
def id(self):
return "%s.%s" % (_strclass(self.__class__), self.__testMethodName)
def __str__(self):
return "%s (%s)" % (self.__testMethodName, _strclass(self.__class__))
def __repr__(self):
return "<%s testMethod=%s>" % \
(_strclass(self.__class__), self.__testMethodName)
def run(self, result=None):
return self(result)
def __call__(self, result=None):
if result is None: result = self.defaultTestResult()
result.startTest(self)
testMethod = getattr(self, self.__testMethodName)
try:
try:
x = self.setUp()
if x:
print 'skipped (%s)' % x
return
except KeyboardInterrupt:
raise
except:
result.addError(self, self.__exc_info())
return
ok = 0
try:
testMethod()
ok = 1
except self.failureException, e:
result.addFailure(self, self.__exc_info())
except KeyboardInterrupt:
raise
except:
result.addError(self, self.__exc_info())
try:
self.tearDown()
except KeyboardInterrupt:
raise
except:
result.addError(self, self.__exc_info())
ok = 0
if ok: result.addSuccess(self)
finally:
result.stopTest(self)
def debug(self):
"""Run the test without collecting errors in a TestResult"""
self.setUp()
getattr(self, self.__testMethodName)()
self.tearDown()
def __exc_info(self):
"""Return a version of sys.exc_info() with the traceback frame
minimised; usually the top level of the traceback frame is not
needed.
"""
exctype, excvalue, tb = sys.exc_info()
if sys.platform[:4] == 'java': ## tracebacks look different in Jython
return (exctype, excvalue, tb)
newtb = tb.tb_next
if newtb is None:
return (exctype, excvalue, tb)
return (exctype, excvalue, newtb)
def _fail(self, msg):
"""Underlying implementation of failure."""
raise self.failureException, msg
def fail(self, msg=None):
"""Fail immediately, with the given message."""
global asserts
asserts += 1
self._fail(msg)
def failIf(self, expr, msg=None):
"Fail the test if the expression is true."
global asserts
asserts += 1
if expr: self._fail(msg)
def failUnless(self, expr, msg=None):
"""Fail the test unless the expression is true."""
global asserts
asserts += 1
if not expr: self._fail(msg)
def failUnlessRaises(self, excClass, callableObj, *args, **kwargs):
"""Fail unless an exception of class excClass is thrown
by callableObj when invoked with arguments args and keyword
arguments kwargs. If a different type of exception is
thrown, it will not be caught, and the test case will be
deemed to have suffered an error, exactly as for an
unexpected exception.
"""
global asserts
asserts += 1
try:
callableObj(*args, **kwargs)
except excClass:
return
else:
if hasattr(excClass,'__name__'): excName = excClass.__name__
else: excName = str(excClass)
raise self._fail(excName)
def failUnlessEqual(self, first, second, msg=None):
"""Fail if the two objects are unequal as determined by the '=='
operator.
"""
global asserts
asserts += 1
if not first == second:
self._fail(msg or '%s != %s' % (`first`, `second`))
def failIfEqual(self, first, second, msg=None):
"""Fail if the two objects are equal as determined by the '=='
operator.
"""
global asserts
asserts += 1
if first == second:
self._fail(msg or '%s == %s' % (`first`, `second`))
def failUnlessAlmostEqual(self, first, second, places=7, msg=None):
"""Fail if the two objects are unequal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero.
Note that decimal places (from zero) is usually not the same
as significant digits (measured from the most signficant digit).
"""
global asserts
asserts += 1
if round(second-first, places) != 0:
self._fail(msg or '%s != %s within %s places' % \
(`first`, `second`, `places`))
def failIfAlmostEqual(self, first, second, places=7, msg=None):
"""Fail if the two objects are equal as determined by their
difference rounded to the given number of decimal places
(default 7) and comparing to zero.
Note that decimal places (from zero) is usually not the same
as significant digits (measured from the most signficant digit).
"""
global asserts
asserts += 1
if round(second-first, places) == 0:
self._fail(msg or '%s == %s within %s places' % \
(`first`, `second`, `places`))
assertEqual = assertEquals = failUnlessEqual
assertNotEqual = assertNotEquals = failIfEqual
assertAlmostEqual = assertAlmostEquals = failUnlessAlmostEqual
assertNotAlmostEqual = assertNotAlmostEquals = failIfAlmostEqual
assertRaises = failUnlessRaises
assert_ = failUnless
class TestSuite:
"""A test suite is a composite test consisting of a number of TestCases.
For use, create an instance of TestSuite, then add test case instances.
When all tests have been added, the suite can be passed to a test
runner, such as TextTestRunner. It will run the individual test cases
in the order in which they were added, aggregating the results. When
subclassing, do not forget to call the base class constructor.
"""
def __init__(self, tests=()):
self._tests = []
self.addTests(tests)
def __repr__(self):
return "<%s tests=%s>" % (_strclass(self.__class__), self._tests)
__str__ = __repr__
def countTestCases(self):
cases = 0
for test in self._tests:
cases = cases + test.countTestCases()
return cases
def addTest(self, test):
self._tests.append(test)
def addTests(self, tests):
for test in tests:
self.addTest(test)
def run(self, result):
return self(result)
def __call__(self, result):
for test in self._tests:
if result.shouldStop:
break
test(result)
return result
def debug(self):
"""Run the tests without collecting errors in a TestResult"""
for test in self._tests: test.debug()
class FunctionTestCase(TestCase):
"""A test case that wraps a test function.
This is useful for slipping pre-existing test functions into the
PyUnit framework. Optionally, set-up and tidy-up functions can be
supplied. As with TestCase, the tidy-up ('tearDown') function will
always be called if the set-up ('setUp') function ran successfully.
"""
def __init__(self, testFunc, setUp=None, tearDown=None,
description=None):
TestCase.__init__(self)
self.__setUpFunc = setUp
self.__tearDownFunc = tearDown
self.__testFunc = testFunc
self.__description = description
def setUp(self):
if self.__setUpFunc is not None:
self.__setUpFunc()
def tearDown(self):
if self.__tearDownFunc is not None:
self.__tearDownFunc()
def runTest(self):
self.__testFunc()
def id(self):
return self.__testFunc.__name__
def __str__(self):
return "%s (%s)" % (_strclass(self.__class__), self.__testFunc.__name__)
def __repr__(self):
return "<%s testFunc=%s>" % (_strclass(self.__class__), self.__testFunc)
def shortDescription(self):
if self.__description is not None: return self.__description
doc = self.__testFunc.__doc__
return doc and string.strip(string.split(doc, "\n")[0]) or None
##############################################################################
# Locating and loading tests
##############################################################################
class TestLoader:
"""This class is responsible for loading tests according to various
criteria and returning them wrapped in a Test
"""
testMethodPrefix = 'test'
sortTestMethodsUsing = cmp
suiteClass = TestSuite
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
return self.suiteClass(map(testCaseClass,
self.getTestCaseNames(testCaseClass)))
def loadTestsFromModule(self, module):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, TestCase)):
tests.append(self.loadTestsFromTestCase(obj))
return self.suiteClass(tests)
def loadTestsFromName(self, name, module=None):
"""Return a suite of all tests cases given a string specifier.
The name may resolve either to a module, a test case class, a
test method within a test case class, or a callable object which
returns a TestCase or TestSuite instance.
The method optionally resolves the names relative to a given module.
"""
parts = string.split(name, '.')
if module is None:
if not parts:
raise ValueError, "incomplete test name: %s" % name
else:
parts_copy = parts[:]
while parts_copy:
try:
module = __import__(string.join(parts_copy,'.'))
break
except ImportError:
del parts_copy[-1]
if not parts_copy: raise
parts = parts[1:]
obj = module
for part in parts:
obj = getattr(obj, part)
import unittest
if type(obj) == types.ModuleType:
return self.loadTestsFromModule(obj)
elif (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, unittest.TestCase)):
return self.loadTestsFromTestCase(obj)
elif type(obj) == types.UnboundMethodType:
return obj.im_class(obj.__name__)
elif callable(obj):
test = obj()
if not isinstance(test, unittest.TestCase) and \
not isinstance(test, unittest.TestSuite):
raise ValueError, \
"calling %s returned %s, not a test" % (obj,test)
return test
else:
raise ValueError, "don't know how to make test from: %s" % obj
def loadTestsFromNames(self, names, module=None):
"""Return a suite of all tests cases found using the given sequence
of string specifiers. See 'loadTestsFromName()'.
"""
suites = []
for name in names:
suites.append(self.loadTestsFromName(name, module))
return self.suiteClass(suites)
def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
testFnNames = filter(lambda n,p=self.testMethodPrefix: n[:len(p)] == p,
dir(testCaseClass))
for baseclass in testCaseClass.__bases__:
for testFnName in self.getTestCaseNames(baseclass):
if testFnName not in testFnNames: # handle overridden methods
testFnNames.append(testFnName)
if self.sortTestMethodsUsing:
testFnNames.sort(self.sortTestMethodsUsing)
return testFnNames
defaultTestLoader = TestLoader()
##############################################################################
# Patches for old functions: these functions should be considered obsolete
##############################################################################
def _makeLoader(prefix, sortUsing, suiteClass=None):
loader = TestLoader()
loader.sortTestMethodsUsing = sortUsing
loader.testMethodPrefix = prefix
if suiteClass: loader.suiteClass = suiteClass
return loader
def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp):
return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass)
def makeSuite(testCaseClass, prefix='test', sortUsing=cmp, suiteClass=TestSuite):
return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass)
def findTestCases(module, prefix='test', sortUsing=cmp, suiteClass=TestSuite):
return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromModule(module)
##############################################################################
# Text UI
##############################################################################
class _WritelnDecorator:
"""Used to decorate file-like objects with a handy 'writeln' method"""
def __init__(self,stream):
self.stream = stream
def __getattr__(self, attr):
return getattr(self.stream,attr)
def writeln(self, *args):
if args: self.write(*args)
self.write('\n') # text-mode streams translate to \r\n if needed
class _TextTestResult(TestResult):
"""A test result class that can print formatted text results to a stream.
Used by TextTestRunner.
"""
separator1 = '=' * 70
separator2 = '-' * 70
def __init__(self, stream, descriptions, verbosity):
TestResult.__init__(self)
self.stream = stream
self.showAll = verbosity > 1
self.dots = verbosity == 1
self.descriptions = descriptions
def getDescription(self, test):
if self.descriptions:
return test.shortDescription() or str(test)
else:
return str(test)
def startTest(self, test):
TestResult.startTest(self, test)
if self.showAll:
self.stream.write(self.getDescription(test))
self.stream.write(" ... ")
def addSuccess(self, test):
TestResult.addSuccess(self, test)
if self.showAll:
self.stream.writeln("ok")
elif self.dots:
self.stream.write('.')
def addError(self, test, err):
TestResult.addError(self, test, err)
if self.showAll:
self.stream.writeln("ERROR")
elif self.dots:
self.stream.write('E')
def addFailure(self, test, err):
TestResult.addFailure(self, test, err)
if self.showAll:
self.stream.writeln("FAIL")
elif self.dots:
self.stream.write('F')
def printErrors(self):
if self.dots or self.showAll:
self.stream.writeln()
self.printErrorList('ERROR', self.errors)
self.printErrorList('FAIL', self.failures)
def printErrorList(self, flavour, errors):
for test, err in errors:
self.stream.writeln(self.separator1)
self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
self.stream.writeln(self.separator2)
self.stream.writeln("%s" % err)
class TextTestRunner:
"""A test runner class that displays results in textual form.
It prints out the names of tests as they are run, errors as they
occur, and a summary of the results at the end of the test run.
"""
def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1):
self.stream = _WritelnDecorator(stream)
self.descriptions = descriptions
self.verbosity = verbosity
def _makeResult(self):
return _TextTestResult(self.stream, self.descriptions, self.verbosity)
def run(self, test):
"Run the given test case or test suite."
result = self._makeResult()
startTime = time.time()
test(result)
stopTime = time.time()
timeTaken = float(stopTime - startTime)
result.printErrors()
self.stream.writeln(result.separator2)
run = result.testsRun
self.stream.writeln("Ran %d test%s in %.3fs" %
(run, run != 1 and "s" or "", timeTaken))
self.stream.writeln()
if not result.wasSuccessful():
self.stream.write("FAILED (")
failed, errored = map(len, (result.failures, result.errors))
if failed:
self.stream.write("failures=%d" % failed)
if errored:
if failed: self.stream.write(", ")
self.stream.write("errors=%d" % errored)
self.stream.writeln(")")
else:
self.stream.writeln("OK")
return result
##############################################################################
# Facilities for running tests from the command line
##############################################################################
class TestProgram:
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
USAGE = """\
Usage: %(progName)s [options] [test] [...]
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
Examples:
%(progName)s - run default set of tests
%(progName)s MyTestSuite - run suite 'MyTestSuite'
%(progName)s MyTestCase.testSomething - run MyTestCase.testSomething
%(progName)s MyTestCase - run all 'test*' test methods
in MyTestCase
"""
def __init__(self, module='__main__', defaultTest=None,
argv=None, testRunner=None, testLoader=defaultTestLoader):
if type(module) == type(''):
self.module = __import__(module)
for part in string.split(module,'.')[1:]:
self.module = getattr(self.module, part)
else:
self.module = module
if argv is None:
argv = sys.argv
self.verbosity = 1
self.defaultTest = defaultTest
self.testRunner = testRunner
self.testLoader = testLoader
self.progName = os.path.basename(argv[0])
self.parseArgs(argv)
self.runTests()
def usageExit(self, msg=None):
if msg: print msg
print self.USAGE % self.__dict__
sys.exit(2)
def parseArgs(self, argv):
import getopt
try:
options, args = getopt.getopt(argv[1:], 'hHvq',
['help','verbose','quiet'])
for opt, value in options:
if opt in ('-h','-H','--help'):
self.usageExit()
if opt in ('-q','--quiet'):
self.verbosity = 0
if opt in ('-v','--verbose'):
self.verbosity = 2
if len(args) == 0 and self.defaultTest is None:
self.test = self.testLoader.loadTestsFromModule(self.module)
return
if len(args) > 0:
self.testNames = args
else:
self.testNames = (self.defaultTest,)
self.createTests()
except getopt.error, msg:
self.usageExit(msg)
def createTests(self):
self.test = self.testLoader.loadTestsFromNames(self.testNames,
self.module)
def runTests(self):
if self.testRunner is None:
self.testRunner = TextTestRunner(verbosity=self.verbosity)
result = self.testRunner.run(self.test)
sys.exit(not result.wasSuccessful())
main = TestProgram
##############################################################################
# Executing this module from the command line
##############################################################################
if __name__ == "__main__":
main(module=None)

1208
others/urllib2.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
{
"devDependencies": {
"@aminda/global-prettier-config": "2025.15.0",
"@prettier/plugin-ruby": "4.0.4",
"@prettier/plugin-xml": "3.4.1",
"corepack": "latest",
"prettier": "3.5.3",
"prettier-plugin-nginx": "1.0.3",
"prettier-plugin-sh": "0.17.1",
"prettier-plugin-toml": "2.0.4"
},
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971",
"prettier": "@aminda/global-prettier-config"
}

303
plugins/Alias.py Normal file
View File

@ -0,0 +1,303 @@
#!/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.
###
"""
Allows aliases for other commands.
"""
__revision__ = "$Id$"
import plugins
import os
import re
import sets
import conf
import utils
import privmsgs
import registry
import callbacks
import structures
import unpreserve
class AliasError(Exception):
pass
class RecursiveAlias(AliasError):
pass
def findAliasCommand(s, alias):
s = re.escape(s)
r = re.compile(r'(?:(^|\[)\s*\b%s\b|\|\s*\b%s\b)' % (s, s))
return bool(r.search(alias))
dollarRe = re.compile(r'\$(\d+)')
def findBiggestDollar(alias):
dollars = dollarRe.findall(alias)
dollars = map(int, dollars)
dollars.sort()
if dollars:
return dollars[-1]
else:
return 0
atRe = re.compile(r'@(\d+)')
def findBiggestAt(alias):
ats = atRe.findall(alias)
ats = map(int, ats)
ats.sort()
if ats:
return ats[-1]
else:
return 0
def makeNewAlias(name, alias):
original = alias
if findAliasCommand(name, alias):
raise RecursiveAlias
biggestDollar = findBiggestDollar(original)
biggestAt = findBiggestAt(original)
wildcard = '$*' in original
if biggestAt and wildcard:
raise AliasError, 'Can\'t mix $* and optional args (@1, etc.)'
if original.count('$*') > 1:
raise AliasError, 'There can be only one $* in an alias.'
testTokens = callbacks.tokenize(original)
if testTokens and isinstance(testTokens[0], list):
raise AliasError, 'Commands may not be the result of nesting.'
def f(self, irc, msg, args):
alias = original.replace('$nick', msg.nick)
if '$channel' in original:
channel = privmsgs.getChannel(msg, args)
alias = alias.replace('$channel', channel)
tokens = callbacks.tokenize(alias)
if not wildcard and biggestDollar or biggestAt:
args = privmsgs.getArgs(args,
required=biggestDollar,
optional=biggestAt)
# Gotta have a mutable sequence (for replace).
if biggestDollar + biggestAt == 1: # We got a string, no tuple.
args = [args]
def regexpReplace(m):
idx = int(m.group(1))
return args[idx-1]
def replace(tokens, replacer):
for (i, token) in enumerate(tokens):
if isinstance(token, list):
replace(token, replacer)
else:
tokens[i] = replacer(token)
replace(tokens, lambda s: dollarRe.sub(regexpReplace, s))
if biggestAt:
assert not wildcard
args = args[biggestDollar:]
replace(tokens, lambda s: atRe.sub(regexpReplace, s))
if wildcard:
assert not biggestAt
# Gotta remove the things that have already been subbed in.
i = biggestDollar
while i:
args.pop(0)
i -= 1
def everythingReplace(tokens):
for (i, token) in enumerate(tokens):
if isinstance(token, list):
if everythingReplace(token):
return
if token == '$*':
tokens[i:i+1] = args
return True
elif '$*' in token:
tokens[i] = token.replace('$*', ' '.join(args))
return True
return False
everythingReplace(tokens)
Owner = irc.getCallback('Owner')
Owner.disambiguate(irc, tokens)
self.Proxy(irc.irc, msg, tokens)
doc ='<an alias, %s>\n\nAlias for %r' % \
(utils.nItems('argument', biggestDollar), alias)
f = utils.changeFunctionName(f, name, doc)
return f
conf.registerPlugin('Alias')
conf.registerGroup(conf.supybot.plugins.Alias, 'aliases')
filename = os.path.join(conf.supybot.directories.conf(), 'aliases.conf')
class Alias(callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
# Schema: {alias: [command, locked]}
self.aliases = {}
group = conf.supybot.plugins.Alias.aliases
for (name, alias) in registry._cache.iteritems():
name = name.lower()
if name.startswith('supybot.plugins.alias.aliases.'):
name = name[len('supybot.plugins.alias.aliases.'):]
if '.' in name:
continue
conf.registerGlobalValue(group, name, registry.String('', ''))
conf.registerGlobalValue(group.get(name), 'locked',
registry.Boolean(False, ''))
for (name, value) in group.getValues(fullNames=False):
name = name.lower() # Just in case.
command = value()
locked = value.locked()
self.aliases[name] = [command, locked]
def __call__(self, irc, msg):
# Adding the aliases requires an Irc. So the first time we get called
# with an Irc, we add our aliases and then delete ourselves :)
for (alias, (command, locked)) in self.aliases.iteritems():
try:
self.addAlias(irc, alias, command, locked)
except Exception, e:
self.log.exception('Exception when trying to add alias %s. '
'Removing from the Alias database.' % alias)
del self.aliases[alias]
del self.__class__.__call__
callbacks.Privmsg.__call__(self, irc, msg)
def lock(self, irc, msg, args):
"""<alias>
Locks an alias so that no one else can change it.
"""
name = privmsgs.getArgs(args)
name = callbacks.canonicalName(name)
if hasattr(self, name) and self.isCommand(name):
self.aliases[name][1] = True
conf.supybot.plugins.Alias.aliases.get(name).locked.setValue(True)
irc.replySuccess()
else:
irc.error('There is no such alias.')
lock = privmsgs.checkCapability(lock, 'admin')
def unlock(self, irc, msg, args):
"""<alias>
Unlocks an alias so that people can define new aliases over it.
"""
name = privmsgs.getArgs(args)
name = callbacks.canonicalName(name)
if hasattr(self, name) and self.isCommand(name):
self.aliases[name][1] = False
conf.supybot.plugins.Alias.aliases.get(name).locked.setValue(False)
irc.replySuccess()
else:
irc.error('There is no such alias.')
unlock = privmsgs.checkCapability(unlock, 'admin')
_invalidCharsRe = re.compile(r'[\[\]\s]')
def addAlias(self, irc, name, alias, lock=False):
if self._invalidCharsRe.search(name):
raise AliasError, 'Names cannot contain spaces or square brackets.'
if conf.supybot.pipeSyntax() and '|' in name:
raise AliasError, 'Names cannot contain pipes.'
if irc.getCallback(name):
raise AliasError, 'Names cannot coincide with names of plugins.'
realName = callbacks.canonicalName(name)
if name != realName:
s = 'That name isn\'t valid. Try %r instead.' % realName
raise AliasError, s
name = realName
cbs = callbacks.findCallbackForCommand(irc, name)
if self in cbs:
if hasattr(self, realName) and realName not in self.aliases:
s = 'You can\'t overwrite commands in this plugin.'
raise AliasError, s
if name in self.aliases:
(currentAlias, locked) = self.aliases[name]
if locked and currentAlias != alias:
raise AliasError, 'Alias %r is locked.' % name
try:
f = makeNewAlias(name, alias)
except RecursiveAlias:
raise AliasError, 'You can\'t define a recursive alias.'
if name in self.aliases:
# We gotta remove it so its value gets updated.
conf.supybot.plugins.Alias.aliases.unregister(name)
conf.supybot.plugins.Alias.aliases.register(name,
registry.String(alias, ''))
conf.supybot.plugins.Alias.aliases.get(name).register('locked',
registry.Boolean(lock, ''))
setattr(self.__class__, name, f)
self.aliases[name] = [alias, lock]
def removeAlias(self, name, evenIfLocked=False):
name = callbacks.canonicalName(name)
if hasattr(self, name) and self.isCommand(name):
if evenIfLocked or not self.aliases[name][1]:
delattr(self.__class__, name)
del self.aliases[name]
conf.supybot.plugins.Alias.aliases.unregister(name)
else:
raise AliasError, 'That alias is locked.'
else:
raise AliasError, 'There is no such alias.'
def add(self, irc, msg, args):
"""<name> <alias>
Defines an alias <name> that executes <alias>. The <alias>
should be in the standard "command argument [nestedcommand argument]"
format. $[digit] (like $1, $2, etc.) can be used to represent
arguments to the alias; they'll be filled with the first, second, etc.
arguments. @1, @2 can be used for optional arguments. $* simply
means "all remaining arguments," and cannot be combined with optional
arguments.
"""
(name, alias) = privmsgs.getArgs(args, required=2)
try:
self.addAlias(irc, name, alias)
self.log.info('Adding alias %r for %r (from %s)' %
(name, alias, msg.prefix))
irc.replySuccess()
except AliasError, e:
irc.error(str(e))
def remove(self, irc, msg, args):
"""<name>
Removes the given alias, if unlocked.
"""
name = privmsgs.getArgs(args)
try:
self.removeAlias(name)
self.log.info('Removing alias %r (from %s)' % (name, msg.prefix))
irc.replySuccess()
except AliasError, e:
irc.error(str(e))
Class = Alias
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

596
plugins/Amazon.py Normal file
View File

@ -0,0 +1,596 @@
#!/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.
###
"""
Amazon module, to use Amazon's Web Services.
"""
__revision__ = "$Id$"
import getopt
import plugins
import amazon
import registry
import conf
import utils
import ircutils
import privmsgs
import callbacks
def configure(advanced):
from questions import output, expect, anything, something, yn
output('To use Amazon\'s Web Services, you must have a license key.')
if yn('Do you have a license key?'):
key = anything('What is it?')
conf.registerPlugin('Amazon', True)
conf.supybot.plugins.Amazon.licenseKey.set(key)
else:
output("""You'll need to get a key before you can use this plugin.
You can apply for a key at
http://www.amazon.com/webservices/""")
class LicenseKey(registry.String):
def set(self, s):
# In case we decide we need to recover
original = getattr(self, 'value', self.default)
registry.String.set(self, s)
if self.value:
amazon.setLicense(self.value)
conf.registerPlugin('Amazon')
conf.registerChannelValue(conf.supybot.plugins.Amazon, 'bold',
registry.Boolean(True, """Determines whether the results are bolded."""))
conf.registerGlobalValue(conf.supybot.plugins.Amazon, 'licenseKey',
LicenseKey('', """Sets the license key for using Amazon Web Services.
Must be set before any other commands in the plugin are used.""",
private=True))
conf.registerChannelValue(conf.supybot.plugins.Amazon, 'linkSnarfer',
registry.Boolean(False, """Determines whether the bot will reply to
Amazon.com URLs in the channel with a description of the item at the
URL."""))
class Amazon(callbacks.PrivmsgCommandAndRegexp):
threaded = True
regexps = ['amzSnarfer']
def callCommand(self, method, irc, msg, *L, **kwargs):
try:
callbacks.PrivmsgCommandAndRegexp.callCommand(self, method, irc, msg, *L, **kwargs)
except amazon.NoLicenseKey, e:
irc.error('You must have a free Amazon web services license key '
'in order to use this command. You can get one at '
'<http://www.amazon.com/webservices>. Once you have '
'one, you can set it with the command '
'"config supybot.plugins.Amazon.licensekey <key>".')
def _genResults(self, reply, attribs, items, url, bold, bold_item):
results = {}
res = []
if isinstance(items, amazon.Bag):
items = [items]
for item in items:
try:
for k,v in attribs.iteritems():
results[v] = getattr(item, k, 'unknown')
if isinstance(results[v], amazon.Bag):
results[v] = getattr(results[v], k[:-1], 'unknown')
if not isinstance(results[v], basestring):
results[v] = utils.commaAndify(results[v])
if bold_item in results:
if bold:
results[bold_item] = ircutils.bold(results[bold_item])
else:
results[bold_item] = '"%s"' % results[bold_item]
if not url:
results['url'] = ''
else:
results['url'] = ' <%s>' % results['url']
s = reply % results
s.encode('utf-8')
res.append(str(s))
except amazon.AmazonError, e:
self.log.warning(str(e))
except UnicodeEncodeError, e:
self.log.warning(str(e))
return res
def isbn(self, irc, msg, args):
"""[--url] <isbn>
Returns the book matching the given ISBN number. If --url is
specified, a link to amazon.com's page for the book will also be
returned.
"""
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', opts)
url = False
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
isbn = privmsgs.getArgs(rest)
isbn = isbn.replace('-', '').replace(' ', '')
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s, written by %(author)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
try:
book = amazon.searchByKeyword(isbn)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, book, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No book was found with that ISBN.')
def books(self, irc, msg, args):
"""[--url] <keywords>
Returns the books matching the given <keywords> search. If --url is
specified, a link to amazon.com's page for the book will also be
returned.
"""
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', opts)
url = False
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
keyword = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s, written by %(author)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
try:
books = amazon.searchByKeyword(keyword)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, books, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No books were found with that keyword search.')
def videos(self, irc, msg, args):
"""[--url] [--{dvd,vhs}] <keywords>
Returns the videos matching the given <keyword> search. If --url is
specified, a link to amazon.com's page for the video will also be
returned. Search defaults to using --dvd.
"""
opts = ['url']
products = ['dvd', 'vhs']
(optlist, rest) = getopt.getopt(args, '', opts + products)
url = False
product = 'dvd'
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
if option in products:
product = option
keyword = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), rated %(mpaa)s; released ' \
'%(date)s; published by %(publisher)s; price: %(price)s%(url)s'
try:
videos = amazon.searchByKeyword(keyword, product_line=product)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, videos, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No videos were found with that keyword search.')
def asin(self, irc, msg, args):
"""[--url] <asin>
Returns the item matching the given ASIN number. If --url is
specified, a link to amazon.com's page for the item will also be
returned.
"""
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', opts)
url = False
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
asin = privmsgs.getArgs(rest)
asin = asin.replace('-', '').replace(' ', '')
attribs = {'ProductName' : 'title',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s; price: %(price)s%(url)s'
try:
item = amazon.searchByASIN(asin)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, item, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No item was found with that ASIN.')
def upc(self, irc, msg, args):
"""[--url] <upc>
Returns the item matching the given UPC number. If --url is
specified, a link to amazon.com's page for the item will also be
returned. Only items in the following categories may be found via upc
search: music, classical, software, dvd, video, vhs, electronics,
pc-hardware, and photo.
"""
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', opts)
url = False
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
upc = privmsgs.getArgs(rest)
upc = upc.replace('-', '').replace(' ', '')
attribs = {'ProductName' : 'title',
'Manufacturer' : 'manufacturer',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s %(manufacturer)s; price: %(price)s%(url)s'
try:
item = amazon.searchByUPC(upc)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, item, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No item was found with that UPC.')
def author(self, irc, msg, args):
"""[--url] <author>
Returns a list of books written by the given author. If --url is
specified, a link to amazon.com's page for the book will also be
returned.
"""
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', opts)
url = False
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
author = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s, written by %(author)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
try:
books = amazon.searchByAuthor(author)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, books, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No books were found by that author.')
# FIXME: Until I get a *good* list of categories (ones that actually work),
# these commands will remain unavailable
'''
_textToNode = {'dvds':'130', 'magazines':'599872', 'music':'301668',
'software':'491286', 'vhs':'404272', 'kitchen':'491864',
'video games':'471280', 'toys':'491290', 'camera':'502394',
'outdoor':'468250', 'computers':'565118', 'tools':'468240',
'electronics':'172282'
}
def categories(self, irc, msg, args):
"""takes no arguments
Returns a list of valid categories to use with the bestsellers
commands.
"""
cats = self._textToNode.keys()
cats.sort()
irc.reply(utils.commaAndify(cats))
def bestsellers(self, irc, msg, args):
"""[--url] <category>
Returns a list of best selling items in <category>. The 'categories'
command will return a list of the available categores. If --url
is specified, a link to amazon.com's page for the item will also be
returned.
"""
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', opts)
url = False
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
category = privmsgs.getArgs(rest).lower()
if category not in self._textToNode:
irc.error('An invalid category was specified. The categories'
' command will return a list of valid categories')
return
category = self._textToNode[category]
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'URL' : 'url'
}
s = '"%(title)s", from %(publisher)s.%(url)s'
try:
#self.log.warning(category)
items = amazon.browseBestSellers(category)
#self.log.warning(items)
res = self._genResults(s, attribs, items, url)
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No items were found on that best seller list.')
'''
def artist(self, irc, msg, args):
"""[--url] [--{music,classical}] <artist>
Returns a list of items by the given artist. If --url is specified, a
link to amazon.com's page for the match will also be returned. The
search defaults to using --music.
"""
products = ['music', 'classical']
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', products + opts)
url = False
product = ''
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
if option in products:
product = option
product = product or 'music'
artist = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Artists' : 'artist',
'Media' : 'media',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), by %(artist)s; published by ' \
'%(publisher)s; price: %(price)s%(url)s'
try:
items = amazon.searchByArtist(artist, product_line=product)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, items, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No items were found by that artist.')
def actor(self, irc, msg, args):
"""[--url] [--{dvd,vhs,video}] <actor>
Returns a list of items starring the given actor. If --url is
specified, a link to amazon.com's page for the match will also be
returned. The search defaults to using --dvd.
"""
products = ['dvd', 'video', 'vhs']
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', products + opts)
url = False
product = ''
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
if option in products:
product = option
product = product or 'dvd'
actor = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), rated %(mpaa)s; released ' \
'%(date)s; published by %(publisher)s; price: %(price)s%(url)s'
try:
items = amazon.searchByActor(actor, product_line=product)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, items, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No items were found starring that actor.')
def director(self, irc, msg, args):
"""[--url] [--{dvd,vhs,video}] <director>
Returns a list of items by the given director. If --url is
specified, a link to amazon.com's page for the match will also be
returned. The search defaults to using --dvd.
"""
products = ['dvd', 'video', 'vhs']
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', products + opts)
url = False
product = ''
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
if option in products:
product = option
product = product or 'dvd'
director = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s (%(media)s), rated %(mpaa)s; released ' \
'%(date)s; published by %(publisher)s; price: %(price)s%(url)s'
try:
items = amazon.searchByDirector(director, product_line=product)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, items, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No items were found by that director.')
def manufacturer(self, irc, msg, args):
""" [--url] \
[--{pc-hardware,kitchen,electronics,videogames,software,photo}] \
<manufacturer>
Returns a list of items by the given manufacturer. If --url is
specified, a link to amazon.com's page for the match will also be
returned. The search defaults to using --pc-hardware.
"""
products = ['electronics', 'kitchen', 'videogames', 'software',
'photo', 'pc-hardware']
opts = ['url']
(optlist, rest) = getopt.getopt(args, '', products + opts)
url = False
product = ''
for (option, argument) in optlist:
option = option.lstrip('-')
if option == 'url':
url = True
if option in products:
product = option
product = product or 'pc-hardware'
manufacturer = privmsgs.getArgs(rest)
attribs = {'ProductName' : 'title',
'OurPrice' : 'price',
'URL' : 'url'
}
s = '%(title)s; price: %(price)s%(url)s'
try:
items = amazon.searchByManufacturer(manufacturer,
product_line=product)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, items, url, bold, 'title')
if res:
irc.reply(utils.commaAndify(res))
return
except amazon.AmazonError, e:
pass
irc.error('No items were found by that manufacturer.')
def amzSnarfer(self, irc, msg, match):
r"http://www.amazon.com/exec/obidos/(?:tg/detail/-/|ASIN/)([^/]+)"
if not self.registryValue('linkSnarfer', msg.args[0]):
return
match = match.group(1)
# attribs is limited to ProductName since the URL can link to
# *any* type of product. The only attribute we know it will have
# is ProductName
attribs = {'ProductName' : 'title',
'Manufacturer' : 'publisher',
'Authors' : 'author',
'MpaaRating' : 'mpaa',
'Media' : 'media',
'ReleaseDate' : 'date',
'OurPrice' : 'price',
'Artists' : 'artist',
}
s = '%(title)s; %(artist)s; %(author)s; %(mpaa)s; %(media)s; '\
'%(date)s; %(publisher)s; price: %(price)s'
try:
item = amazon.searchByASIN(match)
bold = self.registryValue('bold', msg.args[0])
res = self._genResults(s, attribs, item, False, bold, 'title')
if res:
res = utils.commaAndify(res)
res = res.replace('; unknown', '')
res = res.replace('; price: unknown', '')
irc.reply(res, prefixName=False)
return
except amazon.AmazonError, e:
pass
self.log.warning('No item was found with that ASIN.')
Class = Amazon
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

181
plugins/Babelfish.py Normal file
View File

@ -0,0 +1,181 @@
#!/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.
###
"""
Babelfish-related commands.
"""
__revision__ = "$Id$"
import sets
import random
from itertools import imap
import babelfish
import conf
import utils
import privmsgs
import registry
import callbacks
class Languages(registry.OnlySomeStrings):
validStrings = tuple(map(str.capitalize, babelfish.available_languages))
normalize = staticmethod(str.capitalize)
class SpaceSeparatedListOfLanguages(registry.SeparatedListOf):
List = sets.Set
Value = Languages
def splitter(self, s):
return s.split()
joiner = ' '.join
conf.registerPlugin('Babelfish')
conf.registerChannelValue(conf.supybot.plugins.Babelfish, 'languages',
SpaceSeparatedListOfLanguages(babelfish.available_languages, """Determines
which languages are available for translation; valid input is a list of
languages separated by spaces."""))
class Babelfish(callbacks.Privmsg):
threaded = True
_abbrevs = utils.abbrev(imap(str.lower, babelfish.available_languages))
_abbrevs['de'] = 'german'
_abbrevs['jp'] = 'japanese'
_abbrevs['kr'] = 'korean'
_abbrevs['es'] = 'spanish'
_abbrevs['pt'] = 'portuguese'
_abbrevs['it'] = 'italian'
_abbrevs['zh'] = 'chinese'
for language in babelfish.available_languages:
_abbrevs[language] = language
def _getLang(self, fromLang, toLang, chan):
fromLang = self._abbrevs[fromLang.lower()]
toLang = self._abbrevs[toLang.lower()]
languages = map(str.lower, self.registryValue('languages',chan))
if fromLang not in languages:
fromLang = None
if toLang not in languages:
toLang = None
return (fromLang, toLang)
def languages(self, irc, msg, args):
"""takes no arguments
Returns the languages that Babelfish can translate to/from.
"""
irc.reply(utils.commaAndify(babelfish.available_languages))
def translate(self, irc, msg, args):
"""<from-language> [to] <to-language> <text>
Returns <text> translated from <from-language> into <to-language>.
"""
if len(args) >= 2 and args[1] == 'to':
args.pop(1)
(fromLang, toLang, text) = privmsgs.getArgs(args, required=3)
chan = msg.args[0]
try:
(fromLang, toLang) = self._getLang(fromLang, toLang, chan)
if not fromLang or not toLang:
langs = self.registryValue('languages', chan)
if not langs:
irc.error('I do not speak any other languages.')
return
else:
irc.error('I only speak %s.' % utils.commaAndify(langs,
And='or'))
return
translation = babelfish.translate(text, fromLang, toLang)
irc.reply(translation)
except (KeyError, babelfish.LanguageNotAvailableError), e:
irc.error('%s is not a valid language. Valid languages '
'include %s.' %
(e, self.registryValue('languages', chan)))
except babelfish.BabelizerIOError, e:
irc.error(str(e))
except babelfish.BabelfishChangedError, e:
irc.error('Babelfish has foiled our plans by changing its '
'webpage format.')
def babelize(self, irc, msg, args):
"""<from-language> <to-language> <text>
Translates <text> repeatedly between <from-language> and <to-language>
until it doesn't change anymore or 12 times, whichever is fewer. One
of the languages must be English.
"""
(fromLang, toLang, text) = privmsgs.getArgs(args, required=3)
chan = msg.args[0]
try:
(fromLang, toLang) = self._getLang(fromLang, toLang, chan)
if fromLang != 'english' and toLang != 'english':
irc.error('One language in babelize must be English.')
return
if not fromLang or not toLang:
langs = self.registryValue('languages', chan)
if not langs:
irc.error('I do not speak any other languages.')
return
else:
irc.error('I only speak %s.' % utils.commaAndify(langs,
And='or'))
return
translations = babelfish.babelize(text, fromLang, toLang)
irc.reply(translations[-1])
except (KeyError, babelfish.LanguageNotAvailableError), e:
irc.reply('%s is not a valid language. Valid languages '
'include %s.' % (e,
self.registryValue('languages', chan)))
except babelfish.BabelizerIOError, e:
irc.reply(e)
except babelfish.BabelfishChangedError, e:
irc.reply('Babelfish has foiled our plans by changing its '
'webpage format.')
def randomlanguage(self, irc, msg, args):
"""[<allow-english>]
Returns a random language supported by babelfish. If <allow-english>
is provided, will include English in the list of possible languages.
"""
allowEnglish = privmsgs.getArgs(args, required=0, optional=1)
languages = self.registryValue('languages', msg.args[0])
if not languages:
irc.error('I can\'t speak any other languages.')
language = random.choice(languages)
while not allowEnglish and language == 'English':
language = random.choice(languages)
irc.reply(language)
Class = Babelfish
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

163
plugins/BadWords.py Normal file
View File

@ -0,0 +1,163 @@
#!/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.
###
"""
Filters bad words on outgoing messages from the bot, so the bot can't be made
to say bad words.
"""
__revision__ = "$Id$"
import plugins
import re
import math
import sets
import time
import conf
import utils
import ircdb
import ircmsgs
import ircutils
import privmsgs
import registry
import callbacks
def configure(advanced):
from questions import output, expect, anything, something, yn
conf.registerPlugin('BadWords', True)
if yn('Would you like to add some bad words?'):
words = anything('What words? (separate individual words by spaces)')
conf.supybot.plugins.BadWords.words.set(words)
class LastModifiedSetOfStrings(registry.SpaceSeparatedListOfStrings):
List = sets.Set
lastModified = 0
def setValue(self, v):
self.lastModified = time.time()
registry.SpaceSeparatedListOfStrings.setValue(self, v)
conf.registerPlugin('BadWords')
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'words',
LastModifiedSetOfStrings([], """Determines what words are
considered to be 'bad' so the bot won't say them."""))
conf.registerGlobalValue(conf.supybot.plugins.BadWords,'requireWordBoundaries',
registry.Boolean(False, """Determines whether the bot will require bad
words to be independent words, or whether it will censor them within other
words. For instance, if 'darn' is a bad word, then if this is true, 'darn'
will be censored, but 'darnit' will not. You probably want this to be
false."""))
class String256(registry.String):
def setValue(self, s):
multiplier = int(math.ceil(1024/len(s)))
registry.String.setValue(self, s*multiplier)
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'nastyChars',
String256('!@#&', """Determines what characters will replace bad words; a
chunk of these characters matching the size of the replaced bad word will
be used to replace the bad words you've configured."""))
class ReplacementMethods(registry.OnlySomeStrings):
validStrings = ('simple', 'nastyCharacters')
conf.registerGlobalValue(conf.supybot.plugins.BadWords, 'replaceMethod',
ReplacementMethods('nastyCharacters', """Determines the manner in which
bad words will be replaced. 'nastyCharacters' (the default) will replace a
bad word with the same number of 'nasty characters' (like those used in
comic books; configurable by supybot.plugins.BadWords.nastyChars).
'simple' will replace a bad word with a simple strings (regardless of the
length of the bad word); this string is configurable via
supybot.plugins.BadWords.simpleReplacement."""))
conf.registerGlobalValue(conf.supybot.plugins.BadWords,'simpleReplacement',
registry.String('[CENSORED]', """Determines what word will replace bad
words if the replacement method is 'simple'."""))
class BadWords(privmsgs.CapabilityCheckingPrivmsg):
priority = 1
capability = 'admin'
def __init__(self):
privmsgs.CapabilityCheckingPrivmsg.__init__(self)
self.lastModified = 0
self.words = conf.supybot.plugins.BadWords.words
def sub(self, m):
replaceMethod = self.registryValue('replaceMethod')
if replaceMethod == 'simple':
return self.registryValue('simpleReplacement')
elif replaceMethod == 'nastyCharacters':
return self.registryValue('nastyChars')[:len(m.group(1))]
def outFilter(self, irc, msg):
if msg.command == 'PRIVMSG':
if self.lastModified < self.words.lastModified:
self.makeRegexp(self.words())
self.lastModified = time.time()
s = msg.args[1]
s = ircutils.stripFormatting(s)
s = self.regexp.sub(self.sub, s)
return ircmsgs.privmsg(msg.args[0], s)
else:
return msg
def makeRegexp(self, iterable):
s = '(%s)' % '|'.join(map(re.escape, iterable))
if self.registryValue('requireWordBoundaries'):
s = r'\b%s\b' % s
self.regexp = re.compile(s, re.I)
def add(self, irc, msg, args):
"""<word> [<word> ...]
Adds all <word>s to the list of words the bot isn't to say.
"""
set = self.words()
set.update(args)
self.words.setValue(set)
irc.replySuccess()
def remove(self, irc, msg, args):
"""<word> [<word> ...]
Removes a <word>s from the list of words the bot isn't to say.
"""
set = self.words()
for word in args:
set.discard(word)
self.words.setValue(set)
irc.replySuccess()
Class = BadWords
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

409
plugins/Bugzilla.py Normal file
View File

@ -0,0 +1,409 @@
#!/usr/bin/env python
###
# Copyright (c) 2003, Daniel Berlin
# Based on code from kibot
# 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.
###
"""
Bugzilla bug retriever
"""
__revision__ = "$Id$"
import os
import re
import csv
import getopt
import string
import urllib
import urllib2
import xml.dom.minidom as minidom
from itertools import imap, ifilter
from htmlentitydefs import entitydefs as entities
import registry
import conf
import utils
import plugins
import ircutils
import privmsgs
import callbacks
import structures
statusKeys = ['unconfirmed', 'new', 'assigned', 'reopened', 'resolved',
'verified', 'closed']
resolutionKeys = ['fixed', 'invalid', 'worksforme', 'needinfo',
'test-request', 'wontfix', 'cantfix', 'moved', 'duplicate',
'remind', 'later', 'notabug', 'notgnome', 'incomplete',
'gnome1.x', 'moved']
priorityKeys = ['p1', 'p2', 'p3', 'p4', 'p5', 'Low', 'Normal', 'High',
'Immediate', 'Urgent']
severityKeys = ['enhancement', 'trivial', 'minor', 'normal', 'major',
'critical', 'blocker']
dbfilename = os.path.join(conf.supybot.directories.data(), 'Bugzilla.db')
def makeDb(filename):
if os.path.exists(filename):
d = structures.PersistentDictionary(filename)
else:
d = structures.PersistentDictionary(filename)
d['gcc'] = ['http://gcc.gnu.org/bugzilla', 'GCC']
d['rh'] = ['http://bugzilla.redhat.com/bugzilla', 'Red Hat']
d['gnome'] = ['http://bugzilla.gnome.org/bugzilla', 'Gnome']
d['mozilla'] = ['http://bugzilla.mozilla.org', 'Mozilla']
d['ximian'] = ['http://bugzilla.ximian.com/bugzilla', 'Ximian Gnome']
d.flush()
return d
class BugzillaError(Exception):
"""A bugzilla error"""
pass
def configure(advanced):
from questions import output, expect, anything, yn
conf.registerPlugin('Bugzilla', True)
output("""The Bugzilla plugin has the functionality to watch for URLs
that match a specific pattern (we call this a snarfer). When
supybot sees such a URL, he will parse the web page for
information and reply with the results.""")
if yn('Do you want this bug snarfer enabled by default?', default=False):
conf.supybot.plugins.Bugzilla.bugSnarfer.setValue(True)
conf.registerPlugin('Bugzilla')
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'bugSnarfer',
registry.Boolean(False, """Determines whether the bug snarfer will be
enabled, such that any Bugzilla URLs seen in the channel will have their
information reported into the channel."""))
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'bold',
registry.Boolean(True, """Determines whether results are bolded."""))
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'replyNoBugzilla',
registry.String('I don\'t have a bugzilla %r.', """Determines the phrase
to use when notifying the user that there is no information about that
bugzilla site."""))
class Bugzilla(callbacks.PrivmsgCommandAndRegexp):
"""Show a link to a bug report with a brief description"""
threaded = True
regexps = ['bzSnarfer']
def __init__(self):
callbacks.PrivmsgCommandAndRegexp.__init__(self)
self.entre = re.compile('&(\S*?);')
# Schema: {name, [url, description]}
self.db = makeDb(dbfilename)
self.shorthand = utils.abbrev(self.db.keys())
def keywords2query(self, keywords):
"""Turn a list of keywords into a URL query string"""
query = []
for k in keywords:
k = k.lower()
if k in statusKeys:
query.append('bug_status=%s' % k.upper())
elif k in resolutionKeys:
query.append('resolution=%s' % k.upper())
elif k in priorityKeys:
query.append('priority=%s' % k.upper())
elif k in severityKeys:
query.append('bug_severity=%s' % k.upper())
query.append('ctype=csv')
return query
def die(self):
self.db.close()
def add(self, irc, msg, args):
"""<name> <url> <description>
Add a bugzilla <url> to the list of defined bugzillae. <name>
is the name that will be used to reference the zilla in all
commands. Unambiguous abbreviations of <name> will be accepted also.
<description> is the common name for the bugzilla and will
be listed with the bugzilla query.
"""
(name, url, description) = privmsgs.getArgs(args, required=3)
if url[-1] == '/':
url = url[:-1]
self.db[name] = [url, description]
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
def remove(self, irc, msg, args):
"""<abbreviation>
Remove the bugzilla associated with <abbreviation> from the list of
defined bugzillae.
"""
name = privmsgs.getArgs(args)
try:
name = self.shorthand[name]
del self.db[name]
self.shorthand = utils.abbrev(self.db.keys())
irc.replySuccess()
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
def list(self, irc, msg, args):
"""[<abbreviation>]
List defined bugzillae. If <abbreviation> is specified, list the
information for that bugzilla.
"""
name = privmsgs.getArgs(args, required=0, optional=1)
if name:
try:
name = self.shorthand[name]
(url, description) = self.db[name]
irc.reply('%s: %s, %s' % (name, description, url))
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
else:
if self.db:
L = self.db.keys()
L.sort()
irc.reply(utils.commaAndify(L))
else:
irc.reply('I have no defined bugzillae.')
def bzSnarfer(self, irc, msg, match):
r"(http://\S+)/show_bug.cgi\?id=([0-9]+)"
if not self.registryValue('bugSnarfer', msg.args[0]):
return
queryurl = '%s/xml.cgi?id=%s' % (match.group(1), match.group(2))
try:
summary = self._get_short_bug_summary(queryurl,
'Snarfed Bugzilla URL',
match.group(2))
except BugzillaError, e:
irc.reply(str(e))
return
except IOError, e:
msgtouser = '%s. Try yourself: %s' % (e, queryurl)
irc.reply(msgtouser)
return
bold = self.registryValue('bold', msg.args[0])
report = {}
report['id'] = match.group(2)
report['url'] = str('%s/show_bug.cgi?id=%s' % (match.group(1),
match.group(2)))
report['title'] = str(summary['title'])
report['summary'] = str(self._mk_summary_string(summary, bold))
report['product'] = str(summary['product'])
s = '%(product)s bug #%(id)s: %(title)s %(summary)s' % report
irc.reply(s, prefixName=False)
bzSnarfer = privmsgs.urlSnarfer(bzSnarfer)
def urlquery2bugslist(self, url, query):
"""Given a URL and query list for a CSV bug list, it'll return
all the bugs in a dict
"""
u = urllib2.urlopen(url + '/buglist.cgi', string.join(query, '&'))
# actually read in the file
csvreader = csv.reader(u)
# read header
fields = csvreader.next()
# read the rest of the list
bugs = {}
for bug in csvreader:
try:
bugid = int(bug[0])
except ValueError:
bugid = bug[0]
bugs[bugid] = {}
i = 1
for f in fields[1:]:
bugs[bugid][f] = bug[i]
i += 1
u.close()
return bugs
def search(self, irc, msg, args):
"""[--keywords=<keyword>] <bugzilla name> <search string in desc>
Look for bugs with <search string in the desc>, also matching
<keywords>. <keywords> can be statuses, severities, priorities, or
resolutions, seperated by commas"""
keywords = None
(optlist, rest) = getopt.getopt(args, '', ['keywords='])
for (option, arguments) in optlist:
if option == '--keywords':
keywords = arguments.split(',')
(name,searchstr)= privmsgs.getArgs(rest, required=2)
if not keywords:
keywords = ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED']
query = self.keywords2query(keywords)
query.append('short_desc_type=allwordssubstr')
query.append('short_desc=%s' % urllib.quote(searchstr))
query.append('order=Bug+Number')
try:
name = self.shorthand[name]
(url, description) = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
return
bugs = self.urlquery2bugslist(url, query)
bugids = bugs.keys()
bugids.sort()
s = '%s match %r (%s): %s.' % \
(utils.nItems('bug', len(bugs)), searchstr,
' AND '.join(keywords), utils.commaAndify(map(str, bugids)))
irc.reply(s)
def bug(self, irc, msg, args):
"""<abbreviation> <number>
Look up bug <number> in the bugzilla associated with <abbreviation>.
"""
(name, number) = privmsgs.getArgs(args, required=2)
try:
name = self.shorthand[name]
(url, description) = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugzilla', msg.args[0])
irc.error(s % name)
return
queryurl = '%s/xml.cgi?id=%s' % (url, number)
try:
summary = self._get_short_bug_summary(queryurl,description,number)
except BugzillaError, e:
irc.error(str(e))
return
except IOError, e:
s = '%s. Try yourself: %s' % (e, queryurl)
irc.error(s)
bold = self.registryValue('bold', msg.args[0])
report = {}
report['zilla'] = description
report['id'] = number
report['url'] = '%s/show_bug.cgi?id=%s' % (url, number)
report['title'] = str(summary['title'])
report['summary'] = self._mk_summary_string(summary, bold)
s = '%(zilla)s bug #%(id)s: %(title)s %(summary)s %(url)s' % report
irc.reply(s)
def _mk_summary_string(self, summary, bold):
L = []
if bold:
decorate = lambda s: ircutils.bold(s)
else:
decorate = lambda s: s
if 'product' in summary:
L.append(decorate('Product: ') + summary['product'])
if 'component' in summary:
L.append(decorate('Component: ') + summary['component'])
if 'severity' in summary:
L.append(decorate('Severity: ') + summary['severity'])
if 'assigned to' in summary:
L.append(decorate('Assigned to: ') + summary['assigned to'])
if 'status' in summary:
L.append(decorate('Status: ') + summary['status'])
if 'resolution' in summary:
L.append(decorate('Resolution: ') + summary['resolution'])
return ', '.join(imap(str, L))
def _get_short_bug_summary(self, url, desc, number):
try:
bugxml = self._getbugxml(url, desc)
zilladom = minidom.parseString(bugxml)
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s' % (desc, e)
raise BugzillaError, s
bug_n = zilladom.getElementsByTagName('bug')[0]
if bug_n.hasAttribute('error'):
errtxt = bug_n.getAttribute('error')
s = 'Error getting %s bug #%s: %s' % (desc, number, errtxt)
raise BugzillaError, s
summary = {}
try:
node = bug_n.getElementsByTagName('short_desc')[0]
summary['title'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('bug_status')[0]
summary['status'] = self._getnodetxt(node)
try:
node = bug_n.getElementsByTagName('resolution')[0]
summary['resolution'] = self._getnodetxt(node)
except:
pass
node = bug_n.getElementsByTagName('assigned_to')[0]
summary['assigned to'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('product')[0]
summary['product'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('component')[0]
summary['component'] = self._getnodetxt(node)
node = bug_n.getElementsByTagName('bug_severity')[0]
summary['severity'] = self._getnodetxt(node)
except Exception, e:
s = 'Could not parse XML returned by %s bugzilla: %s' % (desc, e)
raise BugzillaError, s
return summary
def _getbugxml(self, url, desc):
try:
fh = urllib2.urlopen(url)
except urllib2.HTTPError, e:
raise IOError, 'Connection to %s bugzilla failed' % desc
bugxml = fh.read()
fh.close()
if not bugxml:
raise IOError, 'Error getting bug content from %s' % desc
return bugxml
def _getnodetxt(self, node):
L = []
for childnode in node.childNodes:
if childnode.nodeType == childnode.TEXT_NODE:
L.append(childnode.data)
val = ''.join(L)
if node.hasAttribute('encoding'):
encoding = node.getAttribute('encoding')
if encoding == 'base64':
try:
val = val.decode('base64')
except:
val = 'Cannot convert bug data from base64.'
while self.entre.search(val):
entity = self.entre.search(val).group(1)
if entity in entities:
val = self.entre.sub(entities[entity], val)
else:
val = self.entre.sub('?', val)
return val
Class = Bugzilla
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

256
plugins/ChannelLogger.py Normal file
View File

@ -0,0 +1,256 @@
#!/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.
###
"""
Logs each channel to its own individual logfile.
"""
__revision__ = "$Id$"
import plugins
import time
from cStringIO import StringIO
import os
import conf
import world
import irclib
import ircmsgs
import ircutils
import registry
import callbacks
conf.registerPlugin('ChannelLogger')
conf.registerGlobalValue(conf.supybot.plugins.ChannelLogger,
'flushImmediately', registry.Boolean(False, """Determines whether channel
logfiles will be flushed anytime they're written to, rather than being
buffered by the operating system."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger, 'timestamp',
registry.Boolean(True, """Determines whether the logs for this channel are
timestamped with the timestamp in supybot.log.timestampFormat."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger, 'noLogPrefix',
registry.String('[nolog]', """Determines what string a message should be
prefixed with in order not to be logged. If you don't want any such
prefix, just set it to the empty string."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger,
'includeNetworkName', registry.Boolean(True, """Determines whether the bot
will include the name of the network in the filename for channel logs.
Since this is a channel-specific value, you can override for any channel.
You almost certainly want this to be True if you're relaying in a given
channel."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger, 'rotateLogs',
registry.Boolean(False, """Determines whether the bot will automatically
rotate the logs for this channel."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger,
'filenameTimestamp', registry.String('%d-%a-%Y', """Determines how to
represent the timestamp used for the filename in rotated logs. When this
timestamp changes, the old logfiles will be closed and a new one started.
The format characters for the timestamp are in the time.strftime docs at
python.org. In order for your logs to be rotated, you'll also have to
enable supybot.plugins.ChannelLogger.rotateLogs."""))
class FakeLog(object):
def flush(self):
return
def close(self):
return
def write(self, s):
return
class ChannelLogger(callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
self.lastMsg = None
self.laststate = None
self.logs = ircutils.IrcDict()
world.flushers.append(self.flush)
def die(self):
for log in self.logs.itervalues():
log.close()
world.flushers = [x for x in world.flushers
if hasattr(x, 'im_class') and x.im_class == self]
def __call__(self, irc, msg):
try:
super(self.__class__, self).__call__(irc, msg)
if self.lastMsg:
self.laststate.addMsg(irc, self.lastMsg)
else:
self.laststate = irc.state.copy()
finally:
# We must make sure this always gets updated.
self.lastMsg = msg
def reset(self):
for log in self.logs.itervalues():
log.close()
self.logs.clear()
def flush(self):
self.checkLogNames()
try:
for log in self.logs.itervalues():
log.flush()
except ValueError, e:
if e.args[0] != 'I/O operation on a closed file':
self.log.exception('Odd exception:')
def logNameTimestamp(self, channel):
format = self.registryValue('filenameTimestamp', channel)
return time.strftime(format)
def getLogName(self, channel):
if self.registryValue('rotateLogs', channel):
return '%s.%s.log' % (channel, self.logNameTimestamp(channel))
else:
return '%s.log' % channel
def checkLogNames(self):
for (channel, log) in self.logs.items():
if self.registryValue('rotateLogs', channel):
name = self.getLogName(channel)
if name != log.name:
log.close()
del self.logs[channel]
def getLog(self, channel):
self.checkLogNames()
if channel in self.logs:
return self.logs[channel]
else:
try:
logDir = conf.supybot.directories.log()
name = self.getLogName(channel)
log = file(os.path.join(logDir, name), 'a')
self.logs[channel] = log
return log
except IOError:
self.log.exception('Error opening log:')
return FakeLog()
def timestamp(self, log):
format = conf.supybot.log.timestampFormat()
if format:
log.write(time.strftime(format))
log.write(' ')
def normalizeChannel(self, irc, channel):
if self.registryValue('includeNetworkName', channel):
channel = '%s@%s' % (channel, irc.network)
return ircutils.toLower(channel)
def doLog(self, irc, channel, s):
channel = self.normalizeChannel(irc, channel)
log = self.getLog(channel)
if self.registryValue('timestamp', channel):
self.timestamp(log)
log.write(s)
if self.registryValue('flushImmediately'):
log.flush()
def doPrivmsg(self, irc, msg):
(recipients, text) = msg.args
for channel in recipients.split(','):
noLogPrefix = self.registryValue('noLogPrefix', channel)
if noLogPrefix and text.startswith(noLogPrefix):
text = '-= THIS MESSAGE NOT LOGGED =-'
if ircutils.isChannel(channel):
nick = msg.nick or irc.nick
if ircmsgs.isAction(msg):
self.doLog(irc, channel,
'* %s %s\n' % (nick, ircmsgs.unAction(msg)))
else:
self.doLog(irc, channel, '<%s> %s\n' % (nick, text))
def doNotice(self, irc, msg):
(recipients, text) = msg.args
for channel in recipients.split(','):
if ircutils.isChannel(channel):
self.doLog(irc, channel, '-%s- %s\n' % (msg.nick, text))
def doJoin(self, irc, msg):
for channel in msg.args[0].split(','):
self.doLog(irc, channel,
'*** %s has joined %s\n' %
(msg.nick or msg.prefix, channel))
def doKick(self, irc, msg):
if len(msg.args) == 3:
(channel, target, kickmsg) = msg.args
else:
(channel, target) = msg.args
kickmsg = ''
if kickmsg:
self.doLog(irc, channel,
'*** %s was kicked by %s (%s)\n' %
(target, msg.nick, kickmsg))
else:
self.doLog(irc, channel,
'*** %s was kicked by %s\n' % (target, msg.nick))
def doPart(self, irc, msg):
for channel in msg.args[0].split(','):
self.doLog(irc, channel,
'*** %s has left %s\n' % (msg.nick, channel))
def doMode(self, irc, msg):
channel = msg.args[0]
if ircutils.isChannel(channel) and msg.args[1:]:
self.doLog(irc, channel,
'*** %s sets mode: %s %s\n' %
(msg.nick or msg.prefix, msg.args[1],
' '.join(msg.args[2:])))
def doTopic(self, irc, msg):
if len(msg.args) == 1:
return # It's an empty TOPIC just to get the current topic.
channel = msg.args[0]
self.doLog(irc, channel,
'*** %s changes topic to "%s"\n' % (msg.nick, msg.args[1]))
def doQuit(self, irc, msg):
for (channel, chan) in self.laststate.channels.iteritems():
if msg.nick in chan.users:
self.doLog(irc, channel, '*** %s has quit IRC\n' % msg.nick)
def outFilter(self, irc, msg):
# Gotta catch my own messages *somehow* :)
# Let's try this little trick...
if msg.command != 'PART':
m = ircmsgs.IrcMsg(msg=msg, prefix=irc.prefix)
self(irc, m)
return msg
Class = ChannelLogger
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

350
plugins/ChannelStats.py Normal file
View File

@ -0,0 +1,350 @@
#!/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.
###
"""
Silently listens to every message received on a channel and keeps statistics
concerning joins, parts, and various other commands in addition to tracking
statistics about smileys, actions, characters, and words.
"""
__revision__ = "$Id$"
import plugins
import os
import re
import sets
import time
import getopt
import string
from itertools import imap, ifilter
import log
import conf
import utils
import world
import ircdb
import irclib
import ircmsgs
import plugins
import ircutils
import privmsgs
import registry
import callbacks
class Smileys(registry.Value):
def set(self, s):
L = s.split()
self.setValue(L)
def setValue(self, v):
self.s = ' '.join(v)
self.value = re.compile('|'.join(imap(re.escape, v)))
def __str__(self):
return self.s
conf.registerPlugin('ChannelStats')
conf.registerChannelValue(conf.supybot.plugins.ChannelStats, 'selfStats',
registry.Boolean(True, """Determines whether the bot will keep channel
statistics on itself, possibly skewing the channel stats (especially in
cases where the bot is relaying between channels on a network)."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelStats, 'smileys',
Smileys(':) ;) ;] :-) :-D :D :P :p (= =)'.split(), """Determines what
words (i.e., pieces of text with no spaces in them) are considered
'smileys' for the purposes of stats-keeping."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelStats, 'frowns',
Smileys(':| :-/ :-\\ :\\ :/ :( :-( :\'('.split(), """Determines what words
(i.e., pieces of text with no spaces in them ) are considered 'frowns' for
the purposes of stats-keeping."""))
class ChannelStat(irclib.IrcCommandDispatcher):
def __init__(self, actions=0, chars=0, frowns=0, joins=0, kicks=0, modes=0,
msgs=0, parts=0, quits=0, smileys=0, topics=0, words=0):
self.actions = actions
self.chars = chars
self.frowns = frowns
self.joins = joins
self.kicks = kicks
self.modes = modes
self.msgs = msgs
self.parts = parts
self.quits = quits
self.smileys = smileys
self.topics = topics
self.words = words
self._values = ['actions', 'chars', 'frowns', 'joins', 'kicks','modes',
'msgs', 'parts', 'quits', 'smileys', 'topics', 'words']
def values(self):
return map(curry(getattr, self), self._values)
def addMsg(self, msg):
self.msgs += 1
method = self.dispatchCommand(msg.command)
if method is not None:
method(msg)
def doPayload(self, channel, payload):
self.chars += len(payload)
self.words += len(payload.split())
fRe = conf.supybot.plugins.ChannelStats.get('frowns').get(channel)()
sRe =conf.supybot.plugins.ChannelStats.get('smileys').get(channel)()
self.frowns += len(fRe.findall(payload))
self.smileys += len(sRe.findall(payload))
def doPrivmsg(self, msg):
self.doPayload(*msg.args)
if ircmsgs.isAction(msg):
self.actions += 1
def doTopic(self, msg):
self.doPayload(*msg.args)
self.topics += 1
def doKick(self, msg):
self.kicks += 1
def doPart(self, msg):
if len(msg.args) == 2:
self.doPayload(*msg.args)
self.parts += 1
def doJoin(self, msg):
if len(msg.args) == 2:
self.doPayload(*msg.args)
self.joins += 1
def doMode(self, msg):
self.modes += 1
# doQuit is handled by the plugin.
class UserStat(ChannelStat):
def __init__(self, kicked=0, *args):
ChannelStat.__init__(self, *args)
self.kicked = kicked
self._values.insert(0, 'kicked')
def doKick(self, msg):
self.doPayload(msg.args[0], msg.args[2])
self.kicks += 1
class StatsDB(plugins.ChannelUserDB):
def __init__(self, *args, **kwargs):
plugins.ChannelUserDB.__init__(self, *args, **kwargs)
def serialize(self, v):
return v.values()
def deserialize(self, channel, id, L):
L = map(int, L)
if id == 'channelStats':
return ChannelStat(*L)
else:
return UserStat(*L)
def addMsg(self, msg, id=None):
channel = msg.args[0]
if ircutils.isChannel(channel):
if (channel, 'channelStats') not in self:
self[channel, 'channelStats'] = ChannelStat()
self[channel, 'channelStats'].addMsg(msg)
try:
if id is None:
id = ircdb.users.getUserId(msg.prefix)
except KeyError:
return
if (channel, id) not in self:
self[channel, id] = UserStat()
self[channel, id].addMsg(msg)
def getChannelStats(self, channel):
return self[channel, 'channelStats']
def getUserStats(self, channel, id):
return self[channel, id]
class ChannelStats(callbacks.Privmsg):
noIgnore = True
def __init__(self):
callbacks.Privmsg.__init__(self)
self.lastmsg = None
self.laststate = None
self.outFiltering = False
self.db = StatsDB(os.path.join(conf.supybot.directories.data(),
'ChannelStats.db'))
world.flushers.append(self.db.flush)
def die(self):
if self.db.flush in world.flushers:
world.flushers.remove(self.db.flush)
else:
self.log.debug('Odd, no flush in flushers: %r', world.flushers)
self.db.close()
callbacks.Privmsg.die(self)
def __call__(self, irc, msg):
try:
if self.lastmsg:
self.laststate.addMsg(irc, self.lastmsg)
else:
self.laststate = irc.state.copy()
finally:
self.lastmsg = msg
self.db.addMsg(msg)
super(ChannelStats, self).__call__(irc, msg)
def outFilter(self, irc, msg):
if msg.command == 'PRIVMSG':
if ircutils.isChannel(msg.args[0]):
if self.registryValue('selfStats', msg.args[0]):
try:
self.outFiltering = True
self.db.addMsg(msg, 0)
finally:
self.outFiltering = False
return msg
def doQuit(self, irc, msg):
try:
id = ircdb.users.getUserId(msg.prefix)
except KeyError:
id = None
for (channel, c) in self.laststate.channels.iteritems():
if msg.nick in c.users:
if (channel, 'channelStats') not in self.db:
self.db[channel, 'channelStats'] = ChannelStat()
self.db[channel, 'channelStats'].quits += 1
if id is not None:
if (channel, id) not in self.db:
self.db[channel, id] = UserStat()
self.db[channel, id].quits += 1
def doKick(self, irc, msg):
(channel, nick, _) = msg.args
hostmask = irc.state.nickToHostmask(nick)
try:
id = ircdb.users.getUserId(hostmask)
except KeyError:
return
if channel not in self.db.channels:
self.db.channels[channel] = {}
if id not in self.db.channels[channel]:
self.db.channels[channel][id] = UserStat()
self.db.channels[channel][id].kicked += 1
def stats(self, irc, msg, args):
"""[<channel>] [<name>]
Returns the statistics for <name> on <channel>. <channel> is only
necessary if the message isn't sent on the channel itself. If <name>
isn't given, it defaults to the user sending the command.
"""
channel = privmsgs.getChannel(msg, args)
name = privmsgs.getArgs(args, required=0, optional=1)
if name == irc.nick:
id = 0
elif not name:
try:
id = ircdb.users.getUserId(msg.prefix)
name = ircdb.users.getUser(id).name
except KeyError:
irc.error('I couldn\'t find you in my user database.')
return
elif not ircdb.users.hasUser(name):
try:
hostmask = irc.state.nickToHostmask(name)
id = ircdb.users.getUserId(hostmask)
except KeyError:
irc.errorNoUser()
return
else:
id = ircdb.users.getUserId(name)
try:
stats = self.db.getUserStats(channel, id)
s = '%s has sent %s; a total of %s, %s, ' \
'%s, and %s; %s of those messages %s' \
'%s has joined %s, parted %s, quit %s, kicked someone %s, ' \
'been kicked %s, changed the topic %s, ' \
'and changed the mode %s.' % \
(name, utils.nItems('message', stats.msgs),
utils.nItems('character', stats.chars),
utils.nItems('word', stats.words),
utils.nItems('smiley', stats.smileys),
utils.nItems('frown', stats.frowns),
stats.actions, stats.actions == 1 and 'was an ACTION. '
or 'were ACTIONs. ',
name,
utils.nItems('time', stats.joins),
utils.nItems('time', stats.parts),
utils.nItems('time', stats.quits),
utils.nItems('time', stats.kicks),
utils.nItems('time', stats.kicked),
utils.nItems('time', stats.topics),
utils.nItems('time', stats.modes))
irc.reply(s)
except KeyError:
irc.error('I have no stats for that %s in %s' % (name, channel))
def channelstats(self, irc, msg, args):
"""[<channel>]
Returns the statistics for <channel>. <channel> is only necessary if
the message isn't sent on the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
try:
stats = self.db.getChannelStats(channel)
s = 'On %s there have been %s messages, containing %s ' \
'characters, %s, %s, and %s; ' \
'%s of those messages %s. There have been ' \
'%s, %s, %s, %s, %s, and %s.' % \
(channel, stats.msgs, stats.chars,
utils.nItems('word', stats.words),
utils.nItems('smiley', stats.smileys),
utils.nItems('frown', stats.frowns),
stats.actions, stats.actions == 1 and 'was an ACTION'
or 'were ACTIONs',
utils.nItems('join', stats.joins),
utils.nItems('part', stats.parts),
utils.nItems('quit', stats.quits),
utils.nItems('kick', stats.kicks),
utils.nItems('change', stats.modes, between='mode'),
utils.nItems('change', stats.topics, between='topic'))
irc.reply(s)
except KeyError:
irc.error('I\'ve never been on %s.' % channel)
Class = ChannelStats
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

89
plugins/Ctcp.py Normal file
View File

@ -0,0 +1,89 @@
#!/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.
###
"""
Handles standard CTCP responses to PING, TIME, SOURCE, VERSION, USERINFO,
and FINGER.
"""
__revision__ = "$Id$"
import plugins
import os
import sys
import time
sys.path.append(os.pardir)
import conf
import ircmsgs
import callbacks
notice = ircmsgs.notice
class Ctcp(callbacks.PrivmsgRegexp):
public = False
def ping(self, irc, msg, match):
"\x01PING (.*)\x01"
self.log.info('Received CTCP PING from %s', msg.prefix)
irc.queueMsg(notice(msg.nick, '\x01PING %s\x01' % match.group(1)))
def version(self, irc, msg, match):
"\x01VERSION\x01"
self.log.info('Received CTCP VERSION from %s', msg.prefix)
s = '\x01VERSION SupyBot %s\x01' % conf.version
irc.queueMsg(notice(msg.nick, s))
def userinfo(self, irc, msg, match):
"\x01USERINFO\x01"
self.log.info('Received CTCP USERINFO from %s', msg.prefix)
irc.queueMsg(notice(msg.nick, '\x01USERINFO\x01'))
def time(self, irc, msg, match):
"\x01TIME\x01"
self.log.info('Received CTCP TIME from %s' % msg.prefix)
irc.queueMsg(notice(msg.nick, '\x01%s\x01' % time.ctime()))
def finger(self, irc, msg, match):
"\x01FINGER\x01"
self.log.info('Received CTCP FINGER from %s' % msg.prefix)
s = '\x01SupyBot, the best Python bot in existence!\x01'
irc.queueMsg(notice(msg.nick, s))
def source(self, irc, msg, match):
"\x01SOURCE\x01"
self.log.info('Received CTCP SOURCE from %s' % msg.prefix)
s = 'http://www.sourceforge.net/projects/supybot/'
irc.queueMsg(notice(msg.nick, s))
Class = Ctcp
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

105
plugins/DCC.py Normal file
View File

@ -0,0 +1,105 @@
#!/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.
###
"""
Supports various DCC things.
"""
__revision__ = "$Id$"
import plugins
import socket
import textwrap
import threading
import conf
import utils
import world
import ircmsgs
import ircutils
import privmsgs
import callbacks
class DCC(callbacks.Privmsg):
def chat(self, irc, msg, args):
"""<text>
Sends <text> to the user via a DCC CHAT. Use nested commands to your
benefit here.
"""
text = privmsgs.getArgs(args)
def openChatPort():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(60)
host = ircutils.hostFromHostmask(irc.prefix)
if conf.supybot.externalIP():
ip = conf.supybot.externalIP()
else:
try:
ip = socket.gethostbyname(host)
except socket.error, e:
s = 'Error trying to determine the external IP ' \
'address of this machine via the host %s: %s'
self.log.warning(s, host, e)
irc.replyError()
return
i = ircutils.dccIP(ip)
sock.bind((host, 0))
port = sock.getsockname()[1]
self.log.info('DCC CHAT port opened at (%s, %s)', host, port)
sock.listen(1)
irc.queueMsg(ircmsgs.privmsg(msg.nick,
'\x01DCC CHAT chat %s %s\x01' % \
(i, port)))
(realSock, addr) = sock.accept()
self.log.info('DCC CHAT accepted from %s', addr)
for line in textwrap.wrap(text, 80):
realSock.send(line)
realSock.send('\n')
finally:
self.log.info('Finally closing sock and realSock.')
sock.close()
try:
realSock.close()
except UnboundLocalError:
pass
t = threading.Thread(target=openChatPort)
world.threadsSpawned += 1
t.setDaemon(True)
t.start()
Class = DCC
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

363
plugins/Debian.py Normal file
View File

@ -0,0 +1,363 @@
#!/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.
###
"""
This is a module to contain Debian-specific commands.
"""
__revision__ = "$Id$"
import plugins
import re
import gzip
import sets
import getopt
import popen2
import socket
import urllib
import fnmatch
import os.path
from itertools import imap, ifilter
import registry
import conf
import utils
import privmsgs
import webutils
import callbacks
def configure(advanced):
from questions import output, expect, anything, something, yn
conf.registerPlugin('Debian', True)
if not utils.findBinaryInPath('zgrep'):
if not advanced:
output("""I can't find zgrep in your path. This is necessary
to run the file command. I'll disable this command
now. When you get zgrep in your path, use the command
'enable Debian.file' to re-enable the command.""")
conf.supybot.defaultCapabilities().add('-Debian.file')
else:
output("""I can't find zgrep in your path. If you want to run
the file command with any sort of expediency, you'll
need it. You can use a python equivalent, but it's
about two orders of magnitude slower. THIS MEANS IT
WILL TAKE AGES TO RUN THIS COMMAND. Don't do this.""")
if yn('Do you want to use a Python equivalent of zgrep?'):
conf.supybot.plugins.Debian.pythonZgrep.setValue(True)
else:
output('I\'ll disable file now.')
conf.supybot.defaultCapabilities().add('-Debian.file')
conf.registerPlugin('Debian')
conf.registerGlobalValue(conf.supybot.plugins.Debian, 'pythonZgrep',
registry.Boolean(False, """An advanced option, mostly just for testing;
uses a Python-coded zgrep rather than the actual zgrep executable,
generally resulting in a 50x slowdown. What would take 2 seconds will
take 100 with this enabled. Don't enable this."""))
class Debian(callbacks.Privmsg,
plugins.PeriodicFileDownloader):
threaded = True
periodicFiles = {
# This file is only updated once a week, so there's no sense in
# downloading a new one every day.
'Contents-i386.gz': ('ftp://ftp.us.debian.org/'
'debian/dists/unstable/Contents-i386.gz',
604800, None)
}
contents = os.path.join(conf.supybot.directories.data(),'Contents-i386.gz')
def __init__(self):
callbacks.Privmsg.__init__(self)
plugins.PeriodicFileDownloader.__init__(self)
def die(self):
callbacks.Privmsg.die(self)
def file(self, irc, msg, args):
"""[--{regexp,exact}=<value>] [<glob>]
Returns packages in Debian that includes files matching <glob>. If
--regexp is given, returns packages that include files matching the
given regexp. If --exact is given, returns packages that include files
matching exactly the string given.
"""
self.getFile('Contents-i386.gz')
# Make sure it's anchored, make sure it doesn't have a leading slash
# (the filenames don't have leading slashes, and people may not know
# that).
(optlist, rest) = getopt.getopt(args, '', ['regexp=', 'exact='])
if not optlist and not rest:
raise callbacks.ArgumentError
if len(optlist) + len(rest) > 1:
irc.error('Only one search option is allowed.')
return
for (option, arg) in optlist:
if option == '--exact':
regexp = arg.lstrip('/')
elif option == '--regexp':
regexp = arg
if rest:
glob = rest.pop()
regexp = fnmatch.translate(glob.lstrip('/'))
regexp = regexp.rstrip('$')
regexp += ".* "
try:
re_obj = re.compile(regexp, re.I)
except re.error, e:
irc.error("Error in regexp: %s" % e)
return
if self.registryValue('pythonZgrep'):
fd = gzip.open(self.contents)
r = imap(lambda tup: tup[0],
ifilter(lambda tup: tup[0],
imap(lambda line:(re_obj.search(line), line),fd)))
else:
try:
(r, w) = popen2.popen4(['zgrep', '-e', regexp, self.contents])
w.close()
except TypeError:
# We're on Windows.
irc.error('This command won\'t work on this platform. '
'If you think it should (i.e., you know that '
'you have a zgrep binary somewhere) then file '
'a bug about it at http://supybot.sf.net/ .')
return
packages = sets.Set() # Make packages unique
try:
for line in r:
if len(packages) > 100:
irc.error('More than 100 packages matched, '
'please narrow your search.')
return
try:
(filename, pkg_list) = line[:-1].split()
if filename == 'FILE':
# This is the last line before the actual files.
continue
except ValueError: # Unpack list of wrong size.
continue # We've not gotten to the files yet.
packages.update(pkg_list.split(','))
finally:
if hasattr(r, 'close'):
r.close()
if len(packages) == 0:
irc.reply('I found no packages with that file.')
else:
irc.reply(utils.commaAndify(packages))
_debreflags = re.DOTALL | re.IGNORECASE
_debbrre = re.compile(r'<li><a href[^>]+>(.*?)</a> \(', _debreflags)
_debverre = re.compile(r'<br>(?:\d+:)?(\S+):', _debreflags)
_deblistre = re.compile(r'<h3>Package ([^<]+)</h3>(.*?)</ul>', _debreflags)
_debBranches = ('stable', 'testing', 'unstable', 'experimental')
def version(self, irc, msg, args):
"""[stable|testing|unstable|experimental] <package name>
Returns the current version(s) of a Debian package in the given branch
(if any, otherwise all available ones are displayed).
"""
if not args:
raise callbacks.ArgumentError
if args and args[0] in self._debBranches:
branch = args.pop(0)
else:
branch = 'all'
if not args:
irc.error('You must give a package name.')
return
responses = []
package = privmsgs.getArgs(args)
package = urllib.quote(package)
url = 'http://packages.debian.org/cgi-bin/search_packages.pl?keywords'\
'=%s&searchon=names&version=%s&release=all' % (package, branch)
try:
html = webutils.getUrl(url)
except webutils.WebError, e:
irc.error('I couldn\'t reach the search page (%s).' % e)
return
if 'is down at the moment' in html:
irc.error('Packages.debian.org is down at the moment. '
'Please try again later.')
return
pkgs = self._deblistre.findall(html)
#self.log.warning(pkgs)
if not pkgs:
irc.reply('No package found for %s (%s)' %
(urllib.unquote(package), branch))
else:
for pkg in pkgs:
pkgMatch = pkg[0]
brMatch = self._debbrre.findall(pkg[1])
verMatch = self._debverre.findall(pkg[1])
if pkgMatch and brMatch and verMatch:
versions = zip(brMatch, verMatch)
for version in versions:
s = '%s (%s)' % (pkgMatch, ': '.join(version))
responses.append(s)
resp = '%s matches found: %s' % \
(len(responses), '; '.join(responses))
irc.reply(resp)
_incomingRe = re.compile(r'<a href="(.*?\.deb)">', re.I)
def incoming(self, irc, msg, args):
"""[--{regexp,arch}=<value>] <glob>
Checks debian incoming for a matching package name. The arch
parameter defaults to i386; --regexp returns only those package names
that match a given regexp, and normal matches use standard *nix
globbing.
"""
(optlist, rest) = getopt.getopt(args, '', ['regexp=', 'arch='])
predicates = []
archPredicate = lambda s: ('_i386.' in s)
for (option, arg) in optlist:
if option == '--regexp':
try:
r = utils.perlReToPythonRe(arg)
predicates.append(r.search)
except ValueError:
irc.error('%r is not a valid regexp.' % arg)
return
elif option == '--arch':
arg = '_%s.' % arg
archPredicate = lambda s, arg=arg: (arg in s)
predicates.append(archPredicate)
globs = privmsgs.getArgs(rest).split()
for glob in globs:
glob = glob.replace('*', '.*').replace('?', '.?')
predicates.append(re.compile(r'.*%s.*' % glob).search)
packages = []
try:
fd = webutils.getUrlFd('http://incoming.debian.org/')
except webutils.WebError, e:
irc.error(e)
return
for line in fd:
m = self._incomingRe.search(line)
if m:
name = m.group(1)
if all(None, imap(lambda p: p(name), predicates)):
realname = rsplit(name, '_', 1)[0]
packages.append(realname)
if len(packages) == 0:
irc.error('No packages matched that search.')
else:
irc.reply(utils.commaAndify(packages))
incoming = privmsgs.thread(incoming)
_newpkgre = re.compile(r'<li><a href[^>]+>([^<]+)</a>')
def new(self, irc, msg, args):
"""[--{main,contrib,non-free}] [<glob>]
Checks for packages that have been added to Debian's unstable branch
in the past week. If no glob is specified, returns a list of all
packages. If no section is specified, defaults to main.
"""
options = ['main', 'contrib', 'non-free']
(optlist, rest) = getopt.getopt(args, '', options)
section = 'main'
for (option, _) in optlist:
option = option.lstrip('-')
if option in options:
section = option
glob = privmsgs.getArgs(rest, required=0, optional=1)
if not glob:
glob = '*'
if '?' not in glob and '*' not in glob:
glob = '*%s*' % glob
try:
fd = webutils.getUrlFd(
'http://packages.debian.org/unstable/newpkg_%s' % section)
except webutils.WebError, e:
irc.error(e)
packages = []
#self.log.warning(section)
#self.log.warning(glob)
for line in fd:
m = self._newpkgre.search(line)
if m:
m = m.group(1)
if fnmatch.fnmatch(m, glob):
packages.append(m)
fd.close()
if packages:
irc.reply(utils.commaAndify(packages))
else:
irc.error('No packages matched that search.')
_severity = re.compile(r'.*(?:severity set to `([^\']+)\'|'
r'severity:\s+([^\s]+))', re.I)
_package = re.compile(r'Package: <[^>]+>([^<]+)<', re.I | re.S)
_reporter = re.compile(r'Reported by: <[^>]+>([^<]+)<', re.I | re.S)
_subject = re.compile(r'<br>([^<]+)</h1>', re.I | re.S)
_date = re.compile(r'Date: ([^;]+);', re.I | re.S)
_searches = (_package, _subject, _reporter, _date)
def bug(self, irc, msg, args):
"""<num>
Returns a description of the bug with bug id <num>.
"""
bug = privmsgs.getArgs(args)
if ' ' in bug:
irc.error('Only one bug can be looked up at a time.')
return
try:
int(bug)
except ValueError:
irc.error('<num> must be an integer.')
return
url = 'http://bugs.debian.org/%s' % bug
text = webutils.getUrl(url)
if "There is no record of Bug" in text:
irc.error('I could not find a bug report matching that number.')
return
searches = map(lambda p: p.search(text), self._searches)
sev = self._severity.search(text)
# This section should be cleaned up to ease future modifications
if all(None, searches):
resp = 'Package: %s; Subject: %s; Reported by %s on %s' %\
tuple(map(utils.htmlToText,
map(lambda p: p.group(1), searches)))
if sev:
sev = filter(None, sev.groups())
if sev:
resp = '; '.join([resp, 'Severity: %s' % sev[0],
'<%s>' % url])
irc.reply(resp)
else:
irc.reply('I was unable to properly parse the BTS page.')
Class = Debian
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

147
plugins/Dict.py Normal file
View File

@ -0,0 +1,147 @@
#!/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.
###
"""
Commands that use the dictd protocol to snag stuff off a server.
"""
__revision__ = "$Id$"
import plugins
import sets
import random
import socket
import dictclient
import conf
import utils
import plugins
import registry
import ircutils
import privmsgs
import callbacks
def configure(advanced):
from questions import output, expect, anything, something, yn
conf.registerPlugin('Dict', True)
output('The default dictd server is dict.org.')
if yn('Would you like to specify a different dictd server?'):
server = something('What server?')
conf.supybot.plugins.Dict.server.set(server)
replyTimeout = 'Timeout on the dictd server.'
conf.registerPlugin('Dict')
# TODO: We should make this check to see if there's actually a dictd server
# running on the host given.
conf.registerGlobalValue(conf.supybot.plugins.Dict, 'server',
registry.String('dict.org', """Determines what server the bot will
retrieve definitions from."""))
class Dict(callbacks.Privmsg):
threaded = True
def dictionaries(self, irc, msg, args):
"""takes no arguments.
Returns the dictionaries valid for the dict command.
"""
try:
server = conf.supybot.plugins.Dict.server()
conn = dictclient.Connection(server)
dbs = conn.getdbdescs().keys()
dbs.sort()
irc.reply(utils.commaAndify(dbs))
except socket.timeout:
irc.error(replyTimeout)
def random(self, irc, msg, args):
"""takes no arguments.
Returns a random valid dictionary.
"""
try:
server = conf.supybot.plugins.Dict.server()
conn = dictclient.Connection(server)
dbs = conn.getdbdescs().keys()
irc.reply(random.choice(dbs))
except socket.timeout:
irc.error(replyTimeout)
def dict(self, irc, msg, args):
"""[<dictionary>] <word>
Looks up the definition of <word> on dict.org's dictd server.
"""
if not args:
raise callbacks.ArgumentError
try:
server = conf.supybot.plugins.Dict.server()
conn = dictclient.Connection(server)
except socket.timeout:
irc.error('Timeout on the dict server.')
return
dbs = sets.Set(conn.getdbdescs())
if args[0] in dbs:
dictionary = args.pop(0)
else:
dictionary = '*'
word = privmsgs.getArgs(args)
definitions = conn.define(dictionary, word)
dbs = sets.Set()
if not definitions:
if dictionary == '*':
irc.reply('No definition for %r could be found.' % word)
else:
irc.reply('No definition for %r could be found in %s' %
(word, ircutils.bold(dictionary)))
return
L = []
for d in definitions:
dbs.add(ircutils.bold(d.getdb().getname()))
(db, s) = (d.getdb().getname(), d.getdefstr())
db = ircutils.bold(db)
s = utils.normalizeWhitespace(s).rstrip(';.,')
L.append('%s: %s' % (db, s))
utils.sortBy(len, L)
if dictionary == '*' and len(dbs) > 1:
s = '%s responded: %s' % (utils.commaAndify(dbs), '; '.join(L))
else:
s = '; '.join(L)
irc.reply(s)
Class = Dict
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

216
plugins/Dunno.py Normal file
View File

@ -0,0 +1,216 @@
#!/usr/bin/env python
###
# Copyright (c) 2003, Daniel DiPaolo
# 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.
###
"""
The Dunno module is used to spice up the "replyWhenNotCommand" behavior with
random "I dunno"-like responses.
"""
__revision__ = "$Id$"
import os
import time
import conf
import utils
import ircdb
import plugins
import privmsgs
import callbacks
try:
import sqlite
except ImportError:
raise callbacks.Error, 'You need to have PySQLite installed to use this ' \
'plugin. Download it at <http://pysqlite.sf.net/>'
dbfilename = os.path.join(conf.supybot.directories.data(), 'Dunno.db')
class Dunno(callbacks.Privmsg):
priority = 100
def __init__(self):
callbacks.Privmsg.__init__(self)
self.makeDb(dbfilename)
def makeDb(self, filename):
"""create Dunno database and tables"""
if os.path.exists(filename):
self.db = sqlite.connect(filename)
return
self.db = sqlite.connect(filename, converters={'bool': bool})
cursor = self.db.cursor()
cursor.execute("""CREATE TABLE dunnos (
id INTEGER PRIMARY KEY,
added_by INTEGER,
added_at TIMESTAMP,
dunno TEXT
)""")
self.db.commit()
def invalidCommand(self, irc, msg, tokens):
cursor = self.db.cursor()
cursor.execute("""SELECT dunno
FROM dunnos
ORDER BY random()
LIMIT 1""")
if cursor.rowcount != 0:
dunno = cursor.fetchone()[0]
dunno = plugins.standardSubstitute(irc, msg, dunno)
irc.reply(dunno, prefixName=False)
def add(self, irc, msg, args):
"""<text>
Adds <text> as a "dunno" to be used as a random response when no
command or factoid key matches. Can optionally contain '$who', which
will be replaced by the user's name when the dunno is displayed.
"""
# Must be registered to use this
try:
id = ircdb.users.getUserId(msg.prefix)
except KeyError:
irc.errorNotRegistered()
return
text = privmsgs.getArgs(args, required=1)
cursor = self.db.cursor()
cursor.execute("""INSERT INTO dunnos
VALUES(NULL, %s, %s, %s)""",
id, int(time.time()), text)
self.db.commit()
irc.replySuccess()
def remove(self, irc, msg, args):
"""<id>
Removes dunno with the given <id>.
"""
# Must be registered to use this
try:
user_id = ircdb.users.getUserId(msg.prefix)
except KeyError:
irc.errorNotRegistered()
return
dunno_id = privmsgs.getArgs(args, required=1)
cursor = self.db.cursor()
cursor.execute("""SELECT added_by, dunno
FROM dunnos
WHERE id = %s""" % dunno_id)
if cursor.rowcount == 0:
irc.error('No dunno with id: %s' % dunno_id)
return
(added_by, dunno) = cursor.fetchone()
if not (ircdb.checkCapability(user_id, 'admin') or \
added_by == user_id):
irc.error('Only admins and the dunno creator may delete a '
'dunno.')
return
cursor.execute("""DELETE FROM dunnos WHERE id = %s""" % dunno_id)
self.db.commit()
irc.replySuccess()
def search(self, irc, msg, args):
"""<text>
Search for dunno containing the given text. Returns the ids of the
dunnos with the text in them.
"""
text = privmsgs.getArgs(args, required=1)
glob = "%" + text + "%"
cursor = self.db.cursor()
cursor.execute("""SELECT id FROM dunnos
WHERE dunno LIKE %s""", glob)
if cursor.rowcount == 0:
irc.error('No dunnos with %r found.' % text)
return
ids = [str(t[0]) for t in cursor.fetchall()]
s = 'Dunno search for %r (%s found): %s' % \
(text, len(ids), utils.commaAndify(ids))
irc.reply(s)
def get(self, irc, msg, args):
"""<id>
Display the text of the dunno with the given id.
"""
id = privmsgs.getArgs(args, required=1)
try:
id = int(id)
except ValueError:
irc.error('%r is not a valid dunno id' % id)
return
cursor = self.db.cursor()
cursor.execute("""SELECT dunno FROM dunnos WHERE id = %s""", id)
if cursor.rowcount == 0:
irc.error('No dunno found with id #%s' % id)
return
dunno = cursor.fetchone()[0]
irc.reply("Dunno #%s: %r" % (id, dunno))
def change(self, irc, msg, args):
"""<id> <regexp>
Alters the dunno with the given id according to the provided regexp.
"""
id, regexp = privmsgs.getArgs(args, required=2)
# Must be registered to use this
try:
user_id = ircdb.users.getUserId(msg.prefix)
except KeyError:
irc.error(msg, conf.replyNotRegistered)
return
# Check id arg
try:
id = int(id)
except ValueError:
irc.error(msg, '%r is not a valid dunno id' % id)
return
cursor = self.db.cursor()
cursor.execute("""SELECT dunno FROM dunnos WHERE id=%s""", id)
if cursor.rowcount == 0:
irc.error(msg, 'There is no dunno #%s' % id)
return
try:
replacer = utils.perlReToReplacer(regexp)
except:
irc.error(msg, '%r is not a valid regexp' % regexp)
return
dunno = cursor.fetchone()[0]
new_dunno = replacer(dunno)
cursor.execute("""UPDATE dunnos SET dunno=%s WHERE id=%s""",
new_dunno, id)
self.db.commit()
irc.replySuccess()
Class = Dunno
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

165
plugins/Ebay.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python
###
# Copyright (c) 2003, James Vega
# 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.
###
"""
Accesses eBay.com for various things
"""
import re
import sets
import getopt
__revision__ = "$Id$"
import conf
import utils
import plugins
import ircutils
import privmsgs
import registry
import webutils
import callbacks
def configure(advanced):
from questions import output, expect, anything, something, yn
conf.registerPlugin('Ebay', True)
output("""The Ebay plugin has the functionality to watch for URLs
that match a specific pattern (we call this a snarfer). When
supybot sees such a URL, he will parse the web page for
information and reply with the results.""")
if yn('Do you want the Ebay snarfer enabled by default?'):
conf.supybot.plugins.Ebay.auctionSnarfer.setValue(True)
class EbayError(callbacks.Error):
pass
conf.registerPlugin('Ebay')
conf.registerChannelValue(conf.supybot.plugins.Ebay, 'auctionSnarfer',
registry.Boolean(False, """Determines whether the bot will automatically
'snarf' Ebay auction URLs and print information about them."""))
class Ebay(callbacks.PrivmsgCommandAndRegexp):
"""
Module for eBay stuff. Currently contains a URL snarfer and a command to
get info about an auction.
"""
threaded = True
regexps = ['ebaySnarfer']
_reopts = re.I | re.S
_invalid = re.compile(r'(is invalid, still pending, or no longer in our '
r'database)', _reopts)
_info = re.compile(r'<title>eBay item (\d+) \([^)]+\) - ([^<]+)</title>',
_reopts)
_bid = re.compile(r'((?:Current|Starting) bid):.+?<b>([^<]+?)<fo', _reopts)
_winningBid = re.compile(r'(Winning bid|Sold for):.+?<b>([^<]+?)<font',
_reopts)
_time = re.compile(r'(Time left):.+?<b>([^<]+?)</b>', _reopts)
_bidder = re.compile(r'(High bidder):.+?(?:">(User ID) (kept private)'
r'</font>|<a href[^>]+>([^<]+)</a>.+?'
r'<a href[^>]+>(\d+)</a>)', _reopts)
_winningBidder = re.compile(r'(Winning bidder|Buyer):.+?<a href[^>]+>'
r'([^<]+)</a>.+?<a href[^>]+>(\d+)</a>',_reopts)
_buyNow = re.compile(r'alt="(Buy It Now)">.*?<b>([^<]+)</b>', _reopts)
_seller = re.compile(r'(Seller information).+?<a href[^>]+>([^<]+)</a>'
r'.+ViewFeedback.+">(\d+)</a>', _reopts)
_searches = (_bid, _winningBid, _time, _bidder,
_winningBidder, _buyNow, _seller)
_multiField = (_bidder, _winningBidder, _seller)
def auction(self, irc, msg, args):
"""<item>
Return useful information about the eBay auction with item number
<item>.
"""
item = privmsgs.getArgs(args)
if not item.isdigit():
irc.error('<item> must be an integer value.')
return
url = 'http://cgi.ebay.com/ws/eBayISAPI.dll?ViewItem&item=%s' % item
try:
irc.reply('%s <%s>' % (self._getResponse(url), url))
except EbayError, e:
irc.reply(str(e))
def ebaySnarfer(self, irc, msg, match):
r"http://cgi\.ebay\.(?:com(?:.au)?|ca|co.uk)/(?:.*?/)?(?:ws/)?" \
r"eBayISAPI\.dll\?ViewItem(?:&item=\d+|&category=\d+)+"
if not self.registryValue('auctionSnarfer', msg.args[0]):
return
url = match.group(0)
try:
irc.reply(self._getResponse(url), prefixName=False)
except EbayError, e:
self.log.exception('ebaySnarfer exception at %s:', url)
ebaySnarfer = privmsgs.urlSnarfer(ebaySnarfer)
def _getResponse(self, url):
try:
s = webutils.getUrl(url)
except webutils.WebError, e:
raise EbayError, str(e)
resp = []
m = self._invalid.search(s)
if m:
raise EbayError, 'That auction %s' % m.group(1)
m = self._info.search(s)
if m:
(num, desc) = m.groups()
resp.append('%s%s: %s' % (ircutils.bold('Item #'),
ircutils.bold(num),
utils.htmlToText(desc)))
def bold(L):
return (ircutils.bold(L[0]),) + L[1:]
for r in self._searches:
m = r.search(s)
if m:
if r in self._multiField:
# Have to filter the results from self._bidder since
# 2 of the 5 items in its tuple will always be None.
#self.log.warning(m.groups())
matches = filter(None, m.groups())
resp.append('%s: %s (%s)' % bold(matches))
else:
resp.append('%s: %s' % bold(m.groups()))
if resp:
return '; '.join(resp)
else:
raise EbayError, 'That doesn\'t appear to be a proper eBay ' \
'auction page. (%s)' % \
conf.supybot.replies.possibleBug()
Class = Ebay
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

280
plugins/Enforcer.py Normal file
View File

@ -0,0 +1,280 @@
#!/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.
###
"""
Enforcer: Enforces capabilities on a channel, watching MODEs, KICKs,
JOINs, etc. to make sure they match the channel's config. Also handles
auto-opping, auto-halfopping, or auto-voicing, as well as cycling an otherwise
empty channel in order to get ops.
"""
__revision__ = "$Id$"
import plugins
import conf
import ircdb
import ircmsgs
import plugins
import ircutils
import privmsgs
import registry
import callbacks
def configure(advanced):
from questions import output, expect, anything, something, yn
conf.registerPlugin('Enforcer', True)
chanserv = anything("""What\'s the name of ChanServ on your network? If
there is no ChanServ on your network, just press
enter without entering anything.""")
revenge = yn('Do you want the bot to take revenge on rule breakers?')
conf.supybot.plugins.Enforcer.ChanServ.set(chanserv)
conf.supybot.plugins.Enforcer.takeRevenge.setValue(revenge)
class ValidNickOrEmptyString(registry.String):
def setValue(self, v):
if v and not ircutils.isNick(v):
raise registry.InvalidRegistryValue, \
'Value must be a valid nick or the empty string.'
self.value = v
conf.registerPlugin('Enforcer')
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'autoOp',
registry.Boolean(False, """Determines whether the bot will automatically op
people with the <channel>,op capability when they join the channel."""))
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'autoHalfop',
registry.Boolean(False, """Determines whether the bot will automatically
halfop people with the <channel>,halfop capability when they join the
channel."""))
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'autoVoice',
registry.Boolean(False, """Determines whether the bot will automatically
voice people with the <channel>,voice capability when they join the
channel."""))
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'takeRevenge',
registry.Boolean(False, """Determines whether the bot will take revenge on
people who do things it doesn't like (somewhat like 'bitch mode' in other
IRC bots)."""))
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'takeRevengeOnOps',
registry.Boolean(False, """Determines whether the bot will even take
revenge on ops (people with the #channel,op capability) who violate the
channel configuration."""))
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'cycleToGetOps',
registry.Boolean(True, """Determines whether the bot will cycle the channel
if it doesn't have ops and there's no one else in the channel."""))
# This is a network value, not a channel value.
conf.registerChannelValue(conf.supybot.plugins.Enforcer, 'ChanServ',
ValidNickOrEmptyString('', """Determines what nick the bot will consider to
be the ChanServ on the network. ChanServ (on networks that support it) is
obviously beyond our abilities to enforce, and so we would ignore all
messages from it."""))
_chanCap = ircdb.makeChannelCapability
class Enforcer(callbacks.Privmsg):
"""Manages various things concerning channel security. Check out the
supybot.plugins.Enforcer.autoOp, supybot.plugins.Enforcer.autoHalfop,
supybot.plugins.Enforcer.autoVoice, supybot.plugins.Enforcer.takeRevenge,
supybot.plugins.Enforcer.cycleToGetOps, and
supybot.plugins.Enforcer.ChanServ to configure the behavior of this plugin.
"""
def __init__(self):
callbacks.Privmsg.__init__(self)
self.topics = ircutils.IrcDict()
def doJoin(self, irc, msg):
channel = msg.args[0]
c = ircdb.channels.getChannel(channel)
if c.checkBan(msg.prefix):
irc.queueMsg(ircmsgs.ban(channel, ircutils.banmask(msg.prefix)))
irc.queueMsg(ircmsgs.kick(channel, msg.nick))
elif ircdb.checkCapability(msg.prefix, _chanCap(channel, 'op')):
if self.registryValue('autoOp', channel):
irc.queueMsg(ircmsgs.op(channel, msg.nick))
elif ircdb.checkCapability(msg.prefix, _chanCap(channel, 'halfop')):
if self.registryValue('autoHalfop', channel):
irc.queueMsg(ircmsgs.halfop(channel, msg.nick))
elif ircdb.checkCapability(msg.prefix, _chanCap(channel, 'voice')):
if self.registryValue('autoVoice', channel):
irc.queueMsg(ircmsgs.voice(channel, msg.nick))
def doTopic(self, irc, msg):
channel = msg.args[0]
topic = msg.args[1]
if msg.nick != irc.nick and channel in self.topics and \
not ircdb.checkCapabilities(msg.prefix,
(_chanCap(channel, 'op'),
_chanCap(channel, 'topic'))):
irc.queueMsg(ircmsgs.topic(channel, self.topics[channel]))
if self.registryValue('takeRevenge', channel):
irc.queueMsg(ircmsgs.kick(channel, msg.nick,
conf.supybot.replies.noCapability() %
_chanCap(channel, 'topic')))
else:
self.topics[channel] = msg.args[1]
def do332(self, irc, msg):
# This command gets sent right after joining a channel.
(channel, topic) = msg.args[1:]
self.topics[channel] = topic
def _isProtected(self, channel, hostmask):
capabilities = [_chanCap(channel, 'op'),_chanCap(channel, 'protected')]
return ircdb.checkCapabilities(hostmask, capabilities)
def _isPowerful(self, irc, msg):
if msg.nick == irc.nick:
return True # It's me.
if not ircutils.isUserHostmask(msg.prefix):
return True # It's a server.
chanserv = self.registryValue('ChanServ')
if ircutils.nickEqual(msg.nick, chanserv):
return True # It's ChanServ.
capability = _chanCap(msg.args[0], 'op')
if ircdb.checkCapability(msg.prefix, capability):
return True # It's a chanop.
return False # Default.
def _revenge(self, irc, channel, hostmask):
irc.queueMsg(ircmsgs.ban(channel, ircutils.banmask(hostmask)))
irc.queueMsg(ircmsgs.kick(channel,ircutils.nickFromHostmask(hostmask)))
def doKick(self, irc, msg):
channel = msg.args[0]
kicked = msg.args[1].split(',')
deop = False
if not self._isPowerful(irc, msg) or \
self.registryValue('takeRevengeOnOps', channel):
for nick in kicked:
hostmask = irc.state.nickToHostmask(nick)
if nick == irc.nick:
# Must be a sendMsg so he joins the channel before MODEing.
irc.sendMsg(ircmsgs.join(channel))
deop = True
if self._isProtected(channel, hostmask):
deop = True
irc.queueMsg(ircmsgs.invite(msg.args[1], channel))
if deop:
deop = False
if self.registryValue('takeRevenge', channel):
self._revenge(irc, channel, msg.prefix)
else:
irc.queueMsg(ircmsgs.deop(channel, msg.nick))
def doMode(self, irc, msg):
channel = msg.args[0]
chanserv = self.registryValue('ChanServ', channel)
if not ircutils.isChannel(channel) or \
(self._isPowerful(irc, msg) and
not self.registryValue('takeRevengeOnOps', channel)):
return
for (mode, value) in ircutils.separateModes(msg.args[1:]):
if value == msg.nick:
continue
elif mode == '+o' and value != irc.nick:
hostmask = irc.state.nickToHostmask(value)
if ircdb.checkCapability(channel,
ircdb.makeAntiCapability('op')):
irc.queueMsg(ircmsgs.deop(channel, value))
elif mode == '+h' and value != irc.nick:
hostmask = irc.state.nickToHostmask(value)
if ircdb.checkCapability(channel,
ircdb.makeAntiCapability('halfop')):
irc.queueMsg(ircmsgs.dehalfop(channel, value))
elif mode == '+v' and value != irc.nick:
hostmask = irc.state.nickToHostmask(value)
if ircdb.checkCapability(channel,
ircdb.makeAntiCapability('voice')):
irc.queueMsg(ircmsgs.devoice(channel, value))
elif mode == '-o':
hostmask = irc.state.nickToHostmask(value)
if self._isProtected(channel, hostmask):
irc.queueMsg(ircmsgs.op(channel, value))
if self.registryValue('takeRevenge', channel):
self._revenge(irc, channel, msg.prefix)
else:
irc.queueMsg(ircmsgs.deop(channel, msg.nick))
elif mode == '-h':
hostmask = irc.state.nickToHostmask(value)
if self._isProtected(channel, hostmask):
irc.queueMsg(ircmsgs.halfop(channel, value))
if self.registryValue('takeRevenge', channel):
self._revenge(irc, channel, msg.prefix)
else:
irc.queueMsg(ircmsgs.deop(channel, msg.nick))
elif mode == '-v':
hostmask = irc.state.nickToHostmask(value)
if self._isProtected(channel, hostmask):
irc.queueMsg(ircmsgs.voice(channel, value))
if self.registryValue('takeRevenge', channel):
self._revenge(irc, channel, msg.prefix)
else:
irc.queueMsg(ircmsgs.deop(channel, msg.nick))
elif mode == '+b':
irc.queueMsg(ircmsgs.unban(channel, value))
if self.registryValue('takeRevenge', channel):
self._revenge(irc, channel, msg.prefix)
else:
irc.queueMsg(ircmsgs.deop(channel, msg.nick))
def _cycle(self, irc, channel):
if self.registryValue('cycleToGetOps', channel):
if 'i' not in irc.state.channels[channel].modes:
# What about keywords?
self.log.info('Cycling %s: I\'m the only one left.', channel)
irc.queueMsg(ircmsgs.part(channel))
irc.queueMsg(ircmsgs.join(channel))
else:
self.log.warning('Not cycling %s: it\'s +i', channel)
def doPart(self, irc, msg):
if msg.prefix != irc.prefix:
channel = msg.args[0]
c = irc.state.channels[channel]
if len(c.users) == 1:
if irc.nick not in c.ops:
self._cycle(irc, channel)
def doQuit(self, irc, msg):
for (channel, c) in irc.state.channels.iteritems():
if len(c.users) == 1:
self._cycle(irc, channel)
def __call__(self, irc, msg):
chanserv = self.registryValue('ChanServ', irc.network)
if chanserv:
if ircutils.isUserHostmask(msg.prefix):
if msg.nick != chanserv:
callbacks.Privmsg.__call__(self, irc, msg)
else:
callbacks.Privmsg.__call__(self, irc, msg)
Class = Enforcer
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

444
plugins/Factoids.py Normal file
View File

@ -0,0 +1,444 @@
#!/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.
###
"""
Handles "factoids," little tidbits of information held in a database and
available on demand via several commands.
"""
__revision__ = "$Id$"
import plugins
import time
import getopt
import string
import os.path
from itertools import imap
import conf
import utils
import ircdb
import ircutils
import privmsgs
import registry
import callbacks
import Owner
try:
import sqlite
except ImportError:
raise callbacks.Error, 'You need to have PySQLite installed to use this ' \
'plugin. Download it at <http://pysqlite.sf.net/>'
conf.registerPlugin('Factoids')
conf.registerChannelValue(conf.supybot.plugins.Factoids, 'learnSeparator',
registry.String('as', """Determines what separator must be used in the
learn command. Defaults to 'as' -- learn <key> as <value>. Users might
feel more comfortable with 'is' or something else, so it's
configurable."""))
conf.registerChannelValue(conf.supybot.plugins.Factoids,
'showFactoidIfOnlyOneMatch', registry.Boolean(True, """Determines whether
the bot will reply with the single matching factoid if only one factoid
matches when using the search command."""))
conf.registerChannelValue(conf.supybot.plugins.Factoids,
'replyWhenInvalidCommand', registry.Boolean(True, """Determines whether
the bot will reply to invalid commands by searching for a factoid;
basically making the whatis unnecessary when you want all factoids for a
given key."""))
class Factoids(plugins.ChannelDBHandler, callbacks.Privmsg):
def __init__(self):
callbacks.Privmsg.__init__(self)
plugins.ChannelDBHandler.__init__(self)
def die(self):
callbacks.Privmsg.die(self)
plugins.ChannelDBHandler.die(self)
def makeDb(self, filename):
if os.path.exists(filename):
return sqlite.connect(filename)
db = sqlite.connect(filename)
cursor = db.cursor()
cursor.execute("""CREATE TABLE keys (
id INTEGER PRIMARY KEY,
key TEXT UNIQUE ON CONFLICT IGNORE,
locked BOOLEAN
)""")
cursor.execute("""CREATE TABLE factoids (
id INTEGER PRIMARY KEY,
key_id INTEGER,
added_by TEXT,
added_at TIMESTAMP,
fact TEXT
)""")
cursor.execute("""CREATE TRIGGER remove_factoids
BEFORE DELETE ON keys
BEGIN
DELETE FROM factoids WHERE key_id = old.id;
END
""")
db.commit()
return db
def learn(self, irc, msg, args):
"""[<channel>] <key> as <value>
Associates <key> with <value>. <channel> is only necessary if the
message isn't sent on the channel itself. The word 'as' is necessary
to separate the key from the value. It can be changed to another
word via the learnSeparator registry value.
"""
channel = privmsgs.getChannel(msg, args)
try:
separator = conf.supybot.plugins.Factoids. \
learnSeparator.get(channel)()
i = args.index(separator)
except ValueError:
raise callbacks.ArgumentError
args.pop(i)
key = ' '.join(args[:i])
factoid = ' '.join(args[i:])
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key)
if cursor.rowcount == 0:
cursor.execute("""INSERT INTO keys VALUES (NULL, %s, 0)""", key)
db.commit()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s",key)
(id, locked) = imap(int, cursor.fetchone())
capability = ircdb.makeChannelCapability(channel, 'factoids')
if not locked:
if ircdb.users.hasUser(msg.prefix):
name = ircdb.users.getUser(msg.prefix).name
else:
name = msg.nick
cursor.execute("""INSERT INTO factoids VALUES
(NULL, %s, %s, %s, %s)""",
id, name, int(time.time()), factoid)
db.commit()
irc.replySuccess()
else:
irc.error('That factoid is locked.')
def _lookupFactoid(self, channel, key):
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT factoids.fact FROM factoids, keys
WHERE keys.key LIKE %s AND factoids.key_id=keys.id
ORDER BY factoids.id
LIMIT 20""", key)
return [t[0] for t in cursor.fetchall()]
def _replyFactoids(self, irc, key, factoids, number=0, error=False):
if factoids:
if number:
try:
irc.reply(factoids[number-1])
except IndexError:
irc.error('That\'s not a valid number for that key.')
return
else:
factoidsS = []
counter = 1
for factoid in factoids:
factoidsS.append('(#%s) %s' % (counter, factoid))
counter += 1
irc.replies(factoidsS, prefixer='%r could be ' % key,
joiner=', or ', onlyPrefixFirst=True)
elif error:
irc.error('No factoid matches that key.')
def invalidCommand(self, irc, msg, tokens):
if ircutils.isChannel(msg.args[0]):
channel = msg.args[0]
if self.registryValue('replyWhenInvalidCommand', channel):
key = ' '.join(tokens)
factoids = self._lookupFactoid(channel, key)
self._replyFactoids(irc, key, factoids, error=False)
def whatis(self, irc, msg, args):
"""[<channel>] <key> [<number>]
Looks up the value of <key> in the factoid database. If given a
number, will return only that exact factoid. <channel> is only
necessary if the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
if len(args) > 1 and args[-1].isdigit():
number = args.pop()
else:
number = ''
key = privmsgs.getArgs(args)
if number:
try:
number = int(number)
except ValueError:
irc.error('%s is not a valid number.' % number)
return
else:
number = 0
factoids = self._lookupFactoid(channel, key)
self._replyFactoids(irc, key, factoids, number)
def lock(self, irc, msg, args):
"""[<channel>] <key>
Locks the factoid(s) associated with <key> so that they cannot be
removed or added to. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
key = privmsgs.getArgs(args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=1 WHERE key LIKE %s", key)
db.commit()
irc.replySuccess()
def unlock(self, irc, msg, args):
"""[<channel>] <key>
Unlocks the factoid(s) associated with <key> so that they can be
removed or added to. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
key = privmsgs.getArgs(args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=0 WHERE key LIKE %s", key)
db.commit()
irc.replySuccess()
def forget(self, irc, msg, args):
"""[<channel>] <key> [<number>|*]
Removes the factoid <key> from the factoids database. If there are
more than one factoid with such a key, a number is necessary to
determine which one should be removed. A * can be used to remove all
factoids associated with a key. <channel> is only necessary if
the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
if args[-1].isdigit():
number = int(args.pop())
number -= 1
if number < 0:
irc.error('Negative numbers aren\'t valid.')
return
elif args[-1] == '*':
del args[-1]
number = True
else:
number = None
key = privmsgs.getArgs(args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT keys.id, factoids.id
FROM keys, factoids
WHERE key LIKE %s AND
factoids.key_id=keys.id""", key)
if cursor.rowcount == 0:
irc.error('There is no such factoid.')
elif cursor.rowcount == 1 or number is True:
(id, _) = cursor.fetchone()
cursor.execute("""DELETE FROM factoids WHERE key_id=%s""", id)
cursor.execute("""DELETE FROM keys WHERE key LIKE %s""", key)
db.commit()
irc.replySuccess()
else:
if number is not None:
results = cursor.fetchall()
try:
(_, id) = results[number]
except IndexError:
irc.error('Invalid factoid number.')
return
cursor.execute("DELETE FROM factoids WHERE id=%s", id)
db.commit()
irc.replySuccess()
else:
irc.error('%s factoids have that key. '
'Please specify which one to remove, '
'or use * to designate all of them.' %
cursor.rowcount)
def random(self, irc, msg, args):
"""[<channel>]
Returns a random factoid from the database for <channel>. <channel>
is only necessary if the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT fact, key_id FROM factoids
ORDER BY random()
LIMIT 3""")
if cursor.rowcount != 0:
L = []
for (factoid, id) in cursor.fetchall():
cursor.execute("""SELECT key FROM keys WHERE id=%s""", id)
(key,) = cursor.fetchone()
L.append('"%s": %s' % (ircutils.bold(key), factoid))
irc.reply('; '.join(L))
else:
irc.error('I couldn\'t find a factoid.')
def info(self, irc, msg, args):
"""[<channel>] <key>
Gives information about the factoid(s) associated with <key>.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
channel = privmsgs.getChannel(msg, args)
key = privmsgs.getArgs(args)
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key)
if cursor.rowcount == 0:
irc.error('No factoid matches that key.')
return
(id, locked) = imap(int, cursor.fetchone())
cursor.execute("""SELECT added_by, added_at FROM factoids
WHERE key_id=%s
ORDER BY id""", id)
factoids = cursor.fetchall()
L = []
counter = 0
for (added_by, added_at) in factoids:
counter += 1
added_at = time.strftime(conf.supybot.humanTimestampFormat(),
time.localtime(int(added_at)))
L.append('#%s was added by %s at %s' % (counter,added_by,added_at))
factoids = '; '.join(L)
s = 'Key %r is %s and has %s associated with it: %s' % \
(key, locked and 'locked' or 'not locked',
utils.nItems('factoid', counter), factoids)
irc.reply(s)
def change(self, irc, msg, args):
"""[<channel>] <key> <number> <regexp>
Changes the factoid #<number> associated with <key> according to
<regexp>.
"""
channel = privmsgs.getChannel(msg, args)
(key, number, regexp) = privmsgs.getArgs(args, required=3)
try:
replacer = utils.perlReToReplacer(regexp)
except ValueError, e:
irc.error('Invalid regexp: %s' % e)
return
try:
number = int(number)
if number <= 0:
raise ValueError
except ValueError:
irc.error('Invalid key id.')
return
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT factoids.id, factoids.fact
FROM keys, factoids
WHERE keys.key LIKE %s AND
keys.id=factoids.key_id""", key)
if cursor.rowcount == 0:
irc.error('I couldn\'t find any key %r' % key)
return
elif cursor.rowcount < number:
irc.error('That\'s not a valid key id.')
return
(id, fact) = cursor.fetchall()[number-1]
newfact = replacer(fact)
cursor.execute("UPDATE factoids SET fact=%s WHERE id=%s", newfact, id)
db.commit()
irc.replySuccess()
_sqlTrans = string.maketrans('*?', '%_')
def search(self, irc, msg, args):
"""[<channel>] [--{regexp}=<value>] [<glob>]
Searches the keyspace for keys matching <glob>. If --regexp is given,
it associated value is taken as a regexp and matched against the keys.
"""
channel = privmsgs.getChannel(msg, args)
(optlist, rest) = getopt.getopt(args, '', ['regexp='])
if not optlist and not rest:
raise callbacks.ArgumentError
criteria = []
formats = []
predicateName = 'p'
db = self.getDb(channel)
for (option, arg) in optlist:
if option == '--regexp':
criteria.append('%s(key)' % predicateName)
try:
r = utils.perlReToPythonRe(arg)
except ValueError, e:
irc.error('Invalid regexp: %s' % e)
return
def p(s, r=r):
return int(bool(r.search(s)))
db.create_function(predicateName, 1, p)
predicateName += 'p'
for glob in rest:
if '*' not in glob and '?' not in glob:
glob = '*%s*' % glob
criteria.append('key LIKE %s')
formats.append(glob.translate(self._sqlTrans))
cursor = db.cursor()
sql = """SELECT key FROM keys WHERE %s""" % ' AND '.join(criteria)
cursor.execute(sql, formats)
if cursor.rowcount == 0:
irc.reply('No keys matched that query.')
elif cursor.rowcount == 1 and \
conf.supybot.plugins.Factoids.showFactoidIfOnlyOneMatch.get(channel)():
self.whatis(irc, msg, [cursor.fetchone()[0]])
elif cursor.rowcount > 100:
irc.reply('More than 100 keys matched that query; '
'please narrow your query.')
else:
keys = [repr(t[0]) for t in cursor.fetchall()]
s = utils.commaAndify(keys)
irc.reply(s)
Class = Factoids
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

454
plugins/Filter.py Normal file
View File

@ -0,0 +1,454 @@
#!/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.
###
"""
Provides numerous filters, and a command (outfilter) to set them as filters on
the output of the bot.
"""
__revision__ = "$Id$"
import plugins
import re
import string
import random
import conf
import utils
import ircmsgs
import ircutils
import privmsgs
import callbacks
class MyFilterProxy(object):
def reply(self, s):
self.s = s
class Filter(callbacks.Privmsg):
def __init__(self):
self.outFilters = ircutils.IrcDict()
callbacks.Privmsg.__init__(self)
def outFilter(self, irc, msg):
if msg.command == 'PRIVMSG':
if msg.args[0] in self.outFilters:
if ircmsgs.isAction(msg):
s = ircmsgs.unAction(msg)
else:
s = msg.args[1]
methods = self.outFilters[msg.args[0]]
for filtercommand in methods:
myIrc = MyFilterProxy()
filtercommand(myIrc, msg, [s])
s = myIrc.s
if ircmsgs.isAction(msg):
msg = ircmsgs.action(msg.args[0], s)
else:
msg = ircmsgs.IrcMsg(msg=msg, args=(msg.args[0], s))
return msg
_filterCommands = ['jeffk', 'leet', 'rot13', 'hexlify', 'binary', 'lithp',
'scramble', 'morse', 'reverse', 'colorize', 'squish',
'supa1337', 'colorstrip']
def outfilter(self, irc, msg, args, channel):
"""[<channel>] [<command>]
Sets the outFilter of this plugin to be <command>. If no command is
given, unsets the outFilter. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
command = privmsgs.getArgs(args, required=0, optional=1)
if command:
command = callbacks.canonicalName(command)
if command in self._filterCommands:
method = getattr(self, command)
self.outFilters.setdefault(channel, []).append(method)
irc.replySuccess()
else:
irc.error('That\'s not a valid filter command.')
else:
self.outFilters[channel] = []
irc.replySuccess()
outfilter = privmsgs.checkChannelCapability(outfilter, 'op')
def squish(self, irc, msg, args):
"""<text>
Removes all the spaces from <text>.
"""
text = privmsgs.getArgs(args)
text = ''.join(text.split())
irc.reply(text)
def binary(self, irc, msg, args):
"""<text>
Returns the binary representation of <text>.
"""
L = []
for c in privmsgs.getArgs(args):
LL = []
i = ord(c)
counter = 8
while i:
counter -= 1
if i & 1:
LL.append('1')
else:
LL.append('0')
i >>= 1
while counter:
LL.append('0')
counter -= 1
LL.reverse()
L.extend(LL)
irc.reply(''.join(L))
def hexlify(self, irc, msg, args):
"""<text>
Returns a hexstring from the given string; a hexstring is a string
composed of the hexadecimal value of each character in the string
"""
text = privmsgs.getArgs(args)
irc.reply(text.encode('hex_codec'))
def unhexlify(self, irc, msg, args):
"""<hexstring>
Returns the string corresponding to <hexstring>. Obviously,
<hexstring> must be a string of hexadecimal digits.
"""
text = privmsgs.getArgs(args)
try:
irc.reply(text.decode('hex_codec'))
except TypeError:
irc.error('Invalid input.')
def rot13(self, irc, msg, args):
"""<text>
Rotates <text> 13 characters to the right in the alphabet. Rot13 is
commonly used for text that simply needs to be hidden from inadvertent
reading by roaming eyes, since it's easily reversible.
"""
text = privmsgs.getArgs(args)
irc.reply(text.encode('rot13'))
def lithp(self, irc, msg, args):
"""<text>
Returns the lisping version of <text>
"""
text = privmsgs.getArgs(args)
text = text.replace('sh', 'th')
text = text.replace('SH', 'TH')
text = text.replace('Sh', 'Th')
text = text.replace('ss', 'th')
text = text.replace('SS', 'TH')
text = text.replace('s', 'th')
text = text.replace('z', 'th')
text = text.replace('S', 'Th')
text = text.replace('Z', 'Th')
text = text.replace('x', 'kth')
text = text.replace('X', 'KTH')
text = text.replace('cce', 'kth')
text = text.replace('CCE', 'KTH')
text = text.replace('tion', 'thion')
text = text.replace('TION', 'THION')
irc.reply(text)
_leettrans = string.maketrans('oOaAeElBTiIts', '004433187!1+5')
_leetres = [(re.compile(r'\b(?:(?:[yY][o0O][oO0uU])|u)\b'), 'j00'),
(re.compile(r'fear'), 'ph33r'),
(re.compile(r'[aA][tT][eE]'), '8'),
(re.compile(r'[aA][tT]'), '@'),
(re.compile(r'[sS]\b'), 'z'),
(re.compile(r'x'), '><'),]
def leet(self, irc, msg, args):
"""<text>
Returns the l33tspeak version of <text>
"""
s = privmsgs.getArgs(args)
for (r, sub) in self._leetres:
s = re.sub(r, sub, s)
s = s.translate(self._leettrans)
irc.reply(s)
_supaleetreplacers = [('xX', '><'), ('kK', '|<'), ('rR', '|2'),
('hH', '|-|'), ('L', '|_'), ('uU', '|_|'),
('O', '()'), ('nN', '|\\|'), ('mM', '/\\/\\'),
('G', '6'), ('Ss', '$'), ('i', ';'), ('aA', '/-\\'),
('eE', '3'), ('t', '+'), ('T', '7'), ('l', '1'),
('D', '|)'), ('B', '|3'), ('I', ']['), ('Vv', '\\/'),
('wW', '\\/\\/'), ('d', 'c|'), ('b', '|>'),
('c', '<'), ('h', '|n'),]
def supa1337(self, irc, msg, args):
"""<text>
Replies with an especially k-rad translation of <text>.
"""
s = privmsgs.getArgs(args)
for (r, sub) in self._leetres:
s = re.sub(r, sub, s)
for (letters, replacement) in self._supaleetreplacers:
for letter in letters:
s = s.replace(letter, replacement)
irc.reply(s)
_scrambleRe = re.compile(r'(?:\b|(?![a-zA-Z]))([a-zA-Z])([a-zA-Z]*)'
r'([a-zA-Z])(?:\b|(?![a-zA-Z]))')
def scramble(self, irc, msg, args):
"""<text>
Replies with a string where each word is scrambled; i.e., each internal
letter (that is, all letters but the first and last) are shuffled.
"""
def _subber(m):
L = list(m.group(2))
random.shuffle(L)
return '%s%s%s' % (m.group(1), ''.join(L), m.group(3))
text = privmsgs.getArgs(args)
s = self._scrambleRe.sub(_subber, text)
irc.reply(s)
_code = {
"A" : ".-",
"B" : "-...",
"C" : "-.-.",
"D" : "-..",
"E" : ".",
"F" : "..-.",
"G" : "--.",
"H" : "....",
"I" : "..",
"J" : ".---",
"K" : "-.-",
"L" : ".-..",
"M" : "--",
"N" : "-.",
"O" : "---",
"P" : ".--.",
"Q" : "--.-",
"R" : ".-.",
"S" : "...",
"T" : "-",
"U" : "..-",
"V" : "...-",
"W" : ".--",
"X" : "-..-",
"Y" : "-.--",
"Z" : "--..",
"0" : "-----",
"1" : ".----",
"2" : "..---",
"3" : "...--",
"4" : "....-",
"5" : ".....",
"6" : "-....",
"7" : "--...",
"8" : "---..",
"9" : "----.",
"." : ".-.-.-",
"," : "--..--",
":" : "---...",
"?" : "..--..",
"'" : ".----.",
"-" : "-....-",
"/" : "-..-.",
'"' : ".-..-.",
"@" : ".--.-.",
"=" : "-...-"
}
_revcode = dict([(y, x) for (x, y) in _code.items()])
_unmorsere = re.compile('([.-]+)')
def unmorse(self, irc, msg, args):
"""<morse code text>
Does the reverse of the morse/ditdaw command.
"""
text = privmsgs.getArgs(args)
text = text.replace('_', '-')
def morseToLetter(m):
s = m.group(1)
return self._revcode.get(s, s)
text = self._unmorsere.sub(morseToLetter, text)
text = text.replace(' ', '\x00')
text = text.replace(' ', '')
text = text.replace('\x00', ' ')
irc.reply(text)
def morse(self, irc, msg, args):
"""<text>
Gives the more code equivalent of a given string.
"""
text = privmsgs.getArgs(args)
L = []
for c in text.upper():
if c in self._code:
L.append(self._code[c])
else:
L.append(c)
irc.reply(' '.join(L))
def reverse(self, irc, msg, args):
"""<text>
Reverses <text>.
"""
text = privmsgs.getArgs(args)
irc.reply(text[::-1])
def _color(self, c):
if c == ' ':
return c
fg = str(random.randint(2, 15)).zfill(2)
return '\x03%s%s' % (fg, c)
def colorize(self, irc, msg, args):
"""<text>
Returns <text> with each character randomly colorized.
"""
text = privmsgs.getArgs(args)
L = [self._color(c) for c in text]
irc.reply('%s%s' % (''.join(L), '\x03'))
def stripcolor(self, irc, msg, args):
"""<text>
Returns <text> stripped of all color codes.
"""
text = privmsgs.getArgs(args)
irc.reply(ircutils.stripColor(text))
def jeffk(self, irc, msg, args):
"""<text>
Returns <text> as if JeffK had said it himself.
"""
def randomlyPick(L):
return random.choice(L)
def quoteOrNothing(m):
return randomlyPick(['"', '']).join(m.groups())
def randomlyReplace(s, probability=0.5):
def f(m):
if random.random() < probability:
return m.expand(s)
else:
return m.group(0)
return f
def randomExclaims(m):
if random.random() < 0.85:
return ('!' * random.randrange(1, 5)) + m.group(1)
else:
return '.' + m.group(1)
def randomlyShuffle(m):
L = list(m.groups())
random.shuffle(L)
return ''.join(L)
def lessRandomlyShuffle(m):
L = list(m.groups())
if random.random() < .4:
random.shuffle(L)
return ''.join(L)
def randomlyLaugh(text, probability=.3):
if random.random() < probability:
if random.random() < .5:
insult = random.choice([' fagot1', ' fagorts', ' jerks',
'fagot' ' jerk', ' dumbshoes',
' dumbshoe'])
else:
insult = ''
laugh1 = random.choice(['ha', 'hah', 'lol', 'l0l', 'ahh'])
laugh2 = random.choice(['ha', 'hah', 'lol', 'l0l', 'ahh'])
laugh1 = laugh1 * random.randrange(1, 5)
laugh2 = laugh2 * random.randrange(1, 5)
exclaim = random.choice(['!', '~', '!~', '~!!~~',
'!!~', '~~~!'])
exclaim += random.choice(['!', '~', '!~', '~!!~~',
'!!~', '~~~!'])
if random.random() < 0.5:
exclaim += random.choice(['!', '~', '!~', '~!!~~',
'!!~', '~~~!'])
laugh = ''.join([' ', laugh1, laugh2, insult, exclaim])
text += laugh
return text
text = privmsgs.getArgs(args)
if random.random() < .03:
irc.reply(randomlyLaugh('NO YUO', probability=1))
return
alwaysInsertions = {
r'er\b': 'ar',
r'\bthe\b': 'teh',
r'\byou\b': 'yuo',
r'\bis\b': 'si',
r'\blike\b': 'liek',
r'[^e]ing\b': 'eing',
}
for (r, s) in alwaysInsertions.iteritems():
text = re.sub(r, s, text)
randomInsertions = {
r'i': 'ui',
r'le\b': 'al',
r'i': 'io',
r'l': 'll',
r'to': 'too',
r'that': 'taht',
r'[^s]c([ei])': r'sci\1',
r'ed\b': r'e',
r'\band\b': 'adn',
r'\bhere\b': 'hear',
r'\bthey\'re': 'their',
r'\bthere\b': 'they\'re',
r'\btheir\b': 'there',
r'[^e]y': 'ey',
}
for (r, s) in randomInsertions.iteritems():
text = re.sub(r, randomlyReplace(s), text)
text = re.sub(r'(\w)\'(\w)', quoteOrNothing, text)
text = re.sub(r'\.(\s+|$)', randomExclaims, text)
text = re.sub(r'([aeiou])([aeiou])', randomlyShuffle, text)
text = re.sub(r'([bcdfghkjlmnpqrstvwxyz])([bcdfghkjlmnpqrstvwxyz])',
lessRandomlyShuffle, text)
text = randomlyLaugh(text)
if random.random() < .4:
text = text.upper()
irc.reply(text)
Class = Filter
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

63
plugins/FixRelayBot.py Normal file
View File

@ -0,0 +1,63 @@
#!/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.
###
"""
Allows people behind a relaybot (which uses <nick@network> to relay messages)
to access the bot.
"""
__revision__ = "$Id$"
import plugins
import re
import random
import irclib
import ircmsgs
import ircutils
class FixRelayBot(irclib.IrcCallback):
_re = re.compile(r'<([^@]+)@[^>]+>\s+(.*)')
def inFilter(self, irc, msg):
if msg.command == 'PRIVMSG':
m = self._re.match(msg.args[1])
if m:
nick = m.group(1)
host = random.random()*100
newprefix = ircutils.joinHostmask(nick, nick, host)
msg = ircmsgs.IrcMsg(command='PRIVMSG', prefix=newprefix,
args=(msg.args[0], m.group(2)))
return msg
Class = FixRelayBot
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

82
plugins/Friendly.py Executable file
View File

@ -0,0 +1,82 @@
#!/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.
###
"""
Just a regexp module to make the bot a wee bit friendlier.
"""
__revision__ = "$Id$"
deprecated = True
import plugins
import ircutils
import callbacks
class Friendly(callbacks.PrivmsgRegexp):
onlyFirstMatch = True
def greet(self, irc, msg, match):
r"^(?:heya?|(?:w(?:hat'?s\b|as)s?up)|howdy|hi|hello)$"
if irc.nick in msg.args[1]:
s = 'howdy, %s :)' % msg.nick
irc.reply(s, prefixName=False)
def greet2(self, irc, msg, match):
r"^(?:heya?|(?:w(?:hat'?s\b|as)s*up)|howdy|hi|hello)" \
r"(?:,\s*|\s+)" \
r"([0-9A-Za-z_\[\]\`^{}\|-]+)[^A-Za-z_\[\]\`^{}\|-]*$"
if ircutils.nickEqual(match.group(1), irc.nick):
s = 'howdy, %s :)' % msg.nick
irc.reply(s, prefixName=False)
def goodbye(self, irc, msg, match):
r"(?:good)?bye|adios|vale|ciao|au revoir|seeya|night$"
if irc.nick in msg.args[1]:
s = 'seeya, %s!' % msg.nick
irc.reply(s, prefixName=False)
def exclaim(self, irc, msg, match):
r"^([^\s]+)!$"
if match.group(1) == irc.nick:
s = msg.nick + '!'
irc.reply(s, prefixName=False)
def beGracious(self, irc, msg, match):
r"\b(?:thank'?s?|thx|tnks?)\b"
if irc.nick in msg.args[1]:
s = 'you\'re welcome, %s' % msg.nick
irc.reply(s, prefixName=False)
Class = Friendly
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

362
plugins/Fun.py Normal file
View File

@ -0,0 +1,362 @@
#!/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.
###
"""
Provides a multitude of fun, useless commands.
"""
__revision__ = "$Id$"
import plugins
import gc
import re
import sys
import md5
import sha
import random
import urllib
import inspect
import mimetypes
from itertools import imap
import conf
import utils
import ircmsgs
import ircutils
import privmsgs
import callbacks
class MyFunProxy(object):
def reply(self, msg, s):
self.s = s
class Fun(callbacks.Privmsg):
def __init__(self):
self.outFilters = ircutils.IrcDict()
callbacks.Privmsg.__init__(self)
def ping(self, irc, msg, args):
"""takes no arguments
Checks to see if the bot is alive.
"""
irc.reply('pong', prefixName=False)
def hexip(self, irc, msg, args):
"""<ip>
Returns the hexadecimal IP for that IP.
"""
ip = privmsgs.getArgs(args)
if not utils.isIP(ip):
irc.error('%r is not a valid IP.' % ip)
return
quads = ip.split('.')
ret = ""
for quad in quads:
i = int(quad)
ret += '%02x' % i
irc.reply(ret.upper())
def ord(self, irc, msg, args):
"""<letter>
Returns the 8-bit value of <letter>.
"""
letter = privmsgs.getArgs(args)
if len(letter) != 1:
irc.error('Letter must be of length 1 (for obvious reasons)')
else:
irc.reply(str(ord(letter)))
def chr(self, irc, msg, args):
"""<number>
Returns the character associated with the 8-bit value <number>
"""
try:
i = privmsgs.getArgs(args)
if i.startswith('0x'):
base = 16
elif i.startswith('0b'):
base = 2
i = i[2:]
elif i.startswith('0'):
base = 8
else:
base = 10
i = int(i, base)
irc.reply(chr(i))
except ValueError:
irc.error('That number doesn\'t map to an 8-bit character.')
def encode(self, irc, msg, args):
"""<encoding> <text>
Returns an encoded form of the given text; the valid encodings are
available in the documentation of the Python codecs module:
<http://www.python.org/doc/lib/node127.html>.
"""
encoding, text = privmsgs.getArgs(args, required=2)
try:
irc.reply(text.encode(encoding))
except LookupError:
irc.error('There is no such encoding %r' % encoding)
def decode(self, irc, msg, args):
"""<encoding> <text>
Returns an un-encoded form of the given text; the valid encodings are
available in the documentation of the Python codecs module:
<http://www.python.org/doc/lib/node127.html>.
"""
encoding, text = privmsgs.getArgs(args, required=2)
try:
irc.reply(text.decode(encoding).encode('utf-8'))
except LookupError:
irc.error('There is no such encoding %r' % encoding)
def xor(self, irc, msg, args):
"""<password> <text>
Returns <text> XOR-encrypted with <password>. See
http://www.yoe.org/developer/xor.html for information about XOR
encryption.
"""
(password, text) = privmsgs.getArgs(args, 2)
passwordlen = len(password)
i = 0
ret = []
for c in text:
ret.append(chr(ord(c) ^ ord(password[i])))
i = (i + 1) % passwordlen
irc.reply(''.join(ret))
def mimetype(self, irc, msg, args):
"""<filename>
Returns the mime type associated with <filename>
"""
filename = privmsgs.getArgs(args)
(type, encoding) = mimetypes.guess_type(filename)
if type is not None:
irc.reply(type)
else:
s = 'I couldn\'t figure out that filename.'
irc.reply(s)
def md5(self, irc, msg, args):
"""<text>
Returns the md5 hash of a given string. Read
http://www.rsasecurity.com/rsalabs/faq/3-6-6.html for more information
about md5.
"""
text = privmsgs.getArgs(args)
irc.reply(md5.md5(text).hexdigest())
def sha(self, irc, msg, args):
"""<text>
Returns the SHA hash of a given string. Read
http://www.secure-hash-algorithm-md5-sha-1.co.uk/ for more information
about SHA.
"""
text = privmsgs.getArgs(args)
irc.reply(sha.sha(text).hexdigest())
def urlquote(self, irc, msg, args):
"""<text>
Returns the URL quoted form of the text.
"""
text = privmsgs.getArgs(args)
irc.reply(urllib.quote(text))
def urlunquote(self, irc, msg, args):
"""<text>
Returns the text un-URL quoted.
"""
text = privmsgs.getArgs(args)
s = urllib.unquote(text)
irc.reply(s)
def coin(self, irc, msg, args):
"""takes no arguments
Flips a coin and returns the result.
"""
if random.randrange(0, 2):
irc.reply('heads')
else:
irc.reply('tails')
_dicere = re.compile(r'(\d+)d(\d+)')
def dice(self, irc, msg, args):
"""<dice>d<sides>
Rolls a die with <sides> number of sides <dice> times.
For example, 2d6 will roll 2 six-sided dice; 10d10 will roll 10
ten-sided dice.
"""
arg = privmsgs.getArgs(args)
m = re.match(self._dicere, arg)
if m:
(dice, sides) = imap(int, m.groups())
if dice > 6:
irc.error('You can\'t roll more than 6 dice.')
elif sides > 100:
irc.error('Dice can\'t have more than 100 sides.')
else:
L = [0] * dice
for i in xrange(dice):
L[i] = random.randrange(1, sides+1)
irc.reply(utils.commaAndify([str(x) for x in L]))
else:
irc.error('Dice must be of the form <dice>d<sides>')
def objects(self, irc, msg, args):
"""takes no arguments.
Returns the number and types of Python objects in memory.
"""
classes = 0
functions = 0
modules = 0
strings = 0
dicts = 0
lists = 0
tuples = 0
refcounts = 0
objs = gc.get_objects()
for obj in objs:
if isinstance(obj, str):
strings += 1
if isinstance(obj, tuple):
tuples += 1
elif inspect.isroutine(obj):
functions += 1
elif isinstance(obj, dict):
dicts += 1
elif isinstance(obj, list):
lists += 1
elif inspect.isclass(obj):
classes += 1
elif inspect.ismodule(obj):
modules += 1
refcounts += sys.getrefcount(obj)
response = 'I have %s objects: %s modules, %s classes, %s functions, '\
'%s dictionaries, %s lists, %s tuples, %s strings, and a ' \
'few other odds and ends. ' \
'I have a total of %s references.' % \
(len(objs), modules, classes, functions,
dicts, lists, tuples, strings, refcounts)
irc.reply(response)
def levenshtein(self, irc, msg, args):
"""<string1> <string2>
Returns the levenshtein distance (also known as the "edit distance"
between <string1> and <string2>
"""
(s1, s2) = privmsgs.getArgs(args, required=2)
irc.reply(str(utils.distance(s1, s2)))
def soundex(self, irc, msg, args):
"""<string> [<length>]
Returns the Soundex hash to a given length. The length defaults to
4, since that's the standard length for a soundex hash. For unlimited
length, use 0.
"""
(s, length) = privmsgs.getArgs(args, optional=1)
if length:
try:
length = int(length)
except ValueError:
irc.error('%r isn\'t a valid length.' % length)
return
else:
length = 4
irc.reply(utils.soundex(s, length))
_eightballs = (
'outlook not so good.',
'my reply is no.',
'don\'t count on it.',
'you may rely on it.',
'ask again later.',
'most likely.',
'cannot predict now.',
'yes.',
'yes, most definitely.',
'better not tell you now.',
'it is certain.',
'very doubtful.',
'it is decidedly so.',
'concentrate and ask again.',
'signs point to yes.',
'my sources say no.',
'without a doubt.',
'reply hazy, try again.',
'as I see it, yes.',
)
def eightball(self, irc, msg, args):
"""[<question>]
Asks the magic eightball a question.
"""
irc.reply(random.choice(self._eightballs))
def roulette(self, irc, msg, args):
"""takes no arguments
Randomly picks a number from 1 to 6 and if it comes up 1, you get
kicked. Otherwise -- *click*
"""
nick = msg.nick
channel = msg.args[0]
if not ircutils.isChannel(channel):
irc.error('This message must be sent in a channel.')
if random.randint(1, 6) == 1:
if irc.nick in irc.state.channels[channel].ops:
irc.queueMsg(ircmsgs.kick(channel, nick, 'BANG!'))
else:
irc.reply('*BANG* Hey, who put a blank in here?!')
else:
irc.reply('*click*')
Class = Fun
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

536
plugins/FunDB.py Executable file
View File

@ -0,0 +1,536 @@
#!/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.
###
"""
Provides fun commands that require a database to operate.
"""
__revision__ = "$Id$"
import plugins
import re
import sets
import time
import getopt
import string
import os.path
from itertools import imap
import registry
import conf
import ircdb
import utils
import world
import ircmsgs
import ircutils
import privmsgs
import callbacks
try:
import sqlite
except ImportError:
raise callbacks.Error, 'You need to have PySQLite installed to use this ' \
'plugin. Download it at <http://pysqlite.sf.net/>'
tableCreateStatements = {
'larts': ("""CREATE TABLE larts (
id INTEGER PRIMARY KEY,
lart TEXT,
added_by TEXT
)""",),
'praises': ("""CREATE TABLE praises (
id INTEGER PRIMARY KEY,
praise TEXT,
added_by TEXT
)""",),
'insults': ("""CREATE TABLE insults (
id INTEGER PRIMARY KEY,
insult TEXT,
added_by TEXT
)""",),
'excuses': ("""CREATE TABLE excuses (
id INTEGER PRIMARY KEY,
excuse TEXT,
added_by TEXT
)""",),
}
conf.registerPlugin('FunDB')
conf.registerChannelValue(conf.supybot.plugins.FunDB, 'showIds',
registry.Boolean(True, """Determine whether the bot will show the id of an
excuse/insult/praise/lart."""))
class FunDB(callbacks.Privmsg, plugins.ChannelDBHandler):
"""
Contains the 'fun' commands that require a database. Currently includes
database-backed commands for crossword puzzle solving, anagram searching,
larting, praising, excusing, and insulting.
"""
_tables = sets.Set(['lart', 'insult', 'excuse', 'praise'])
def __init__(self):
callbacks.Privmsg.__init__(self)
plugins.ChannelDBHandler.__init__(self)
def die(self):
callbacks.Privmsg.die(self)
plugins.ChannelDBHandler.die(self)
def makeDb(self, dbfilename, replace=False):
if os.path.exists(dbfilename):
if replace:
os.remove(dbfilename)
db = sqlite.connect(dbfilename)
cursor = db.cursor()
for table in tableCreateStatements:
try:
cursor.execute("""SELECT * FROM %s LIMIT 1""" % table)
except sqlite.DatabaseError: # The table doesn't exist.
for sql in tableCreateStatements[table]:
cursor.execute(sql)
db.commit()
return db
def add(self, irc, msg, args):
"""[<channel>] <lart|excuse|insult|praise> <text>
Adds another record to the data referred to in the first argument. For
commands that will later respond with an ACTION (lart and praise), $who
should be in the message to show who should be larted or praised. I.e.
'fundb add lart slices $who in half with a free AOL cd' would make the
bot, when it used that lart against, say, jemfinch, to say '/me slices
jemfinch in half with a free AOL cd' <channel> is only necessary if
the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(table, s) = privmsgs.getArgs(args, required=2)
table = table.lower()
try:
name = ircdb.users.getUser(msg.prefix).name
except KeyError:
irc.errorNotRegistered()
return
if table == "lart" or table == "praise":
if '$who' not in s:
irc.error('There must be a $who in the lart/praise somewhere')
return
elif table not in self._tables:
irc.error('"%s" is not valid. Valid values include %s.' %
(table, utils.commaAndify(self._tables)))
return
db = self.getDb(channel)
cursor = db.cursor()
sql = """INSERT INTO %ss VALUES (NULL, %%s, %%s)""" % table
cursor.execute(sql, s, name)
db.commit()
sql = """SELECT id FROM %ss WHERE %s=%%s""" % (table, table)
cursor.execute(sql, s)
id = cursor.fetchone()[0]
irc.replySuccess('(%s #%s added)' % (table, id))
def remove(self, irc, msg, args):
"""[<channel>] <lart|excuse|insult|praise> <id>
Removes the data, referred to in the first argument, with the id
number <id> from the database. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(table, id) = privmsgs.getArgs(args, required=2)
table = table.lower()
try:
ircdb.users.getUser(msg.prefix).name
except KeyError:
irc.errorNotRegistered()
return
try:
id = int(id)
except ValueError:
irc.error('The <id> argument must be an integer.')
return
if table not in self._tables:
irc.error('"%s" is not valid. Valid values include %s.' %
(table, utils.commaAndify(self._tables)))
return
db = self.getDb(channel)
cursor = db.cursor()
sql = """DELETE FROM %ss WHERE id=%%s""" % table
cursor.execute(sql, id)
db.commit()
irc.replySuccess()
def change(self, irc, msg, args):
"""[<channel>] <lart|excuse|insult|praise> <id> <regexp>
Changes the data, referred to in the first argument, with the id
number <id> according to the regular expression <regexp>. <id> is the
zero-based index into the db; <regexp> is a regular expression of the
form s/regexp/replacement/flags. <channel> is only necessary if the
message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(table, id, regexp) = privmsgs.getArgs(args, required=3)
table = table.lower()
try:
name = ircdb.users.getUser(msg.prefix).name
except KeyError:
irc.errorNotRegistered()
return
try:
id = int(id)
except ValueError:
irc.error('The <id> argument must be an integer.')
return
if table not in self._tables:
irc.error('"%s" is not valid. Valid values include %s.' %
(table, utils.commaAndify(self._tables)))
return
try:
replacer = utils.perlReToReplacer(regexp)
except ValueError, e:
irc.error('The regexp wasn\'t valid: %s.' % e.args[0])
except re.error, e:
irc.error(utils.exnToString(e))
return
db = self.getDb(channel)
cursor = db.cursor()
sql = """SELECT %s FROM %ss WHERE id=%%s""" % (table, table)
cursor.execute(sql, id)
if cursor.rowcount == 0:
irc.error('There is no such %s.' % table)
else:
old_entry = cursor.fetchone()[0]
new_entry = replacer(old_entry)
sql = """UPDATE %ss SET %s=%%s, added_by=%%s WHERE id=%%s""" % \
(table, table)
cursor.execute(sql, new_entry, name, id)
db.commit()
irc.replySuccess()
def stats(self, irc, msg, args):
"""[<channel>] <lart|excuse|insult|praise>
Returns the number of records, of the type specified, currently in
the database. <channel> is only necessary if the message isn't sent
in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
table = privmsgs.getArgs(args)
table = table.lower()
if table not in self._tables:
irc.error('%r is not valid. Valid values include %s.' %
(table, utils.commaAndify(self._tables)))
return
db = self.getDb(channel)
cursor = db.cursor()
sql = """SELECT count(*) FROM %ss""" % table
cursor.execute(sql)
total = int(cursor.fetchone()[0])
irc.reply('There %s currently %s in my database.' %
(utils.be(total), utils.nItems(table, total)))
def get(self, irc, msg, args):
"""[<channel>] <lart|excuse|insult|praise> <id>
Gets the record with id <id> from the table specified. <channel> is
only necessary if the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(table, id) = privmsgs.getArgs(args, required=2)
table = table.lower()
try:
id = int(id)
except ValueError:
irc.error('The <id> argument must be an integer.')
return
if table not in self._tables:
irc.error('"%s" is not valid. Valid values include %s.' %
(table, utils.commaAndify(self._tables)))
return
db = self.getDb(channel)
cursor = db.cursor()
sql = """SELECT %s FROM %ss WHERE id=%%s""" % (table, table)
cursor.execute(sql, id)
if cursor.rowcount == 0:
irc.error('There is no such %s.' % table)
else:
reply = cursor.fetchone()[0]
irc.reply(reply)
def info(self, irc, msg, args):
"""[<channel>] <lart|excuse|insult|praise> <id>
Gets the info for the record with id <id> from the table specified.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
channel = privmsgs.getChannel(msg, args)
(table, id) = privmsgs.getArgs(args, required=2)
table = table.lower()
try:
id = int(id)
except ValueError:
irc.error('The <id> argument must be an integer.')
return
if table not in self._tables:
irc.error('"%s" is not valid. Valid values include %s.' %
(table, utils.commaAndify(self._tables)))
return
db = self.getDb(channel)
cursor = db.cursor()
sql = """SELECT added_by FROM %ss WHERE id=%%s""" % table
cursor.execute(sql, id)
if cursor.rowcount == 0:
irc.error('There is no such %s.' % table)
else:
add = cursor.fetchone()[0]
reply = '%s #%s: Created by %s.' % (table, id, add)
irc.reply(reply)
def _formatResponse(self, s, id, showids):
if showids:
return '%s (#%s)' % (s, id)
else:
return s
def insult(self, irc, msg, args):
"""[<channel>] <nick>
Insults <nick>. <channel> is only necessary if the message isn't
sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
nick = privmsgs.getArgs(args)
if not nick:
raise callbacks.ArgumentError
db = self.getDb(channel)
cursor = db.cursor()
cursor.execute("""SELECT id, insult FROM insults
WHERE insult NOT NULL
ORDER BY random()
LIMIT 1""")
if cursor.rowcount == 0:
irc.error('There are currently no available insults.')
else:
(id, insult) = cursor.fetchone()
nick = re.sub(r'\bme\b', msg.nick, nick)
nick = re.sub(r'\bmy\b', '%s\'s' % msg.nick, nick)
insult = insult.replace('$who', nick)
showid = self.registryValue('showIds', channel)
irc.reply(self._formatResponse(insult, id, showid), to=nick)
def excuse(self, irc, msg, args):
"""[<channel>] [<id>]
Gives you a standard, random BOFH excuse or the excuse with the given
<id>. <channel> is only necessary if the message isn't sent in the
channel itself.
"""
channel = privmsgs.getChannel(msg, args)
id = privmsgs.getArgs(args, required=0, optional=1)
db = self.getDb(channel)
cursor = db.cursor()
if id:
try:
id = int(id)
except ValueError:
irc.error('The <id> argument must be an integer.')
return
cursor.execute("""SELECT id, excuse FROM excuses WHERE id=%s""",
id)
if cursor.rowcount == 0:
irc.error('There is no such excuse.')
return
else:
cursor.execute("""SELECT id, excuse FROM excuses
WHERE excuse NOTNULL
ORDER BY random()
LIMIT 1""")
if cursor.rowcount == 0:
irc.error('There are currently no available excuses.')
else:
(id, excuse) = cursor.fetchone()
showid = self.registryValue('showIds', channel)
irc.reply(self._formatResponse(excuse, id, showid))
def lart(self, irc, msg, args):
"""[<channel>] [<id>] <text> [for <reason>]
Uses a lart on <text> (giving the reason, if offered). Will use lart
number <id> from the database when <id> is given. <channel> is only
necessary if the message isn't sent in the channel itself.
"""
channel = privmsgs.getChannel(msg, args)
(id, nick) = privmsgs.getArgs(args, optional=1)
try:
id = int(id)
if id < 1:
irc.error('There is no such lart.')
return
except ValueError:
nick = ' '.join([id, nick]).strip()
id = 0
nick = nick.rstrip('.')
if not nick:
raise callbacks.ArgumentError
if nick == irc.nick:
nick = msg.nick
try:
(nick, reason) = imap(' '.join,
utils.itersplit('for'.__eq__, nick.split(), 1))
except ValueError:
reason = ''
db = self.getDb(channel)
cursor = db.cursor()
if id:
cursor.execute("""SELECT id, lart FROM larts WHERE id=%s""", id)
if cursor.rowcount == 0:
irc.error('There is no such lart.')
return
else:
cursor.execute("""SELECT id, lart FROM larts
WHERE lart NOTNULL
ORDER BY random()
LIMIT 1""")
if cursor.rowcount == 0:
irc.error('There are currently no available larts.')
else:
(id, lart) = cursor.fetchone()
nick = re.sub(r'\bme\b', msg.nick, nick)
reason = re.sub(r'\bme\b', msg.nick, reason)
nick = re.sub(r'\bmy\b', '%s\'s' % msg.nick, nick)
reason = re.sub(r'\bmy\b', '%s\'s' % msg.nick, reason)
lartee = nick
s = lart.replace('$who', lartee)
if reason:
s = '%s for %s' % (s, reason)
s = s.rstrip('.')
showid = self.registryValue('showIds', channel)
irc.reply(self._formatResponse(s, id, showid), action=True)
def praise(self, irc, msg, args):
"""[<channel>] [<id>] <text> [for <reason>]
Uses a praise on <text> (giving the reason, if offered). Will use
praise number <id> from the database when <id> is given.
<channel> is only necessary if the message isn't sent in the channel
itself.
"""
channel = privmsgs.getChannel(msg, args)
(id, nick) = privmsgs.getArgs(args, optional=1)
try:
id = int(id)
if id < 1:
irc.error('There is no such praise.')
return
except ValueError:
nick = ' '.join([id, nick]).strip()
id = 0
nick = nick.rstrip('.')
if not nick:
raise callbacks.ArgumentError
try:
(nick, reason) = imap(' '.join,
utils.itersplit('for'.__eq__, nick.split(), 1))
except ValueError:
reason = ''
db = self.getDb(channel)
cursor = db.cursor()
if id:
cursor.execute("""SELECT id, praise FROM praises WHERE id=%s""",id)
if cursor.rowcount == 0:
irc.error('There is no such praise.')
return
else:
cursor.execute("""SELECT id, praise FROM praises
WHERE praise NOTNULL
ORDER BY random()
LIMIT 1""")
if cursor.rowcount == 0:
irc.error('There are currently no available praises.')
else:
(id, praise) = cursor.fetchone()
nick = re.sub(r'\bme\b', msg.nick, nick)
reason = re.sub(r'\bme\b', msg.nick, reason)
nick = re.sub(r'\bmy\b', '%s\'s' % msg.nick, nick)
reason = re.sub(r'\bmy\b', '%s\'s' % msg.nick, reason)
praisee = nick
s = praise.replace('$who', praisee)
if reason:
s = '%s for %s' % (s, reason)
s = s.rstrip('.')
showid = self.registryValue('showIds', channel)
irc.reply(self._formatResponse(s, id, showid), action=True)
Class = FunDB
if __name__ == '__main__':
import sys
if len(sys.argv) < 3 or len(sys.argv) > 4:
print 'Usage: %s <channel> <larts|excuses|insults|zipcodes> file' \
' [<console>]' % sys.argv[0]
sys.exit(-1)
if len(sys.argv) == 4:
added_by = sys.argv.pop()
else:
added_by = '<console>'
(channel, category, filename) = sys.argv[1:]
plugin = Class()
db = plugin.getDb(channel)
cursor = db.cursor()
for line in open(filename, 'r'):
line = line.rstrip()
if not line:
continue
elif category == 'larts':
if '$who' in line:
cursor.execute("""INSERT INTO larts VALUES (NULL, %s, %s)""",
line, added_by)
else:
print 'Invalid lart: %s' % line
elif category == 'praises':
if '$who' in line:
cursor.execute("""INSERT INTO praises VALUES (NULL, %s, %s)""",
line, added_by)
else:
print 'Invalid praise: %s' % line
elif category == 'insults':
cursor.execute("""INSERT INTO insults VALUES (NULL, %s, %s)""",
line, added_by)
elif category == 'excuses':
cursor.execute("""INSERT INTO excuses VALUES (NULL, %s, %s )""",
line, added_by)
db.commit()
db.close()
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

Some files were not shown because too many files have changed in this diff Show More