mirror of
https://github.com/Mikaela/Limnoria.git
synced 2026-02-07 02:27:59 +01:00
Compare commits
No commits in common. "gh-pages" and "v0.77.1" have entirely different histories.
3
.cvsignore
Normal file
3
.cvsignore
Normal file
@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
@ -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
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @Mikaela
|
||||
5
.github/renovate.json5
vendored
5
.github/renovate.json5
vendored
@ -1,5 +0,0 @@
|
||||
/** @format */
|
||||
|
||||
{
|
||||
extends: ["local>Mikaela/shell-things:.renovate-shared"],
|
||||
}
|
||||
25
.github/workflows/html5validator.yml.disabled
vendored
25
.github/workflows/html5validator.yml.disabled
vendored
@ -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
32
.gitignore
vendored
@ -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
|
||||
@ -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]
|
||||
@ -1,5 +0,0 @@
|
||||
_includes/
|
||||
_layouts/
|
||||
_sass/
|
||||
css/
|
||||
feed.xml
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1 +0,0 @@
|
||||
3.4.2
|
||||
@ -1,4 +0,0 @@
|
||||
# @format
|
||||
|
||||
language: ruby
|
||||
script: "bundle exec jekyll build"
|
||||
5
ACKS
Normal file
5
ACKS
Normal 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
11
BUGS
Normal 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.
|
||||
617
ChangeLog
Normal file
617
ChangeLog
Normal file
@ -0,0 +1,617 @@
|
||||
2004-04-09 Jeremy Fincher <jemfinch@supybot.org>
|
||||
|
||||
* 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
|
||||
);
|
||||
|
||||
59
DEVS
Normal file
59
DEVS
Normal file
@ -0,0 +1,59 @@
|
||||
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.
|
||||
|
||||
8
Gemfile
8
Gemfile
@ -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"
|
||||
286
Gemfile.lock
286
Gemfile.lock
@ -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
28
LICENSE
Normal 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
42
README
Normal 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.
|
||||
11
README.md
11
README.md
@ -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
|
||||
72
RELNOTES
Normal file
72
RELNOTES
Normal file
@ -0,0 +1,72 @@
|
||||
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.
|
||||
@ -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?
|
||||
|
||||
---
|
||||
145
Supybot.markdown
145
Supybot.markdown
@ -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
11
TODO
Normal 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
43
__init__.py
Normal 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:
|
||||
38
_config.yml
38
_config.yml
@ -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
|
||||
@ -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>
|
||||
@ -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>
|
||||
210
_sass/_base.scss
210
_sass/_base.scss
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
145
assets/main.scss
145
assets/main.scss
@ -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
6
debian/changelog
vendored
Normal 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
1
debian/compat
vendored
Normal file
@ -0,0 +1 @@
|
||||
4
|
||||
20
debian/control
vendored
Normal file
20
debian/control
vendored
Normal 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)
|
||||
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
25
debian/copyright
vendored
Normal 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
1
debian/dirs
vendored
Normal file
@ -0,0 +1 @@
|
||||
usr/bin
|
||||
8
debian/docs
vendored
Normal file
8
debian/docs
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
BUGS
|
||||
DEVS
|
||||
LICENSE
|
||||
README
|
||||
RELNOTES
|
||||
TODO
|
||||
docs/CAPABILITIES
|
||||
docs/GETTING_STARTED
|
||||
87
debian/rules
vendored
Executable file
87
debian/rules
vendored
Executable 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
118
docs/CAPABILITIES
Normal 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
188
docs/CONFIGURATION
Normal 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
25
docs/DocBook/Makefile
Normal 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
177
docs/DocBook/README.DocBook
Normal 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.
|
||||
206
docs/DocBook/capabilities.sgml
Normal file
206
docs/DocBook/capabilities.sgml
Normal 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) “what a user can do” is set
|
||||
in one of two ways. On the <emphasis>really</emphasis> 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.
|
||||
</para>
|
||||
<para>
|
||||
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 <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
|
||||
“cool”, but not many people would say it was
|
||||
“awesome”. 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
|
||||
“anticapability” for that command. An anticapability is
|
||||
a capability that, instead of saying “what a user can
|
||||
do”, says what a user <emphasis>cannot</emphasis> do. It's
|
||||
formed rather simply by adding a dash (“-”) 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 – 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> –
|
||||
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 “This user can be trusted not to try and
|
||||
crash the bot.” 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 …) 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>
|
||||
|
||||
|
||||
311
docs/DocBook/configuration.sgml
Normal file
311
docs/DocBook/configuration.sgml
Normal 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
|
||||
– 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>
|
||||
<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
|
||||
</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
|
||||
“supybot.” 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>
|
||||
<jemfinch|lambda> @config list --groups supybot
|
||||
<supybot> 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 “supybot.” 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>
|
||||
<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.
|
||||
</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>
|
||||
<jemfinch|lambda> @config supybot.prefixChars
|
||||
<supybot> jemfinch|lambda: '@'
|
||||
</ircsession>
|
||||
<para>
|
||||
To set this value, just stick an extra argument after
|
||||
the name:
|
||||
</para>
|
||||
<ircsession>
|
||||
<jemfinch|lambda> @config supybot.prefixChars @$
|
||||
<supybot> jemfinch|lambda: The operation succeeded.
|
||||
</ircsession>
|
||||
<para>
|
||||
Now, check this out:
|
||||
</para>
|
||||
<ircsession>
|
||||
<jemfinch|lambda> $config supybot.prefixChars
|
||||
<supybot> 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>
|
||||
<jemfinch|lambda> $config supybot.prefixChars @
|
||||
<supybot> jemfinch|lambda: The operation succeeded.
|
||||
<jemfinch|lambda> $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>
|
||||
<jemfinch|lambda> @config default supybot.prefixChars
|
||||
<supybot> jemfinch|lambda: ''
|
||||
</ircsession>
|
||||
<para>
|
||||
Thus, to reset a configuration variable to its default
|
||||
value, you can simply say:
|
||||
</para>
|
||||
<ircsession>
|
||||
<jemfinch|lambda> @config supybot.prefixChars [config default
|
||||
supybot.prefixChars]
|
||||
<supybot> jemfinch|lambda: The operation succeeded.
|
||||
<jemfinch|lambda> @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>
|
||||
<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
|
||||
</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
714
docs/DocBook/example.sgml
Normal 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>
|
||||
<angryman> jemfinch: random takes no arguments (for more help
|
||||
use the morehelp command)
|
||||
<jemfinch> $morehelp random
|
||||
<angryman> jemfinch: Returns the next random number from the
|
||||
current random number generator.
|
||||
</ircsession>
|
||||
<para>
|
||||
'help <command>' 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 <command>'
|
||||
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):
|
||||
"""<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(msg, '<seed> 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):
|
||||
"""<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(msg, '<start> and <end> 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):
|
||||
"""<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(msg, '<number of items> must be an integer.')
|
||||
return
|
||||
if n > len(args):
|
||||
irc.error(msg, '<number of items> 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):
|
||||
"""[<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(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
284
docs/DocBook/faq.sgml
Normal 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>
|
||||
<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 :)
|
||||
</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
|
||||
<reply>, <action>, "see", and alternations
|
||||
(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. <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&atid=489447">
|
||||
http://sourceforge.net/tracker/?group_id=58965&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 … 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
|
||||
“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 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>
|
||||
|
||||
|
||||
297
docs/DocBook/getting_started.sgml
Normal file
297
docs/DocBook/getting_started.sgml
Normal 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 “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 <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>
|
||||
<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)
|
||||
</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>
|
||||
<jemfinch> more
|
||||
<supybot> 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>
|
||||
|
||||
|
||||
585
docs/DocBook/interfaces.sgml
Normal file
585
docs/DocBook/interfaces.sgml
Normal 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> –
|
||||
<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 – 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 – 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 –
|
||||
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>
|
||||
|
||||
|
||||
46
docs/DocBook/supybot-html.dsl
Normal file
46
docs/DocBook/supybot-html.dsl
Normal 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)))
|
||||
43
docs/DocBook/supybot-print.dsl
Normal file
43
docs/DocBook/supybot-print.dsl
Normal 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
41
docs/DocBook/supybot.css
Normal 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
23
docs/DocBook/supybot.dsl
Normal 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
139
docs/DocBook/supybot.dtd
Normal 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
439
docs/EXAMPLE
Normal 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 :)
|
||||
152
docs/FAQ
Normal file
152
docs/FAQ
Normal file
@ -0,0 +1,152 @@
|
||||
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.
|
||||
173
docs/GETTING_STARTED
Normal file
173
docs/GETTING_STARTED
Normal 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
259
docs/INTERFACES
Normal 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
64
docs/OVERVIEW
Normal 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.
|
||||
19
docs/RELEASE-CHECKLIST
Normal file
19
docs/RELEASE-CHECKLIST
Normal file
@ -0,0 +1,19 @@
|
||||
These are things not to be forgotten when making a Supybot release:
|
||||
|
||||
Update the version in conf.py.
|
||||
Update the version in setup.py.
|
||||
Update the version in scripts/supybot
|
||||
Make a release-MAJOR_MINOR_PATCHLEVEL tag. (cvs tag ...)
|
||||
Make a release-MAJOR_MINOR_PATCHLEVEL-branch branch tag. (cvs tag -b ...)
|
||||
Remove CVS directories from the tarball.
|
||||
Create .tar.gz, .tar.bz2, and .zip archives.
|
||||
Add the release to the supybot package in SF.net.
|
||||
Upload the archives to upload.sf.net, add to the release on SF.net.
|
||||
Announce the release in a News item on SF.net.
|
||||
Announce the release on Freshmeat.net.
|
||||
Announce the release on PyPI via "python setup.py register".
|
||||
Announce the release on the forums.
|
||||
Announce the release on comp.lang.python and comp.lang.python.announce.
|
||||
Add a topic to #supybot announcing the release.
|
||||
Send a GPG-signed tar.gz to vomjom for packing in Debian.
|
||||
Update the version in version.txt and upload to the website.
|
||||
86
docs/STYLE
Normal file
86
docs/STYLE
Normal 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
154
examples/Random.py
Normal 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:
|
||||
30
feed.xml
30
feed.xml
@ -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>
|
||||
@ -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
3978
others/SOAP.py
Normal file
File diff suppressed because it is too large
Load Diff
0
others/__init__.py
Normal file
0
others/__init__.py
Normal file
301
others/amazon.py
Normal file
301
others/amazon.py
Normal 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
294
others/asynchat.py
Normal 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
494
others/asyncore.py
Normal 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
167
others/babelfish.py
Normal 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
1183
others/convertcore.py
Executable file
File diff suppressed because it is too large
Load Diff
313
others/dictclient.py
Normal file
313
others/dictclient.py
Normal 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
433
others/google.py
Normal 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
1013
others/rfc822.py
Executable file
File diff suppressed because it is too large
Load Diff
722
others/rssparser.py
Normal file
722
others/rssparser.py
Normal 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('<', '<')
|
||||
data = data.replace('>', '>')
|
||||
data = data.replace('"', '"')
|
||||
data = data.replace(''', "'")
|
||||
data = data.replace('&', '&')
|
||||
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 " ", 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 "©", 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
218
others/shlex.py
Normal 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
785
others/unittest.py
Normal 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
1208
others/urllib2.py
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -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
303
plugins/Alias.py
Normal 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
596
plugins/Amazon.py
Normal 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:
|
||||
168
plugins/Babelfish.py
Normal file
168
plugins/Babelfish.py
Normal file
@ -0,0 +1,168 @@
|
||||
#!/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 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):
|
||||
Value = Languages
|
||||
def splitter(self, s):
|
||||
return s.split()
|
||||
joiner = ' '.join
|
||||
|
||||
conf.registerPlugin('Babelfish')
|
||||
conf.registerChannelValue(conf.supybot.plugins.Babelfish, 'disabledLanguages',
|
||||
SpaceSeparatedListOfLanguages([], """Determines which languages are
|
||||
unavailable 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()]
|
||||
disabled = map(str.lower, self.registryValue('disabledLanguages',chan))
|
||||
if fromLang in disabled:
|
||||
fromLang = None
|
||||
if toLang in disabled:
|
||||
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)
|
||||
try:
|
||||
(fromLang, toLang) = self._getLang(fromLang, toLang, msg.args[0])
|
||||
if not fromLang or not toLang:
|
||||
langs = self.registryValue('disabledLanguages', msg.args[0])
|
||||
irc.error('I do not 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, utils.commaAndify(babelfish.available_languages)))
|
||||
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)
|
||||
try:
|
||||
(fromLang, toLang) = self._getLang(fromLang, toLang, msg.args[0])
|
||||
if fromLang != 'english' and toLang != 'english':
|
||||
irc.error('One language must be English.')
|
||||
return
|
||||
if not fromLang or not toLang:
|
||||
langs = self.registryValue('disabledLanguages', msg.args[0])
|
||||
irc.error('I do not 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, utils.commaAndify(babelfish.available_languages)))
|
||||
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)
|
||||
language = random.choice(babelfish.available_languages)
|
||||
disabled = self.registryValue('disabledLanguages', msg.args[0])
|
||||
while not allowEnglish and language == 'English' and\
|
||||
language not in disabled:
|
||||
language = random.choice(babelfish.available_languages)
|
||||
irc.reply(language)
|
||||
|
||||
Class = Babelfish
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
||||
161
plugins/BadWords.py
Normal file
161
plugins/BadWords.py
Normal file
@ -0,0 +1,161 @@
|
||||
#!/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 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 = 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
409
plugins/Bugzilla.py
Normal 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:
|
||||
242
plugins/ChannelLogger.py
Normal file
242
plugins/ChannelLogger.py
Normal file
@ -0,0 +1,242 @@
|
||||
#!/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, '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."""))
|
||||
|
||||
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 doLog(self, channel, s):
|
||||
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(channel,
|
||||
'* %s %s\n' % (nick, ircmsgs.unAction(msg)))
|
||||
else:
|
||||
self.doLog(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(channel, '-%s- %s\n' % (msg.nick, text))
|
||||
|
||||
def doJoin(self, irc, msg):
|
||||
for channel in msg.args[0].split(','):
|
||||
self.doLog(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(channel,
|
||||
'*** %s was kicked by %s (%s)\n' %
|
||||
(target, msg.nick, kickmsg))
|
||||
else:
|
||||
self.doLog(channel,
|
||||
'*** %s was kicked by %s\n' % (target, msg.nick))
|
||||
|
||||
def doPart(self, irc, msg):
|
||||
for channel in msg.args[0].split(','):
|
||||
self.doLog(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(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(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(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
350
plugins/ChannelStats.py
Normal 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
89
plugins/Ctcp.py
Normal 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
105
plugins/DCC.py
Normal 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
363
plugins/Debian.py
Normal 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
147
plugins/Dict.py
Normal 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
216
plugins/Dunno.py
Normal 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
165
plugins/Ebay.py
Normal 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
280
plugins/Enforcer.py
Normal 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
444
plugins/Factoids.py
Normal 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:
|
||||
453
plugins/Filter.py
Normal file
453
plugins/Filter.py
Normal file
@ -0,0 +1,453 @@
|
||||
#!/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('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(''.join(L))
|
||||
|
||||
def colorstrip(self, irc, msg, args):
|
||||
"""<text>
|
||||
|
||||
Returns <text> stripped of all color codes.
|
||||
"""
|
||||
text = privmsgs.getArgs(args)
|
||||
irc.reply(ircutils.unColor(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
63
plugins/FixRelayBot.py
Normal 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
82
plugins/Friendly.py
Executable 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
362
plugins/Fun.py
Normal 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:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user