Compare commits

...

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

300 changed files with 1546 additions and 67532 deletions

View File

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

8
.editorconfig Normal file
View File

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

1
.gitattributes vendored Normal file
View File

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

1
.github/CODEOWNERS vendored Normal file
View File

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

5
.github/renovate.json5 vendored Normal file
View File

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

View File

@ -0,0 +1,25 @@
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 Normal file
View File

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

83
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,83 @@
# @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]

5
.prettierignore Normal file
View File

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

1
.prettierrc Normal file
View File

@ -0,0 +1 @@
{}

1
.ruby-version Normal file
View File

@ -0,0 +1 @@
3.4.2

4
.travis.yml Normal file
View File

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

5
ACKS
View File

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

9
BUGS
View File

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

1
CNAME Normal file
View File

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

1079
ChangeLog

File diff suppressed because it is too large Load Diff

66
DEVS
View File

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

8
Gemfile Normal file
View File

@ -0,0 +1,8 @@
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 Normal file
View File

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

108
INSTALL
View File

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

28
LICENSE
View File

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

38
README
View File

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

11
README.md Normal file
View File

@ -0,0 +1,11 @@
<!-- @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

133
RELNOTES
View File

@ -1,133 +0,0 @@
Version 0.79.9999
Some more bugs fixed, added a few features and a couple configuration
variabless. This should hopefully be the last release before 0.80.0,
which will finally bring us to pure Beta status.
Version 0.79.999
Some bugs fixed, but the ones that were fixed were pretty big. This
is, of course, completely compatible with the last release.
Version 0.79.99
Many bugs fixed, thanks to the users who reported them. We're
getting asymptotically closer to 0.80.0 -- maybe this'll be the last
one, maybe we'll have to release an 0.79.999 -- either way, we're
getting close :) Check out the ChangeLog for the fixes and a few new
features.
Version 0.79.9
We've changed so much stuff in this release that we've given up on
users upgrading their configuration files for the new release. So
do a clean install (python2.3 setup.py install --clean), run the
wizard again, and kick some butt.
(It's rumored that you can save most of your old configuration by
appending your new configuration at the end of your old configuration
and running supybot with that new configuration file. This, of
course, comes with no warranty or guarantee of utility -- try it if
you want, but backup your original configuration file!)
Version 0.77.2
This is a drop-in replacement for 0.77.1, with two exceptions. The
configuration variable formerly known as
"supybot.plugins.Services.password" is now known as
"supybot.plugins.Services.NickServ.password", due to the fact that
there might be different passwords for NickServ and ChanServ (and
ChanServ passwords are per-channel, whereas NickServ passwords are
global). If you're using the Services plugin, you'll need to make
this change in order to continue identifying with services. The
configuration variable formerly known as
"supybot.plugins.Babelfish.disabledLanguages" is now known as
"supybot.plugins.Babelfish.languages". The configuration variable now
accepts the languages that *will* be translated as opposed to ones
that are *not* translated.
Tests and the developer sandbox are not longer delivered with our
release tarballs. If you're a developer and you want these, you
should either check out CVS or download one of our weekly CVS
snapshots, available at http://supybot.sourceforge.net/snapshots/ .
Version 0.77.1
This is a drop-in replacement for 0.77.0 -- no incompatibilities, to
out knowledge. Simply install over your old installation and restart
your bot :)
Version 0.77.0
Setup.py will automatically remove your old installations for you, no
need to worry about that yourself.
Configuration has been *entirely* redone. Read the new
GETTING_STARTED document to see how to work with configuration
variables now. Your old botscripts from earlier versions *will not*
work with the new configuration method. We'd appreciate it if you'd
rerun the wizard in order for us to find any bugs that remain in it
before we officially declare ourselves Beta. Note also that because
of the new configuration method, the interface for plugins' configure
function has changed: there are no longer any onStart or afterConnect
arguments, so all configuration should be performed via the registry.
Channel capabilities have been changed; rather than being
#channel.capability, they're now #channel,capability. It's a bit
uglier, we know, but dots can be valid in channel names, and we
needed the dot for handling plugin.command capabilities.
tools/ircdbConvert.py should update this for you.
The on-disk format of the user/channel databases has changed to be far
more readable. A conversion utility is included, as mentioned before:
tools/ircdbConvert.py. Run this with no arguments to see the
directions for using it.
Uh, we were just kidding about the upgrade script in 0.76.0 :) It'll
be a little while longer. We do have several little upgrade scripts,
though.
Version 0.76.1
Almost entirely bugfixes, just some minor (and some less minor) bugs
that need to get in before we really start hacking on the next
version. Should be *entirely* compatible with 0.76.0.
Version 0.76.0
Major bugfix release. A great number of bugs fixed. This is the last
release without an upgrade script.
The only hiccup in the upgrade from 0.75.0 should be that you'll need
to update your botscript to reflect the removal of the debug module.
We'd rather you use supybot-wizard to generate a new botscript, of
course, but if you insist on modifying your existing botscript, take a
look at
<http://cvs.sourceforge.net/viewcvs.py/supybot/supybot/src/template.py?r1=1.20&r2=1.21>
to see what you need to do.
Version 0.75.0
Don't forget to reinstall (i.e., run "python setup.py install" as
root). Sometimes it even does good to remove the old installation;
$PYTHON/site-packages/supybot can be removed with no problems
whatsoever.
You will need to re-run supybot-wizard and generate a new botscript.
The Infobot plugin has been removed from this release; it's not ready
for prime time. If you're interested in getting it running (i.e., you
want full Infobot compatibility and aren't satisfied with either
MoobotFactoids or Factoids) then swing over to #supybot and we can
discuss the tests. We simply don't know enough about Infobot to make
sure our Infobot plugin is an exact replica, and need someone's help
with making the changes necessary for that.

71
Relaybot.markdown Normal file
View File

@ -0,0 +1,71 @@
---
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 Normal file
View File

@ -0,0 +1,145 @@
---
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)

27
TODO
View File

@ -1,27 +0,0 @@
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.
Hard problems that won't get done until someone really wants to have
some fun:
* Redundant relaying -- having more than one bot handle relaying,
where a secondary one will automatically take over if the first
becomes incapacitated.
* Have the bot detect when other bots are in the channel responding
to the same prefix character, and do something based on that.
* Unicode support. Basically, we'd have to have a way to set the
encoding for the server, and use unicode throughout internally.
The real issue is how much it would affect the current code to
switch it over to unicode, and what kind of burden it would put on
plugin authors to deal with that issue.

38
_config.yml Normal file
View File

@ -0,0 +1,38 @@
# @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

32
_includes/footer.html Normal file
View File

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

15
_includes/head.html Normal file
View File

@ -0,0 +1,15 @@
<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 Normal file
View File

@ -0,0 +1,210 @@
: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;
}
}
}

236
_sass/_layout.scss Normal file
View File

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

View File

@ -0,0 +1,67 @@
/**
* 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 Normal file
View File

@ -0,0 +1,145 @@
---
# 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;
}
}

33
debian/changelog vendored
View File

@ -1,33 +0,0 @@
supybot (0.79.9-1) unstable; urgency=high
* New upstream (Closes: #268311)
* New maintainer: James Vega <vega.james@gmail.com>
* Urgency high to get the RC fix into Sarge (hopefully)
* debian/control:
+ Downgrade Recommends: python-sqlite to Suggests: python-sqlite
+ Update Build-Depends: cdbs, debhelper (>= 4.1.67)
+ Bump Standard-Version to 3.6.1.0, no changes necessary
* Convert to cdbs
-- James Vega <vega.james@gmail.com> Thu, 26 Aug 2004 17:28:03 -0400
supybot (0.77.2-1) unstable; urgency=low
* New upstream
-- Jonathan Hseu <vomjom@debian.org> Sun, 18 Apr 2004 02:58:25 -0500
supybot (0.77.1-1) unstable; urgency=low
* New upstream
* Add python-dev as a build-dep (closes: Bug#242953)
-- Jonathan Hseu <vomjom@debian.org> Sat, 10 Apr 2004 13:06:38 -0500
supybot (0.77.0-1) unstable; urgency=low
* Initial Release.
* Paste LICENSE file into copyright
-- Jonathan Hseu <vomjom@debian.org> Tue, 10 Mar 2004 00:59:46 -0600

1
debian/compat vendored
View File

@ -1 +0,0 @@
4

21
debian/control vendored
View File

@ -1,21 +0,0 @@
Source: supybot
Section: net
Priority: optional
Maintainer: James Vega <vega.james@gmail.com>
Build-Depends: debhelper (>= 4.1.67), cdbs, python (>= 2.3), python-dev (>= 2.3), docbook-to-man (>= 2.0.0)
Standards-Version: 3.6.1.0
Package: supybot
Architecture: all
Depends: ${python:Depends}
Suggests: python-twisted, python-sqlite
Description: 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.
.
Homepage: http://supybot.sourceforge.net/

53
debian/copyright vendored
View File

@ -1,53 +0,0 @@
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.sourceforge.net/
Upstream Authors:
/usr/share/doc/supybot/DEVS includes the names and descriptions of the
developers.
Copyright:
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
The copyright for all other files are as follows (BSD-style):
Copyright (c) 2002, 2003, 2004 Jeremiah Fincher
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions, and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author of this software nor the name of
contributors to this software may be used to endorse or promote products
derived from this software without specific prior written 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.

15
debian/docs vendored
View File

@ -1,15 +0,0 @@
ACKS
BUGS
DEVS
README
RELNOTES
TODO
docs/CAPABILITIES
docs/CONFIGURATION
docs/FAQ
docs/GETTING_STARTED
docs/HACKING
docs/INTERFACES
docs/OVERVIEW
docs/PLUGIN-EXAMPLE
docs/STYLE

View File

@ -1,183 +0,0 @@
<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
<!-- Process this file with docbook-to-man to generate an nroff manual
page: `docbook-to-man manpage.sgml > manpage.1'. You may view
the manual page with: `docbook-to-man manpage.sgml | nroff -man |
less'. A typical entry in a Makefile or Makefile.am is:
manpage.1: manpage.sgml
docbook-to-man $< > $@
The docbook-to-man binary is found in the docbook-to-man package.
Please remember that if you create the nroff version in one of the
debian/rules file targets (such as build), you will need to include
docbook-to-man in your Build-Depends control field.
-->
<!-- Fill in your name for FIRSTNAME and SURNAME. -->
<!ENTITY dhfirstname "<firstname>Jonathan</firstname>">
<!ENTITY dhsurname "<surname>Hseu</surname>">
<!-- Please adjust the date whenever revising the manpage. -->
<!ENTITY dhdate "<date>August 27, 2004</date>">
<!-- SECTION should be 1-8, maybe w/ subsection other parameters are
allowed: see man(7), man(1). -->
<!ENTITY dhsection "<manvolnum>1</manvolnum>">
<!ENTITY dhemail "<email>vomjom@debian.org</email>">
<!ENTITY dhusername "Jonathan Hseu">
<!ENTITY dhucpackage "<refentrytitle>SUPYBOT-ADDUSER</refentrytitle>">
<!ENTITY dhpackage "supybot-adduser">
<!ENTITY debian "<productname>Debian</productname>">
<!ENTITY gnu "<acronym>GNU</acronym>">
<!ENTITY gpl "&gnu; <acronym>GPL</acronym>">
]>
<refentry>
<refentryinfo>
<address>
&dhemail;
</address>
<author>
&dhfirstname;
&dhsurname;
</author>
<copyright>
<year>2003</year>
<holder>&dhusername;</holder>
</copyright>
&dhdate;
</refentryinfo>
<refmeta>
&dhucpackage;
&dhsection;
</refmeta>
<refnamediv>
<refname>&dhpackage;</refname>
<refpurpose>Adds a user to a Supybot configuration file</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>&dhpackage;</command>
<arg><option>options</option></arg>
<arg choice="req"><replaceable>users.conf</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>DESCRIPTION</title>
<para><command>&dhpackage;</command> adds a user to a Supybot
configuration file.</para>
</refsect1>
<refsect1>
<title>OPTIONS</title>
<para>These programs follow the usual &gnu; command line syntax,
with long options starting with two dashes (`-'). A summary of
options is included below.</para>
<variablelist>
<varlistentry>
<term><option>--version</option>
</term>
<listitem>
<para>Show version of program.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-h</option>
<option>--help</option>
</term>
<listitem>
<para>Show summary of options.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-uNAME</option>
<option>--username=NAME</option>
</term>
<listitem>
<para>Username for the user.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-pPASSWORD</option>
<option>--password=PASSWORD</option>
</term>
<listitem>
<para>Password for the user.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-x</option>
<option>--hashed</option>
</term>
<listitem>
<para>Hash encrypt the password.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-n</option>
<option>--plain</option>
</term>
<listitem>
<para>Store the password in plain text.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-cCAPABILITY</option>
<option>--capability=CAPABILITY</option>
</term>
<listitem>
<para>Capability the user should have; this option may be
given multiple times</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>SEE ALSO</title>
<para>python (1), supybot-wizard (1), supybot (1),
supybot-newplugin (1).</para>
</refsect1>
<refsect1>
<title>AUTHOR</title>
<para>This manual page was originally written by &dhusername; &dhemail; for
the &debian; system (but may be used by others). Permission is
granted to copy, distribute and/or modify this document under
the terms of the Supybot license, a BSD-style license.
</para>
<para>
On Debian systems with Supybot installed, the complete text
of the supybot license can be found in /usr/share/doc/supybot/LICENSE
</para>
</refsect1>
</refentry>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:2
sgml-indent-data:t
sgml-parent-document:nil
sgml-default-dtd-file:nil
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
-->

View File

@ -1,165 +0,0 @@
<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
<!-- Process this file with docbook-to-man to generate an nroff manual
page: `docbook-to-man manpage.sgml > manpage.1'. You may view
the manual page with: `docbook-to-man manpage.sgml | nroff -man |
less'. A typical entry in a Makefile or Makefile.am is:
manpage.1: manpage.sgml
docbook-to-man $< > $@
The docbook-to-man binary is found in the docbook-to-man package.
Please remember that if you create the nroff version in one of the
debian/rules file targets (such as build), you will need to include
docbook-to-man in your Build-Depends control field.
-->
<!-- Fill in your name for FIRSTNAME and SURNAME. -->
<!ENTITY dhfirstname "<firstname>Jonathan</firstname>">
<!ENTITY dhsurname "<surname>Hseu</surname>">
<!-- Please adjust the date whenever revising the manpage. -->
<!ENTITY dhdate "<date>August 27, 2004</date>">
<!-- SECTION should be 1-8, maybe w/ subsection other parameters are
allowed: see man(7), man(1). -->
<!ENTITY dhsection "<manvolnum>1</manvolnum>">
<!ENTITY dhemail "<email>vomjom@debian.org</email>">
<!ENTITY dhusername "Jonathan Hseu">
<!ENTITY dhucpackage "<refentrytitle>SUPYBOT-NEWPLUGIN</refentrytitle>">
<!ENTITY dhpackage "supybot-newplugin">
<!ENTITY debian "<productname>Debian</productname>">
<!ENTITY gnu "<acronym>GNU</acronym>">
<!ENTITY gpl "&gnu; <acronym>GPL</acronym>">
]>
<refentry>
<refentryinfo>
<address>
&dhemail;
</address>
<author>
&dhfirstname;
&dhsurname;
</author>
<copyright>
<year>2003</year>
<holder>&dhusername;</holder>
</copyright>
&dhdate;
</refentryinfo>
<refmeta>
&dhucpackage;
&dhsection;
</refmeta>
<refnamediv>
<refname>&dhpackage;</refname>
<refpurpose>A wizard for creating Supybot plugins</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>&dhpackage;</command>
<arg><option>options</option></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>DESCRIPTION</title>
<para><command>&dhpackage;</command> is a wizard that creates a template
python source file for a new plugin.</para>
</refsect1>
<refsect1>
<title>OPTIONS</title>
<para>These programs follow the usual &gnu; command line syntax,
with long options starting with two dashes (`-'). A summary of
options is included below.</para>
<variablelist>
<varlistentry>
<term><option>--version</option>
</term>
<listitem>
<para>Show version of program.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-h</option>
<option>--help</option>
</term>
<listitem>
<para>Show summary of options.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-r</option>
<option>--regexp</option>
</term>
<listitem>
<para>Uses a regexp-based callback.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-nNAME</option>
<option>--name=NAME</option>
</term>
<listitem>
<para>Sets the name for the plugin.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-t</option>
<option>--thread</option>
</term>
<listitem>
<para>Makes the plugin threaded.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>SEE ALSO</title>
<para>python (1), supybot-wizard (1), supybot-adduser (1),
supybot (1).</para>
</refsect1>
<refsect1>
<title>AUTHOR</title>
<para>This manual page was originally written by &dhusername; &dhemail; for
the &debian; system (but may be used by others). Permission is
granted to copy, distribute and/or modify this document under
the terms of the Supybot license, a BSD-style license.
</para>
<para>
On Debian systems with Supybot installed, the complete text
of the supybot license can be found in /usr/share/doc/supybot/LICENSE
</para>
</refsect1>
</refentry>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:2
sgml-indent-data:t
sgml-parent-document:nil
sgml-default-dtd-file:nil
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
-->

View File

@ -1,151 +0,0 @@
<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
<!-- Process this file with docbook-to-man to generate an nroff manual
page: `docbook-to-man manpage.sgml > manpage.1'. You may view
the manual page with: `docbook-to-man manpage.sgml | nroff -man |
less'. A typical entry in a Makefile or Makefile.am is:
manpage.1: manpage.sgml
docbook-to-man $< > $@
The docbook-to-man binary is found in the docbook-to-man package.
Please remember that if you create the nroff version in one of the
debian/rules file targets (such as build), you will need to include
docbook-to-man in your Build-Depends control field.
-->
<!-- Fill in your name for FIRSTNAME and SURNAME. -->
<!ENTITY dhfirstname "<firstname>Jonathan</firstname>">
<!ENTITY dhsurname "<surname>Hseu</surname>">
<!-- Please adjust the date whenever revising the manpage. -->
<!ENTITY dhdate "<date>August 27, 2004</date>">
<!-- SECTION should be 1-8, maybe w/ subsection other parameters are
allowed: see man(7), man(1). -->
<!ENTITY dhsection "<manvolnum>1</manvolnum>">
<!ENTITY dhemail "<email>vomjom@debian.org</email>">
<!ENTITY dhusername "Jonathan Hseu">
<!ENTITY dhucpackage "<refentrytitle>SUPYBOT-WIZARD</refentrytitle>">
<!ENTITY dhpackage "supybot-wizard">
<!ENTITY debian "<productname>Debian</productname>">
<!ENTITY gnu "<acronym>GNU</acronym>">
<!ENTITY gpl "&gnu; <acronym>GPL</acronym>">
]>
<refentry>
<refentryinfo>
<address>
&dhemail;
</address>
<author>
&dhfirstname;
&dhsurname;
</author>
<copyright>
<year>2003</year>
<holder>&dhusername;</holder>
</copyright>
&dhdate;
</refentryinfo>
<refmeta>
&dhucpackage;
&dhsection;
</refmeta>
<refnamediv>
<refname>&dhpackage;</refname>
<refpurpose>A wizard for creating configuration files</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>&dhpackage;</command>
<arg><option>options</option></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>DESCRIPTION</title>
<para><command>&dhpackage;</command> is an in-depth wizard that provides
a nice user interface for creating configuration files for
<command>supybot</command>.</para>
</refsect1>
<refsect1>
<title>OPTIONS</title>
<para>These programs follow the usual &gnu; command line syntax,
with long options starting with two dashes (`-'). A summary of
options is included below.</para>
<variablelist>
<varlistentry>
<term><option>--version</option>
</term>
<listitem>
<para>Show version of program.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-h</option>
<option>--help</option>
</term>
<listitem>
<para>Show summary of options.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--allow-root</option></term>
<listitem>
<para>Determines whether the wizard will be allowed to run
as root. You don't want this. Don't do it. Even if you
think you want it, you don't.
</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>SEE ALSO</title>
<para>python (1), supybot (1), supybot-adduser (1),
supybot-newplugin (1).</para>
</refsect1>
<refsect1>
<title>AUTHOR</title>
<para>This manual page was originally written by &dhusername; &dhemail; for
the &debian; system (but may be used by others). Permission is
granted to copy, distribute and/or modify this document under
the terms of the Supybot license, a BSD-style license.
</para>
<para>
On Debian systems with Supybot installed, the complete text
of the Supybot license can be found in /usr/share/doc/supybot/LICENSE
</para>
</refsect1>
</refentry>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:2
sgml-indent-data:t
sgml-parent-document:nil
sgml-default-dtd-file:nil
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
-->

View File

@ -1,230 +0,0 @@
<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
<!-- Process this file with docbook-to-man to generate an nroff manual
page: `docbook-to-man manpage.sgml > manpage.1'. You may view
the manual page with: `docbook-to-man manpage.sgml | nroff -man |
less'. A typical entry in a Makefile or Makefile.am is:
manpage.1: manpage.sgml
docbook-to-man $< > $@
The docbook-to-man binary is found in the docbook-to-man package.
Please remember that if you create the nroff version in one of the
debian/rules file targets (such as build), you will need to include
docbook-to-man in your Build-Depends control field.
-->
<!-- Fill in your name for FIRSTNAME and SURNAME. -->
<!ENTITY dhfirstname "<firstname>Jonathan</firstname>">
<!ENTITY dhsurname "<surname>Hseu</surname>">
<!-- Please adjust the date whenever revising the manpage. -->
<!ENTITY dhdate "<date>August 27, 2004</date>">
<!-- SECTION should be 1-8, maybe w/ subsection other parameters are
allowed: see man(7), man(1). -->
<!ENTITY dhsection "<manvolnum>1</manvolnum>">
<!ENTITY dhemail "<email>vomjom@debian.org</email>">
<!ENTITY dhusername "Jonathan Hseu">
<!ENTITY dhucpackage "<refentrytitle>SUPYBOT</refentrytitle>">
<!ENTITY dhpackage "supybot">
<!ENTITY debian "<productname>Debian</productname>">
<!ENTITY gnu "<acronym>GNU</acronym>">
<!ENTITY gpl "&gnu; <acronym>GPL</acronym>">
]>
<refentry>
<refentryinfo>
<address>
&dhemail;
</address>
<author>
&dhfirstname;
&dhsurname;
</author>
<copyright>
<year>2003</year>
<holder>&dhusername;</holder>
</copyright>
&dhdate;
</refentryinfo>
<refmeta>
&dhucpackage;
&dhsection;
</refmeta>
<refnamediv>
<refname>&dhpackage;</refname>
<refpurpose>A robust and user friendly Python IRC bot</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>&dhpackage;</command>
<arg><option>options</option></arg>
<arg choice="req"><replaceable>configFile</replaceable></arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>DESCRIPTION</title>
<para><command>&dhpackage;</command> is a robust, user-friendly,
and programmer-friendly Python IRC bot. It aims to be an adequate
replacement for most existing IRC bots. It includes a very
flexible and poerful ACL system for controlling access to
commands, as well as more than 50 builtin plugins providing
around 400 actual commands.
</para>
</refsect1>
<refsect1>
<title>OPTIONS</title>
<para>These programs follow the usual &gnu; command line syntax,
with long options starting with two dashes (`-'). A summary of
options is included below.
</para>
<variablelist>
<varlistentry>
<term><option>--version</option></term>
<listitem>
<para>Show version of program.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-h</option>
<option>--help</option>
</term>
<listitem>
<para>Show summary of options.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-P</option>
<option>--profile</option>
</term>
<listitem>
<para>Enable profiling.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-O</option></term>
<listitem>
<para>Optimizes asserts out of the code; -O0 optimizes asserts
and uses psyco.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-nNICK</option>
<option>--nick=NICK</option>
</term>
<listitem>
<para>Nick the bot should use.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-uUSER</option>
<option>--user=USER</option>
</term>
<listitem>
<para>Full username the bot should use.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-iIDENT</option>
<option>--ident=IDENT</option>
</term>
<listitem>
<para>Ident the bot should use. </para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-d</option>
<option>--daemon</option>
</term>
<listitem>
<para>Determines whether the bot will daemonize. This is a
no-op on non-POSIX systems.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--allow-eval</option></term>
<listitem>
<para>Determines whether the bot will allow the evaluation
of arbitrary Python code.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--allow-default-owner</option></term>
<listitem>
<para>Determines whether the bot will allow its defaultCapabilities
not to include "-owner", thus giving all users the owner capability
by default. This is dumb, hence we require a command-line option.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--allow-root</option></term>
<listitem>
<para>Determines whether the bot will be allowed to run as root. You
don't want this. Don't do it. Even if you think you want it, you don't.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--debug</option></term>
<listitem>
<para>Determines whether some extra debugging stuff will be logged by
this script.
</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>SEE ALSO</title>
<para>python (1), supybot-wizard (1), supybot-adduser (1),
supybot-newplugin (1).</para>
</refsect1>
<refsect1>
<title>AUTHOR</title>
<para>This manual page was originally written by &dhusername; &dhemail; for
the &debian; system (but may be used by others). Permission is
granted to copy, distribute and/or modify this document under
the terms of the Supybot license, a BSD-style license.
</para>
<para>
On Debian systems with Supybot installed, the complete text
of the Supybot license can be found in /usr/share/doc/supybot/LICENSE
</para>
</refsect1>
</refentry>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:2
sgml-indent-data:t
sgml-parent-document:nil
sgml-default-dtd-file:nil
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
-->

15
debian/rules vendored
View File

@ -1,15 +0,0 @@
#!/usr/bin/make -f
include /usr/share/cdbs/1/rules/debhelper.mk
include /usr/share/cdbs/1/class/python-distutils.mk
DEB_INSTALL_MANPAGES_supybot := debian/supybot-adduser.1 debian/supybot-newplugin.1\
debian/supybot.1 debian/supybot-wizard.1
build/supybot::
docbook-to-man debian/manpage_supybot-adduser.sgml > debian/supybot-adduser.1
docbook-to-man debian/manpage_supybot-newplugin.sgml > debian/supybot-newplugin.1
docbook-to-man debian/manpage_supybot.sgml > debian/supybot.1
docbook-to-man debian/manpage_supybot-wizard.sgml > debian/supybot-wizard.1
clean::
-rm -f $(DEB_INSTALL_MANPAGES_supybot)

6
debian/watch vendored
View File

@ -1,6 +0,0 @@
# Example watch control file for uscan
# Rename this file to "watch" and then you can run the "uscan" command
# to check for upstream updates and more.
# Site Directory Pattern Version Script
version=2
http://osdn.dl.sourceforge.net/supybot/Supybot-(\d.\d\d.\d(.\d)?)\.tar\.gz

View File

@ -1,118 +0,0 @@
Ok, 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 fine-grained 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, completely 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 programming 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 someone gave the
bot the command 're [format join "" s/./ [dict go] /] [dict go]'
Other plugins may require different capabilities; the Factoids plugin
requires #channel,factoids, the Topic plugin requires #channel,topic,
etc.

View File

@ -1,172 +0,0 @@
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: config, 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> @capabilities, @commands, @databases, @debug, @directories, @drivers,
@log, @networks, @nick, @plugins, @protocols, @replies, @reply,
alwaysJoinOnInvite, channels, defaultIgnore, defaultSocketTimeout,
externalIP, flush, followIdentificationThroughNickChanges,
humanTimestampFormat, ident, pidFile, snarfThrottle, upkeepInterval,
and user
These are all the configuration groups and values 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. The first entries in the output are
the groups (distinguished by the @ symbol in front of them), and the rest are
the configuration values.
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.snarfThrottle" configuration variable is all about, we'd
do this:
<jemfinch|lambda> @config help supybot.snarfThrottle
<supybot> jemfinch|lambda: A floating point number of seconds to throttle snarfed
URLs, in order to prevent loops between two bots snarfing the same URLs and
having the snarfed URL in the output of the snarf message. (Current value:
10.0)
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.reply.whenAddressedBy.chars
<supybot> jemfinch|lambda: '@'
To set this value, just stick an extra argument after the name:
<jemfinch|lambda> @config supybot.reply.whenAddressedBy.chars @$
<supybot> jemfinch|lambda: The operation succeeded.
Now, check this out:
<jemfinch|lambda> $config supybot.reply.whenAddressedBy.chars
<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.reply.whenAddressedBy.chars @
<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.reply.whenAddressedBy.chars
<supybot> jemfinch|lambda: ''
Thus, to reset a configuration variable to its default value, you can
simply say:
<jemfinch|lambda> @config supybot.reply.whenAddressedBy.chars [config
default supybot.reply.whenAddressedBy.chars]
<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.autoHalfop,
supybot.plugins.Enforcer.takeRevenge.onOps,
supybot.plugins.Enforcer.cycleToGetOps,
supybot.plugins.Topic, supybot.plugins.Topic.public,
supybot.plugins.Topic.separator,
supybot.plugins.Topic.format,
supybot.plugins.Topic.recognizeTopiclen,
supybot.plugins.Topic.default,
supybot.plugins.Topic.undo.maz, 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.
Many configuration variables can be specific to individual channels.
The Config plugin provides an easy way to configure something for a
specific channel; for instance, in order to set the prefix chars for a
specific channel, do this in that channel:
config channel supybot.reply.whenAddressedBy.chars !
That'll set the prefix chars in the channel that message is sent in to
!. Voila, channel-specific values!
Anyway, that's about it for configuration. Have fun, and enjoy your
configurable bot!

View File

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

View File

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

View File

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

View File

@ -1,316 +0,0 @@
<!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>
<revision>
<revnumber>0.3</revnumber>
<date>4 Sep 2004</date>
<revremark>Update Docbook translation</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>config</botcommand>,
<botcommand>list</botcommand>, and
<botcommand>help</botcommand>. If you don't know how to get help on
those commands, go ahead and read our
<filename>GETTING_STARTED</filename> document before this one.
</para>
</sect1>
<sect1>
<title>Supybot's registry</title>
<para>
Now, if you're used to the Windows registry, don't worry,
Supybot's registry is completely different. For one, it's
completely plain text. There's no binary database sensitive to
corruption, it's not necessary to use another program to edit it
&ndash; all you need is a simple text editor. But there is at
least one good idea in Windows' registry: hierarchical
configuration. Supybot's configuration variables are organized in
a hierarchy: variables having to do with the way Supybot makes
replies all start with
<registrygroup>supybot.reply</registrygroup>; variables having to
do with the way a plugin works all start with
<registrygroup>supybot.plugins.Plugin</registrygroup> (where
<plugin>Plugin</plugin> is the name of the plugin in question).
This hierarchy is nice because it means the user isn't inundated
with hundreds of unrelated and unsorted configuration variables.
</para>
<para>
Some of the more important configuration values are located
directly under the base group,
<registrygroup>supybot</registrygroup>. Things like the bot's
nick, its ident, etc. Along with these config values are a few
subgroups that contain other values. Some of the more prominent
subgroups are: <registrygroup>plugins</registrygroup> (where all
the plugin-specific configuration is held),
<registrygroup>reply</registrygroup> (where variables affecting
the way a Supybot makes its replies resides),
<registrygroup>replies</registrygroup> (where all the specific
standard replies are kept), and
<registrygroup>directories</registrygroup> (where all the
directories a Supybot uses are defined). There are other
subgroups as well, but these are the ones we'll use in our
example.
</para>
<sect2>
<title>Config plugin commands</title>
<sect3>
<title>Listing registry contents</title>
<para>
Using the <plugin>Config</plugin> plugin, you can list
the values in a subgroup and get or set any of the values
anywhere in the configuration hierarchy. For example,
let's say you wanted to see what configuration values were
under the <registrygroup>supybot</registrygroup> (the base
group) hierarchy. You would simply issue this command:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config list supybot
&lt;supybot&gt; @capabilities, @commands, @databases, @debug, @directories, @drivers,
@log, @networks, @nick, @plugins, @protocols, @replies, @reply,
alwaysJoinOnInvite, channels, defaultIgnore, defaultSocketTimeout,
externalIP, flush, followIdentificationThroughNickChanges,
humanTimestampFormat, ident, pidFile, snarfThrottle, upkeepInterval,
and user
</ircsession>
<para>
These are all the configuration groups and values which
are under the base <registrygroup>supybot</registrygroup>
group. Actually, their full names would each have a
&ldquo;supybot.&rdquo; appended on to the front of them,
but it is omitted in the listing in order to shorten the
output. The first entries in the output are the groups
(distinguished by the @ symbol in front of them), and the
rest are the configuration values.
</para>
</sect3>
<sect2>
<title>Supybot's registry</title>
<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.snarfThrottle</registrygroup>
configuration variable is all about, we'd do this:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config help supybot.snarfThrottle
&lt;supybot&gt; jemfinch|lambda: A floating point number of seconds to throttle snarfed
URLs, in order to prevent loops between two bots snarfing the same URLs and
having the snarfed URL in the output of the snarf message. (Current value:
10.0)
</ircsession>
<para>
Pretty simple, eh?
</para>
</sect4>
<sect4>
<title>Getting/setting registry values</title>
<para>
Now, if you're curious what the current value of a
configuration variable is, you'll use the
<botcommand>config</botcommand> command with one
argument, the name of the variable you want to see the
value of:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.reply.whenAddressedBy.chars
&lt;supybot&gt; jemfinch|lambda: '@'
</ircsession>
<para>
To set this value, just stick an extra argument after
the name:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.reply.whenAddressedBy.chars @$
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
</ircsession>
<para>
Now, check this out:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; $config supybot.reply.whenAddressedBy.chars
&lt;supybot&gt; jemfinch|lambda: '@$'
</ircsession>
<para>
Note that we used <literal>$</literal> as our prefix
character, and that the value of the configuration
variable changed. If I were to use the
<botcommand>flush</botcommand> command now, this
change would be flushed to the registry file on disk
(this would also happen if I made the bot quit, or
pressed
<keycombo>
<keycap>Ctrl</keycap>
<keycap>C</keycap>
</keycombo>
in the terminal the bot was running in). Instead,
I'll revert the change:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; $config supybot.reply.whenAddressedBy.chars @
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; $note that this makes no response.
</ircsession>
<para>
If you're ever curious what the default for a given
configuration variable is, use the <botcommand>config
default</botcommand> command:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config default supybot.reply.whenAddressedBy.chars
&lt;supybot&gt; jemfinch|lambda: ''
</ircsession>
<para>
Thus, to reset a configuration variable to its default
value, you can simply say:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config supybot.reply.whenAddressedBy.chars [config default
supybot.reply.whenAddressedBy.chars]
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; @note that this does nothing
</ircsession>
<para>
Simple, eh?
</para>
</sect4>
</sect3>
<sect3>
<title>Searching the registry</title>
<para>
Now, let's say you want to find all configuration
variables that might be even remotely related to opping.
For that, you'll want the <botcommand>config
search</botcommand> command. Check this out:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; @config search op
&lt;supybot&gt; jemfinch|lambda:
supybot.plugins.Enforcer.autoOp,
supybot.plugins.Enforcer.autoHalfop,
supybot.plugins.Enforcer.takeRevenge.onOps,
supybot.plugins.Enforcer.cycleToGetOps,
supybot.plugins.Topic, supybot.plugins.Topic.public,
supybot.plugins.Topic.separator,
supybot.plugins.Topic.format,
supybot.plugins.Topic.recognizeTopiclen,
supybot.plugins.Topic.default,
supybot.plugins.Topic.undo.maz, 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>
<sect3>
<title>Channel-specific configuration</title>
<para>
Many configuration variables can be specific to individual
channels. The <plugin>Config</plugin> plugin provides an
easy way to configure something for a specific channel;
for instance, in order to set the prefix chars for a
specific channel, do this in that channel:
</para>
<ircsession>
config channel supybot.reply.whenAddressedBy.chars !
</ircsession>
<para>
That'll set the prefix chars in the channel that message
is sent in to <literal>!</literal>. Voila,
channel-specific values!
</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>

View File

@ -1,359 +0,0 @@
<!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've not given it anything to recognize you
from! You'll need to identify with the bot
(<botcommand>help identify</botcommand> to see how that
works) or add your hostmask to your user record
(<botcommand>help addhostmask</botcommand> to see how that
works) for it to know that you're you. You may wish to
note that <botcommand>addhostmask</botcommand> can accept
a password; rather than identify, you can send the command
<botcommand>addhostmask myOwnerUser [hostmask]
myOwnerUserPassword</botcommand> and the bot will add your
current hostmask to your owner user (of course, you should
change <literal>myOwnerUser</literal> and
<literal>myOwnerUserPassword</literal> appropriately for
your bot).
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
How do I make Supybot op my users?
</para>
</question>
<answer>
<para>
First, you'll have to make sure that your users register
with the bot. They can do this with the
<botcommand>register</botcommand> command. After they do
so, you'll want to add the
<capability>#channel,op</capability> capability to their
user. Use the <botcommand>channel
addcapability</botcommand> command to do this. After
that, your users should be able to use the
<botcommand>op</botcommand> command to get ops.
</para>
<para>
If you want your users to be auto-opped when they join the
channel, you'll need to load the <plugin>Enforcer</plugin>
plugin and turn its <registrygroup>autoOp</registrygroup>
configuration variable on. Use the
<botcommand>config</botcommand> command to do so. Here's
an example of how to do these steps:
</para>
<ircsession>
&lt;jemfinch|lambda&gt; I'm going to make an example session for giving
you auto-ops, for our FAQ.
&lt;dunk1&gt; ah ok ;]
&lt;jemfinch|lambda&gt; First, I need you to register with supybot, using
the "register" command (remember to send it in private).
&lt;dunk1&gt; done
&lt;jemfinch|lambda&gt; what name are you registered under?
&lt;dunk1&gt; dunk1
&lt;jemfinch|lambda&gt; ok, cool.
&lt;jemfinch|lambda&gt; @channel addcapability dunk1 op
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; now use the "op" command to get ops.
&lt;dunk1&gt; @op
&mdash; supybot gives channel operator status to dunk1
&lt;dunk1&gt; works!
&lt;dunk1&gt; ;]
&lt;jemfinch|lambda&gt; @load Enforcer
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; @config channel supybot.plugins.Enforcer.autoOp On
&lt;supybot&gt; jemfinch|lambda: The operation succeeded.
&lt;jemfinch|lambda&gt; ok, now cycle the channel (part and then rejoin)
&lt;&ndash; dunk1 (dunker@freebsd.nl) has left #supybot
&ndash;&gt; dunk1 (dunker@freebsd.nl) has joined #supybot
&mdash; supybot gives channel operator status to dunk1
&lt;jemfinch|lambda&gt; cool, thanks :)
</ircsession>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can users with the <capability>admin</capability>
capability change configuration variables?
</para>
</question>
<answer>
<para>
Currently, no. Since this is the first release of Supybot
that uses the registry, we wanted to stay on the
conservative side and require the
<capability>owner</capability> capability for changing all
non-channel-related configuration variables. Feel free to
make your case to us as to why a certain configuration
variable should only require the
<capability>admin</capability> capability instead of the
<capability>owner</capability> capability, and if we agree
with you, we'll change it for the next release.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Can Supybot do factoids?
</para>
</question>
<answer>
<para>
Supybot most certainly can! In fact, we offer three
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. Factoids
also uses a channel-specific database instead of a global
database though that's configurable with the
<registrygroup>supybot.databases.plugins.channelSpecific</registrygroup>
configuration variable.
</para>
<para>
<plugin>MoobotFactoids</plugin> (written by
<nick>Strike</nick>) is much more full-featured, offering
users the ability to define factoids in a slightly more
user-friendly way, as well as parsing factoids to handle
&lt;reply&gt;, &lt;action&gt;, and alternations (defining
a factoid &ldquo;test&rdquo; as
&ldquo;&lt;reply&gt;(foo|bar|baz)&rdquo; will make the bot
send &ldquo;foo&rdquo; or &ldquo;bar&rdquo; or
&ldquo;baz&rdquo; to the channel (without the normal
&ldquo;test is &rdquo; 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
comaptible 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.
</para>
<para>
<plugin>Infobot</plugin> (written by
<nick>jamessan</nick>) is used for Infobot compatibility;
if you still want the basic functionality of Infobot, this
is the plugin to use.
</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 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.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Do I really have to use separate databases for each
channel?
</para>
</question>
<answer>
<para>
Of course not! We default to separate databases for each
channel because, well, that's what <nick>jemfinch</nick>
always thought was reasonable. Anyway, if you change the
configuration variable
<registrygroup>supybot.databases.plugins.channelSpecific</registrygroup>
to <literal>False</literal> instead of
<literal>True</literal>, for <emphasis>most</emphasis>
databases, each channel will share the same database (the
exceptions are <plugin>ChannelStats</plugin>,
<plugin>Herald</plugin>, <plugin>Seen</plugin>, and
<plugin>WordStats</plugin>, which are inherently rather
channel-based).
</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>
<qandaentry>
<question>
<para>
I added an alias, but it doesn't work!
</para>
</question>
<answer>
<para>
Take a look at <botcommand>help &lt;alias you
added&gt;</botcommand>. If the alias the bot has listed
doesn't match what you're giving it, chances re you need
to quote your alias in order for the brackets not to be
evaluated. For instance, if you're adding an alias to
give you a link to your homepage, you need to say:
<para>
<ircsession>
alias add mylink "format concat http://myhost.com/
[urlquote $1]"
</ircsession>
<para>
and not:
</para>
<ircsession>
alias add mylink format concat http://myhost.com/
[urlquote $1]
</ircsession>
<para>
The first version works; the second version will always
return the same url.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Is there a command that can tell me what capability
another command requires?
</para>
</question>
<answer>
<para>
No, there isn't, and there probably never will be.
Commands have the flexibility to check any capabilities
they wish to check; while this flexibility is useful, it
also makes it hard to guess what capability a certain
command requires. We could make a solution that would
work in a large majority of cases, but it wouldn't (and
couldn't!) be absolutely correct in all circumstances, and
since we're anal and we hate doing things halfway, we
probably won't ever add this partial solution.
</para>
<para>
Besides, is the error message so bad? If we did have such
a command, many users would call the command, see that
they could perform it, and then run the command, thus
doubling the activity in the channel. Is that something
you want?
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
How do I make my Supybot connect to multiple servers?
</para>
</question>
<answer>
<para>
Just use the <botcommand>connect</botcommand> command in
the <plugin>Owner</plugin> plugin. Easy as pie!
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
I found a bug, what do I do?
</para>
</question>
<answer>
<para>
Submit it on Sourceforge through our Sourceforge project
page:
<ulink
url="http://sourceforge.net/tracker/?group_id=58965&amp;atid=489447">
http://sourceforge.net/tracker/?group_id=58965&amp;atid=489447
</ulink>. If Sourceforge happens to be down when you try
to submit your bug, then post it in the "Supybot Developer
Discussion" forum at our forums at
<ulink url="http://forums.supybot.org">
http://forums.supybot.org/
</ulink>. If that doesn't work, email
<email>supybot-bugs@lists.sourceforge.net</email>. If
that doesn't work, email
<email>jemfinch@supybot.org</email>. If that doesn't
work, find yourself some carrier pigeons and &hellip; hah!
You thought I was serious!
</para>
<para>
Anyway, when you submit your bug, we'll need several
things. If the bug involved an uncaught exception, we
need the traceback (basically the stuff from
&ldquo;Uncaught exception in &hellip;&rdquo; to the next
log entry). We'd also like to see the commands that
caused the bug, or happened around the time you saw the
bug. If the bug involved a database, we'd love to see the
database. Remember, it's always worse to send us too
little information in a bug report than too much.
</para>
</answer>
</qandaentry>
</qandaset>
</article>

View File

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

View File

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

View File

@ -1,633 +0,0 @@
<!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>
<revision>
<revnumber>0.5</revnumber>
<date>4 Sep 2004</date>
<revremark>Updated Docbook translation</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) 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.
"""
__revision__ = "$Id$"
__author__ = ''
import supybot.plugins as plugins
import supybot.conf as conf
import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
def configure(onStart, afterConnect, 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:
</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 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.
</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, you should get those
values from the registry.
</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(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
<botcommand>random</botcommand>. 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 is 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 &ndash; 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, you'll very
rarely be using these objects.
</para>
<para>
(In case you're curious, the answer is yes, you
<emphasis>must</emphasis> name your arguments <varname>(self,
irc, msg, args)</varname>. The names of those arguments is
one of the ways that supybot uses to determine which methods
in a plugin class are commands and which aren't. And while
we're talking about naming restrictions, all your commands
should be named in all-lowercase with no underscores. Before
calling a command, supybot always converts the command name to
lowercase and removes all dashes and underscores. On the
other hand, you now know an easy way to make sure a method is
never called (even if its arguments are <varname>(self, irc,
msg, args)</varname>, however unlikely that may be). Just
name it with an underscore or an uppercase letter in it :))
</para>
<para>
You'll also note that the docstring is odd. The wonderful
thing about the supybot framework is that it's easy to write
complete commands with help and everything: the docstring
<emphasis>is</emphasis> the help! Given the above docstring,
this is what a supybot does:
</para>
<ircsession>
&lt;jemfinch&gt; @help random
&lt;angryman&gt; jemfinch: (random takes no arguments) -- Returns the
next random number from the random number generator.
</ircsession>
<para>
Now on to the actual body of the function:
</para>
<programlisting>
irc.reply(msg, str(self.rng.random()))
</programlisting>
<para>
<function>irc.reply</function> simply takes one simple
argument: a string The string is the reply to be sent. Don't
worry about length restrictions or anything
&ndash; 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()" &ndash; we had to give
<function>irc.reply</function> a string.
</para>
<para>
Anyway, now that we have an RNG, we have a need for seed! Of
course, Python gives us a good seed already (it uses the
current time as a seed if we don't give it one) but users
might want to be able to repeat "random" sequences, so letting
them set the seed is a good thing. So we'll add a seed
command to give the RNG a specific seed:
</para>
<programlisting>
def seed(self, irc, msg, args):
"""&lt;seed&gt;
Sets the seed of the random number generator. &lt;seed&gt; must be
an int or a long.
"""
seed = privmsgs.getArgs(args)
try:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error(msg, '&lt;seed&gt; must be a valid int or long.')
return
self.rng.seed(seed)
irc.replySuccess()
</programlisting>
<para>
So this one's a bit more complicated. But it's still pretty
simple. The method name is <botcommand>seed</botcommand> 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 <literal>"Error: "</literal> 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 <varname>seed</varname> 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 &ndash; 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
<function>irc.replySuccess</function> says. By default, it
has the very dry (and appropriately robot-like) "The operation
succeeded." but you're perfectly welcome to customize it
yourself &ndash; the registry was written to be modified!
</para>
<para>
So that's a bit more complicated command. But we still
haven't dealt with multiple arguments. Let's do that
next.
</para>
<para>
So these random numbers are useful, but they're not the kind
of random numbers we usually want in Real Life. In Real Life,
we like to tell someone to "pick a number between 1 and 10."
So let's write a function that does that. Of course, we won't
hardcode the 1 or the 10 into the function, but we'll take
them as arguments. First the function:
</para>
<programlisting>
def range(self, irc, msg, args):
"""&lt;start&gt; &lt;end&gt;
Returns a number between &lt;start&gt; and &lt;end&gt;, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, required=2)
try:
end = int(end)
start = int(start)
except ValueError:
irc.error(msg, '&lt;start&gt; and &lt;end&gt; must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(msg, str(self.rng.randrange(start, end+1)))
</programlisting>
<para>
Pretty simple. This is becoming old hat by now. The only new
thing here is the call to
<function>privmsgs.getArgs</function>. We have to make sure,
since we want two values, to pass a keyword parameter
"required" into <function>privmsgs.getArgs</function>. Of
course, <function>privmsgs.getArgs</function> handles all the
checking for missing arguments and whatnot so we don't have
to.
</para>
<para>
The <classname>Random</classname> object we're using offers us
a "sample" method that takes a sequence and a number (we'll
call it <varname>N</varname>) and returns a list of
<varname>N</varname> items taken randomly from the sequence.
So I'll show you an example that takes advantage of multiple
arguments but doesn't use
<function>privmsgs.getArgs</function> (and thus has to handle
its own errors if the number of arguments isn't right).
Here's the code:
</para>
<programlisting>
def sample(self, irc, msg, args):
"""&lt;number of items&gt; [&lt;text&gt; ...]
Returns a sample of the &lt;number of items&gt; taken from the remaining
arguments. Obviously &lt;number of items&gt; must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error('&lt;number of items&gt; must be an integer.')
return
if n &gt; len(args):
irc.error('&lt;number of items&gt; must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(utils.commaAndify(map(repr, sample)))
</programlisting>
<para>
Most everything here is familiar. The difference between this
and the previous examples is that we're dealing with
<varname>args</varname> directly, rather than through
<function>getArgs</function>. Since we already have the
arguments in a list, it doesn't make any sense to have
<function>privmsgs.getArgs</function> smush them all together
into a big long string that we'll just have to re-split. But
we still want the nice error handling of
<function>privmsgs.getArgs</function>. So what do we do? We
raise <classname>callbacks.ArgumentError</classname>! That's
the secret juju that <function>privmsgs.getArgs</function> is
doing; now we're just doing it ourself. Someone up our
callchain knows how to handle it so a neat error message is
returned. So in this function, if
<function>.pop(0)</function> fails, we weren't given enough
arguments and thus need to tell the user how to call us.
</para>
<para>
So we have the args, we have the number, we do a simple call
to <function>random.sample</function> and then we do this
funky <function>utils.commaAndify</function> to it. Yeah, so
I was running low on useful names :) Anyway, what it does is
take a list of strings and return a string with them joined by
a comma, the last one being joined with a comma and "and". So
the list ['foo', 'bar', 'baz'] becomes "foo, bar, and baz".
It's pretty useful for showing the user lists in a useful
form. We map the strings with <function>repr()</function>
first just to surround them with quotes.
</para>
<para>
So we have one more example. Yes, I hear your groans, but
it's pedagogically useful :) This time we're going to write a
command that makes the bot roll a die. It'll take one
argument (the number of sides on the die) and will respond
with the equivalent of "/me rolls a __" where __ is the number
the bot rolled. So here's the code:
</para>
<programlisting>
def diceroll(self, irc, msg, args):
"""[&lt;number of sides&gt;]
Rolls a die with &lt;number of sides&gt; sides. The default number
of sides is 6.
"""
try:
n = privmsgs.getArgs(args, required=0, optional=1)
if not n:
n = 6
n = int(n)
except ValueError:
irc.error(msg, 'Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.reply(s, action=True)
</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>
You'll also note that <function>irc.reply</function> was given
a keyword argument here, <varname>action</varname>. This
means that the reply is to be made as an action rather than a
normal reply.
</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>Using the registry in your plugin</title>
<para>
TODO: Describe the registry and how to write a proper plugin
configure function.
</para>
</sect2>
<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>
</sect1>
</article>

View File

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

View File

@ -1,43 +0,0 @@
(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))

View File

@ -1,41 +0,0 @@
.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;
}

View File

@ -1,23 +0,0 @@
<!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>

View File

@ -1,139 +0,0 @@
<!-- 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;
>

207
docs/FAQ
View File

@ -1,207 +0,0 @@
Q: Why does my bot not recognize me or tell me that I don't have the
"owner" capability?
A: Because you've 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> I'm going to make an example session for giving
you auto-ops, for our FAQ.
<dunk1> ah ok ;]
<jemfinch> First, I need you to register with supybot, using
the "register" command (remember to send it in
private).
<dunk1> done
<jemfinch> what name are you registered under?
<dunk1> dunk1
<jemfinch> ok, cool.
<jemfinch> @channel addcapability dunk1 op
<supybot> jemfinch: The operation succeeded.
<jemfinch> now use the "op" command to get ops.
<dunk1> @op
--- supybot gives channel operator status to dunk1
<dunk1> works!
<dunk1> ;]
<jemfinch> @load Enforcer
<supybot> jemfinch: The operation succeeded.
<jemfinch> @config channel supybot.plugins.Enforcer.autoOp On
<supybot> jemfinch: The operation succeeded.
<jemfinch> 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> cool, thanks :)
Q: Can users with the "admin" capability change configuration
variables?
A: Currently, no. 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: Can Supybot do factoids?
A: Supybot most certainly can! In fact, we offer three 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, though
that's configurable with the
supybot.databases.plugins.channelSpecific configuration variable.
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>, 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.
Infobot (written by jamessan) is used for Infobot compatibility;
if you still want the basic functionality of Infobot, this is the
plugin to use.
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: Do I really have to use separate databases for each channel?
A: Of course not! We default to separate databases for each channel
because, well, that's what jemfinch always thought was
reasonable. Anyway, if you change the configuration variable
supybot.databases.plugins.channelSpecific to False instead of
True, for *most* databases, each channel will share the same
database (the exceptions are ChannelStats, Herald, Seen, and
WordStats, which are inherently rather channel-based).
Q: Karma doesn't seem to work for me.
A: Karma by default doesn't acknowledge karma updates. If you check
the karma of whatever you increased/decreased, you'll note that
your increment or decrement still took place. If you'd rather
Karma acknowledge karma updates, change the
supybot.plugins.Karma.response configuration variable to On.
Q: I added an alias, but it doesn't work!
A: Take a look at "help <alias you added>". If the alias the bot has
listed doesn't match what you're giving it, chances are you need
to quote your alias in order for the brackets not to be
evaluated. For instance, if you're adding an alias to give you a
link to your homepage, you need to say:
alias add mylink "format concat http://my.host.com/ [urlquote $1]"
and not:
alias add mylink format concat http://my.host.com/ [urlquote $1]
The first version works; the second version will always return the
same url.
Q: Is there a command that can tell me what capability another
command requires?
A: No, there isn't, and there probably never will be. Commands have
the flexibility to check any capabilities they wish to check;
while this flexibility is useful, it also makes it hard to guess
what capability a certain command requires. We could make a
solution that would work in a large majority of cases, but it
wouldn't (and couldn't!) be absolutely correct in all
circumstances, and since we're anal and we hate doing things
halfway, we probably won't ever add this partial solution.
Besides, is the error message so bad? If we did have such a
command, many users would call the command, see that they could
perform it, and then run the command, thus doubling the activity
in the channel. Is that something you want?
Q: How do I make my Supybot connect to multiple servers?
A: Just use the "connect" command in the Owner plugin. Easy as pie!
Q: I've edited my configuration file, but my Supybot doesn't notice
the changes! Even if I restart it, it doesn't see them. What's
the deal?
A: Supybot won't reload its configuration files unless you tell it
to. In addition, when Supybot exits (and periodically while it
runs) it flushes its configuration file to disk. The safest way
to avoid problems with configuration file edits is simply to exit
the bot before editing the configuration file(s). If you don't
wish to do that, however, you can edit the file, save the changes,
and tell the bot to reload its configuration, either via the
reload command in the Config plugin, or by sending the bot a
SIGHUP. There is a brief period in this whole sequence where the
bot can flush its configuration to disk after you write your
changes, but we even have something to fix that: set the
configuration variable supybot.flush to False, and then reload the
configuration.
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.

View File

@ -1,135 +0,0 @@
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. You should have already read through INSTALL
before reading any further.
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 default value of
supybot.replies.genericNoCapability -- it's so long that it'll
stretch across two messages.
<jemfinch|lambda> $config default
supybot.replies.genericNoCapability
<lambdaman> jemfinch|lambda: You're missing some capability
you need. This could be because you actually
possess the anti-capability for the capability
that's required of you, or because the channel
provides that anti-capability by default, or
because the global capabilities include that
anti-capability. Or, it could be because the
channel or the global defaultAllow is set to
False, meaning (1 more message)
<jemfinch|lambda> $more
<lambdaman> jemfinch|lambda: that no commands are allowed
unless explicitly in your capabilities. Either
way, you can't do what you want to do.
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!

View File

@ -1,48 +0,0 @@
So, you want to hack on Supybot? Cool! I'm glad -- more developers
means more users, and more users means better software (although I
suppose more developers means better software even without the
addition of more users :))
Anyway, there are a few things you should know before you submit your
code to be accepted into Supybot. The first, and most important
thing is that we really do value your contribution. We may say that
it's not appropriate for the core distribution and any number of
varying reasons, but regardless, we're happy that you're hacking on
Supybot and bending it to your will, and we'll be happy to post your
patch as long as it applies cleanly.
The second thing you should know is that, despite the fact that we're
happy you want to contribute to Supybot, we're not afraid to piss you
off by turning down your code. We won't hesitate to reject code
because it's "bad" or because it doesn't fit our style guidelines
(read docs/STYLE). We don't really care if it makes you angry or
makes you use another IRC bot; we're in the practice of writing good
software, not placating whiners. Despite this, we're not entirely
heartless, and if you've done something we're interested in, we're
willing to work with you and your code until such a time as it's
ready to be accepted into the core. But if, at some point, we say,
"This needs fixed" and you say, "I refuse to fix it," you can go put
your code on the patch tracker; our time together is done. Supybot
is #1 here -- we don't care about your feelings, we don't care about
jamessan's feelings, we don't care about jemfinch's feelings if it
means that the code quality and user experience of Supybot is to
suffer.
Anyway, the normal process is that you'll submit a few patches,
jemfinch will review them and tell you what needs to happen for them
to be accepted into the core, you'll fix those problems, jemfinch
will review them again, that cycle will repeat a few times. When
your code is to jemfinch's satisfaction, it'll be integrated into the
core. For many people, this is the end of the line. For some
others (perhaps you!), you'll continue to write patches for Supybot,
and your coding ability and commitment will be obvious through
those. If your code quality is consistently high enough that
jemfinch (or other Supybot developers) don't have to spend a
significant amount of time reviewing your code, you'll be added as a
developer on the SF.net project and given commit access to our CVS
repository. From then on, you can do what you want, but be aware
that the other developers are watching what you do -- if you have a
big architecture change, you should probably talk to them before you
commit.
So welcome aboard, and have fun hacking on Supybot!

View File

@ -1,271 +0,0 @@
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.
replies: Sends a collection of messages to a given
target, much like reply; except in this case, the user
can configure whether the messages will be sent
one-by-one or combined into a single message. Thus, the
method accepts a "prefixer" argument, which prefixes the
messages with a given string (or according to a given
function), a "joiner" string (or function) used to join
the messages into a single message if necessary, and an
onlyPrefixFirst argument which determines whether only
the first message will be prefixed when the messages are
sent separately (it defaults to False).

View File

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

View File

@ -1,441 +0,0 @@
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 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.
"""
__revision__ = "$Id$"
__author__ = ''
import supybot.plugins as plugins
import supybot.conf as conf
import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.callbacks as 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, you should get those values
from the registry.
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 is 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
irc.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 -- the registry 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 :)

View File

@ -1,130 +0,0 @@
====================================================================
Code not following these style guidelines fastidiously is likely
(*very* likely) not to be accepted into the Supybot core.
====================================================================
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.
This is **NON-NEGOTIABLE**. Your code will not be accepted while you
are violating this guidline.
Identation is 4 spaces per level. No tabs. This also is
**NON-NEGOTIABLE**. Your code, again, will *never* be accepted while
you have literal tabs in it.
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.
Raw strings (r'' or r"") should be used for regular expressions.
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).
Functions calls should look like this: "foo(bar(baz(x), y))". They
should not look like "foo (bar (baz (x), y))", or like
"foo(bar(baz(x), y) )" or like anything else. I hate extraneous
spaces.
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).
Unless absolutely required by some external force, imports should be
ordered by the string length of the module imported. I just think it
looks prettier.
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'. plugins.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')
try:
s = fd.read()
finally:
fd.close()
This is to be sure the bot doesn't leak file descriptors.
All plugin files should include a docstring decsribing what the
plugin does. This docstring will be returned when the user is
configuring the plugin. All plugin classes should also include a
docstring describing how to do things with the plugin; this docstring
will be returned when the user requests help on a plugin name.
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.
People who use string concatenation in a for loop will be swiftly
kicked in the head.
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. If you got the %d wrong, you'll get an exception that says,
"foo instance can't be converted to an integer." But if you use %s,
you'll get to see your nice little foo instance, if it doesn't
convert to a string cleanly, and if it does convert cleanly, you'll
get to see what you expect to see. Basically, %d just sucks.
As a corrolary to the above, note that sometimes %f is used, but on
when floats need to be formatted, e.g., %.2f.
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.
While on the topic of logs, note that we do not use % with logged
strings; we simple pass the format parameters as additional
arguments. The reason is simple: the logging module supports it, and
it's cleaner (fewer tokens/glyphs) to read.
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; PluginTestCase
checks that every command has documentation)
All uses of eval() that expect to get integrated in Supybot must be
approved by jemfinch, no exceptions. Chances are, it won't be
accepted. Have you looked at utils.safeEval?
SQL table names should be all-lowercase and include underscores to
separate words. This is because SQL itself is case-insensitive.
This doesn't change, however the fact that variable/member names
should be camel case.
SQL statements in code should put SQL words in ALL CAPS:
"""SELECT quote FROM quotes ORDER BY random() LIMIT 1""". This makes
SQL significantly easier to read.

View File

@ -1,154 +0,0 @@
#!/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 supybot.plugins as plugins
import random
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.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 Normal file
View File

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

26
index.markdown Normal file
View File

@ -0,0 +1,26 @@
---
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)

View File

@ -1,450 +0,0 @@
"""Beautiful Soup
Elixir and Tonic
"The Screen-Scraper's Friend"
The BeautifulSoup class turns arbitrarily bad HTML into a tree-like
nested tag-soup list of Tag objects and text snippets. A Tag object
corresponds to an HTML tag. It knows about the HTML tag's attributes,
and contains a representation of everything contained between the
original tag and its closing tag (if any). It's easy to extract Tags
that meet certain criteria.
A well-formed HTML document will yield a well-formed data
structure. An ill-formed HTML document will yield a correspondingly
ill-formed data structure. If your document is only locally
well-formed, you can use this to process the well-formed part of it.
#Example:
#--------
from BeautifulSoup import BeautifulSoup
text = '''<html>
<head><title>The Title</title></head>
<body>
<a class="foo" href="http://www.crummy.com/">Link <i>text (italicized)</i></a>
<a href="http://www.foo.com/">Link text 2</a>
</body>
</html>'''
soup = BeautifulSoup()
soup.feed(text)
print soup("a") #Returns a list of 2 Tag objects, one for each link in
#the source
print soup.first("a", {'class':'foo'})['href'] #Returns http://www.crummy.com/
print soup.first("title").contents[0] #Returns "The title"
print soup.first("a", {'href':'http://www.crummy.com/'}).first("i").contents[0]
#Returns "text (italicized)"
#Example of SQL-style attribute wildcards -- all four 'find' calls will
#find the link.
#----------------------------------------------------------------------
soup = BeautifulSoup()
soup.feed('''<a href="http://foo.com/">bla</a>''')
print soup.fetch('a', {'href': 'http://foo.com/'})
print soup.fetch('a', {'href': 'http://%'})
print soup.fetch('a', {'href': '%.com/'})
print soup.fetch('a', {'href': '%o.c%'})
#Example with horrible HTML:
#---------------------------
soup = BeautifulSoup()
soup.feed('''<body>
Go <a class="that" href="here.html"><i>here</i></a>
or <i>go <b><a href="index.html">Home</a>
</html>''')
print soup.fetch('a') #Returns a list of 2 Tag objects.
print soup.first(attrs={'href': 'here.html'})['class'] #Returns "that"
print soup.first(attrs={'class': 'that'}).first('i').contents[0] #returns "here"
This library has no external dependencies. It works with Python 1.5.2
and up. If you can install a Python extension, you might want to use
the ElementTree Tidy HTML Tree Builder instead:
http://www.effbot.org/zone/element-tidylib.htm
You can use BeautifulSoup on any SGML-like substance, such as XML or a
domain-specific language that looks like HTML but has different tag
names. For such purposes you may want to use the BeautifulStoneSoup
class, which knows nothing at all about HTML per se. I also reserve
the right to make the BeautifulSoup parser smarter between releases,
so if you want forwards-compatibility without having to think about
it, you might want to go with BeautifulStoneSoup.
Release status:
(I do a new release whenever I make a change that breaks backwards
compatibility.)
Current release:
Applied patch from Richie Hindle (richie at entrian dot com) that
makes tag.string a shorthand for tag.contents[0].string when the tag
has only one string-owning child.
1.2 "Who for such dainties would not stoop?" (2004/07/08): Applied
patch from Ben Last (ben at benlast dot com) that made
Tag.renderContents() correctly handle Unicode.
Made BeautifulStoneSoup even dumber by making it not implicitly
close a tag when another tag of the same type is encountered; only
when an actual closing tag is encountered. This change courtesy of
Fuzzy (mike at pcblokes dot com). BeautifulSoup still works as
before.
1.1 "Swimming in a hot tureen": Added more 'nestable' tags. Changed
popping semantics so that when a nestable tag is encountered, tags are
popped up to the previously encountered nestable tag (of whatever kind).
I will revert this if enough people complain, but it should make
more people's lives easier than harder.
This enhancement was suggested by Anthony Baxter (anthony at
interlink dot com dot au).
1.0 "So rich and green": Initial release.
Retreived from: http://www.crummy.com/software/BeautifulSoup/
"""
__author__ = "Leonard Richardson (leonardr@segfault.org)"
__version__ = "1.1 $Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Leonard Richardson"
__license__ = "Python"
from sgmllib import SGMLParser
import string
import types
class PageElement:
"""Contains the navigational information for some part of the page
(either a tag or a piece of text)"""
def __init__(self, parent=None, previous=None):
self.parent = parent
self.previous = previous
self.next = None
class NavigableText(PageElement):
"""A simple wrapper around a string that keeps track of where in
the document the string was found. Doesn't implement all the
string methods because I'm lazy. You could have this extend
UserString if you were using 2.2."""
def __init__(self, string, parent=None, previous=None):
PageElement.__init__(self, parent, previous)
self.string = string
def __eq__(self, other):
return self.string == str(other)
def __str__(self):
return self.string
def strip(self):
return self.string.strip()
class Tag(PageElement):
"""Represents a found HTML tag with its attributes and contents."""
def __init__(self, name, attrs={}, parent=None, previous=None):
PageElement.__init__(self, parent, previous)
self.name = name
self.attrs = attrs
self.contents = []
self.foundClose = 0
def get(self, key, default=None):
return self._getAttrMap().get(key, default)
def __call__(self, *args):
return apply(self.fetch, args)
def __getitem__(self, key):
return self._getAttrMap()[key]
def __setitem__(self, key, value):
self._getAttrMap()
self.attrMap[key] = value
for i in range(0, len(self.attrs)):
if self.attrs[i][0] == key:
self.attrs[i] = (key, value)
def _getAttrMap(self):
if not hasattr(self, 'attrMap'):
self.attrMap = {}
for (key, value) in self.attrs:
self.attrMap[key] = value
return self.attrMap
def __repr__(self):
return str(self)
def __ne__(self, other):
return not self == other
def __eq__(self, other):
if not isinstance(other, Tag) or self.name != other.name or self.attrs != other.attrs or len(self.contents) != len(other.contents):
return 0
for i in range(0, len(self.contents)):
if self.contents[i] != other.contents[i]:
return 0
return 1
def __str__(self):
attrs = ''
if self.attrs:
for key, val in self.attrs:
attrs = attrs + ' %s="%s"' % (key, val)
close = ''
closeTag = ''
if self.isSelfClosing():
close = ' /'
elif self.foundClose:
closeTag = '</%s>' % self.name
s = self.renderContents()
if not hasattr(self, 'hideTag'):
s = '<%s%s%s>' % (self.name, attrs, close) + s + closeTag
return s
def renderContents(self):
s='' #non-Unicode
for c in self.contents:
try:
s = s + str(c)
except UnicodeEncodeError:
if type(s) <> types.UnicodeType:
s = s.decode('utf8') #convert ascii to Unicode
#str() should, strictly speaking, not return a Unicode
#string, but NavigableText never checks and will return
#Unicode data if it was initialised with it.
s = s + str(c)
return s
def isSelfClosing(self):
return self.name in BeautifulSoup.SELF_CLOSING_TAGS
def append(self, tag):
self.contents.append(tag)
def first(self, name=None, attrs={}, contents=None, recursive=1):
r = None
l = self.fetch(name, attrs, contents, recursive)
if l:
r = l[0]
return r
def fetch(self, name=None, attrs={}, contents=None, recursive=1):
"""Extracts Tag objects that match the given criteria. You
can specify the name of the Tag, any attributes you want the
Tag to have, and what text and Tags you want to see inside the
Tag."""
if contents and type(contents) != type([]):
contents = [contents]
results = []
for i in self.contents:
if isinstance(i, Tag):
if not name or i.name == name:
match = 1
for attr, value in attrs.items():
check = i.get(attr)
#By default, find the specific value called for.
#Use SQL-style wildcards to find substrings, prefix,
#suffix, etc.
result = (check == value)
if check and value:
if len(value) > 1 and value[0] == '%' and value[-1] == '%' and value[-2] != '\\':
result = (check.find(value[1:-1]) != -1)
elif value[0] == '%':
print "blah"
result = check.rfind(value[1:]) == len(check)-len(value)+1
elif value[-1] == '%':
result = check.find(value[:-1]) == 0
if not result:
match = 0
break
match = match and (not contents or i.contents == contents)
if match:
results.append(i)
if recursive:
results.extend(i.fetch(name, attrs, contents, recursive))
return results
class BeautifulSoup(SGMLParser, Tag):
"""The actual parser. It knows the following facts about HTML, and
not much else:
* Some tags have no closing tag and should be interpreted as being
closed as soon as they are encountered.
* Most tags can't be nested; encountering an open tag when there's
already an open tag of that type in the stack means that the
previous tag of that type should be implicitly closed. However,
some tags can be nested. When a nestable tag is encountered,
it's okay to close all unclosed tags up to the last nestable
tag. It might not be safe to close any more, so that's all it
closes.
* The text inside some tags (ie. 'script') may contain tags which
are not really part of the document and which should be parsed
as text, not tags. If you want to parse the text as tags, you can
always get it and parse it explicitly."""
SELF_CLOSING_TAGS = ['br', 'hr', 'input', 'img', 'meta', 'spacer',
'link', 'frame']
NESTABLE_TAGS = ['font', 'table', 'tr', 'td', 'th', 'tbody', 'p',
'div']
QUOTE_TAGS = ['script']
IMPLICITLY_CLOSE_TAGS = 1
def __init__(self, text=None):
Tag.__init__(self, '[document]')
SGMLParser.__init__(self)
self.quoteStack = []
self.hideTag = 1
self.reset()
if text:
self.feed(text)
def feed(self, text):
SGMLParser.feed(self, text)
self.endData()
def reset(self):
SGMLParser.reset(self)
self.currentData = ''
self.currentTag = None
self.tagStack = []
self.pushTag(self)
def popTag(self, closedTagName=None):
tag = self.tagStack.pop()
if closedTagName == tag.name:
tag.foundClose = 1
# Tags with just one string-owning child get the same string
# property as the child, so that soup.tag.string is shorthand
# for soup.tag.contents[0].string
if len(self.currentTag.contents) == 1 and \
hasattr(self.currentTag.contents[0], 'string'):
self.currentTag.string = self.currentTag.contents[0].string
#print "Pop", tag.name
self.currentTag = self.tagStack[-1]
return self.currentTag
def pushTag(self, tag):
#print "Push", tag.name
if self.currentTag:
self.currentTag.append(tag)
self.tagStack.append(tag)
self.currentTag = self.tagStack[-1]
def endData(self):
if self.currentData:
if not string.strip(self.currentData):
if '\n' in self.currentData:
self.currentData = '\n'
else:
self.currentData = ' '
o = NavigableText(self.currentData, self.currentTag, self.previous)
if self.previous:
self.previous.next = o
self.previous = o
self.currentTag.contents.append(o)
self.currentData = ''
def _popToTag(self, name, closedTag=0):
"""Pops the tag stack up to and including the most recent
instance of the given tag. If a list of tags is given, will
accept any of those tags as an excuse to stop popping, and will
*not* pop the tag that caused it to stop popping."""
if self.IMPLICITLY_CLOSE_TAGS:
closedTag = 1
numPops = 0
mostRecentTag = None
oneTag = (type(name) == types.StringType)
for i in range(len(self.tagStack)-1, 0, -1):
thisTag = self.tagStack[i].name
if (oneTag and thisTag == name) \
or (not oneTag and thisTag in name):
numPops = len(self.tagStack)-i
break
if not oneTag:
numPops = numPops - 1
closedTagName = None
if closedTag:
closedTagName = name
for i in range(0, numPops):
mostRecentTag = self.popTag(closedTagName)
return mostRecentTag
def unknown_starttag(self, name, attrs):
if self.quoteStack:
#This is not a real tag.
#print "<%s> is not real!" % name
attrs = map(lambda(x, y): '%s="%s"' % (x, y), attrs)
self.handle_data('<%s %s>' % (name, attrs))
return
self.endData()
tag = Tag(name, attrs, self.currentTag, self.previous)
if self.previous:
self.previous.next = tag
self.previous = tag
if not name in self.SELF_CLOSING_TAGS:
if name in self.NESTABLE_TAGS:
self._popToTag(self.NESTABLE_TAGS)
else:
self._popToTag(name)
self.pushTag(tag)
if name in self.SELF_CLOSING_TAGS:
self.popTag()
if name in self.QUOTE_TAGS:
#print "Beginning quote (%s)" % name
self.quoteStack.append(name)
def unknown_endtag(self, name):
if self.quoteStack and self.quoteStack[-1] != name:
#This is not a real end tag.
#print "</%s> is not real!" % name
self.handle_data('</%s>' % name)
return
self.endData()
self._popToTag(name, 1)
if self.quoteStack and self.quoteStack[-1] == name:
#print "That's the end of %s!" % self.quoteStack[-1]
self.quoteStack.pop()
def handle_data(self, data):
self.currentData = self.currentData + data
def handle_comment(self, text):
"Propagate comments right through."
self.handle_data("<!--%s-->" % text)
def handle_charref(self, ref):
"Propagate char refs right through."
self.handle_data('&#%s;' % ref)
def handle_entityref(self, ref):
"Propagate entity refs right through."
self.handle_data('&%s;' % ref)
def handle_decl(self, data):
"Propagate DOCTYPEs right through."
self.handle_data('<!%s>' % data)
class BeautifulStoneSoup(BeautifulSoup):
"""A version of BeautifulSoup that doesn't know anything at all
about what HTML tags have special behavior. Useful for parsing
things that aren't HTML, or when BeautifulSoup makes an assumption
counter to what you were expecting."""
IMPLICITLY_CLOSE_TAGS = 0
SELF_CLOSING_TAGS = []
NESTABLE_TAGS = []
QUOTE_TAGS = []

File diff suppressed because it is too large Load Diff

View File

View File

@ -1,301 +0,0 @@
"""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/xml3?v=2.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')

View File

@ -1,294 +0,0 @@
# -*- 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:

View File

@ -1,494 +0,0 @@
# -*- 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()

View File

@ -1,175 +0,0 @@
# 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=\"([^\"]*)\">'),
re.compile(r'div style=padding:10px;>([^<]+)</div'),
]
__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()
try:
begin = html.index('<!-- Target text (content) -->')
end = html.index('<!-- end: Target text (content) -->')
html = html[begin:end]
except ValueError:
pass
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 }
results = []
if callback:
def_callback = callback
else:
def_callback = results.append
def_callback(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
def_callback(phrase)
next = flip[next]
# next is set to the language of the last entry. this should be the same
# as the language we are translating to
if next != through_language:
phrase = translate(phrase, next, flip[next])
def_callback(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)

File diff suppressed because it is too large Load Diff

View File

@ -1,323 +0,0 @@
# 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
# Retrieved from: http://gopher.quux.org:80/devel
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."""
quotechars = "'\""
while len(s) and s[0] in quotechars:
s = s[1:]
while len(s) and s[-1] in quotechars:
s = s[0:-1]
return s
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"' % 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):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
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
m = re.search('<(.*)> (<.*>)$', string)
assert m is not None
capstr, msgid = m.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
m = re.search('^"(.+)" (\S+)', text)
assert m is not None
resultword, resultdb = m.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

View File

@ -1,433 +0,0 @@
"""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:

File diff suppressed because it is too large Load Diff

View File

@ -1,730 +0,0 @@
#!/usr/bin/python
"""Ultra-liberal feed parser
Visit http://diveintomark.org/projects/feed_parser/ for the latest version
Handles RSS 0.9x, RSS 1.0, RSS 2.0, Pie/Atom/Echo feeds
RSS 0.9x/common elements:
- title, link, guid, description, webMaster, managingEditor, language
copyright, lastBuildDate, pubDate
Additional RSS 1.0/2.0 elements:
- dc:rights, dc:language, dc:creator, dc:date, dc:subject,
content:encoded, admin:generatorAgent, admin:errorReportsTo,
Addition Pie/Atom/Echo elements:
- subtitle, created, issued, modified, summary, id, content
Things it handles that choke other parsers:
- bastard combinations of RSS 0.9x and RSS 1.0
- illegal XML characters
- naked and/or invalid HTML in description
- content:encoded in item element
- guid in item element
- fullitem in item element
- non-standard namespaces
- inline XML in content (Pie/Atom/Echo)
- multiple content items per entry (Pie/Atom/Echo)
Requires Python 2.2 or later
"""
__version__ = "2.5.3"
__author__ = "Mark Pilgrim <http://diveintomark.org/>"
__copyright__ = "Copyright 2002-3, Mark Pilgrim"
__contributors__ = ["Jason Diamond <http://injektilo.org/>",
"John Beimler <http://john.beimler.org/>"]
__license__ = "Python"
__history__ = """
1.0 - 9/27/2002 - MAP - fixed namespace processing on prefixed RSS 2.0 elements,
added Simon Fell's test suite
1.1 - 9/29/2002 - MAP - fixed infinite loop on incomplete CDATA sections
2.0 - 10/19/2002
JD - use inchannel to watch out for image and textinput elements which can
also contain title, link, and description elements
JD - check for isPermaLink="false" attribute on guid elements
JD - replaced openAnything with open_resource supporting ETag and
If-Modified-Since request headers
JD - parse now accepts etag, modified, agent, and referrer optional
arguments
JD - modified parse to return a dictionary instead of a tuple so that any
etag or modified information can be returned and cached by the caller
2.0.1 - 10/21/2002 - MAP - changed parse() so that if we don't get anything
because of etag/modified, return the old etag/modified to the caller to
indicate why nothing is being returned
2.0.2 - 10/21/2002 - JB - added the inchannel to the if statement, otherwise its
useless. Fixes the problem JD was addressing by adding it.
2.1 - 11/14/2002 - MAP - added gzip support
2.2 - 1/27/2003 - MAP - added attribute support, admin:generatorAgent.
start_admingeneratoragent is an example of how to handle elements with
only attributes, no content.
2.3 - 6/11/2003 - MAP - added USER_AGENT for default (if caller doesn't specify);
also, make sure we send the User-Agent even if urllib2 isn't available.
Match any variation of backend.userland.com/rss namespace.
2.3.1 - 6/12/2003 - MAP - if item has both link and guid, return both as-is.
2.4 - 7/9/2003 - MAP - added preliminary Pie/Atom/Echo support based on Sam Ruby's
snapshot of July 1 <http://www.intertwingly.net/blog/1506.html>; changed
project name
2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree);
removed unnecessary urllib code -- urllib2 should always be available anyway;
return actual url, status, and full HTTP headers (as result['url'],
result['status'], and result['headers']) if parsing a remote feed over HTTP --
this should pass all the HTTP tests at <http://diveintomark.org/tests/client/http/>;
added the latest namespace-of-the-week for RSS 2.0
2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom
User-Agent (otherwise urllib2 sends two, which confuses some servers)
2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for
inline <xhtml:body> and <xhtml:div> as used in some RSS 2.0 feeds
2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or
textInput, and also to return the character encoding (if specified)
"""
try:
import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
timeoutsocket.setDefaultSocketTimeout(10)
except ImportError:
pass
import cgi, re, sgmllib, string, StringIO, gzip, urllib2
sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
USER_AGENT = "UltraLiberalFeedParser/%s +http://diveintomark.org/projects/feed_parser/" % __version__
def decodeEntities(data):
data = data or ''
data = data.replace('&lt;', '<')
data = data.replace('&gt;', '>')
data = data.replace('&quot;', '"')
data = data.replace('&apos;', "'")
data = data.replace('&amp;', '&')
return data
class FeedParser(sgmllib.SGMLParser):
namespaces = {"http://backend.userland.com/rss": "",
"http://blogs.law.harvard.edu/tech/rss": "",
"http://purl.org/rss/1.0/": "",
"http://example.com/newformat#": "",
"http://example.com/necho": "",
"http://purl.org/echo/": "",
"uri/of/echo/namespace#": "",
"http://purl.org/pie/": "",
"http://purl.org/rss/1.0/modules/textinput/": "ti",
"http://purl.org/rss/1.0/modules/company/": "co",
"http://purl.org/rss/1.0/modules/syndication/": "sy",
"http://purl.org/dc/elements/1.1/": "dc",
"http://webns.net/mvcb/": "admin",
"http://www.w3.org/1999/xhtml": "xhtml"}
def reset(self):
self.channel = {}
self.items = []
self.elementstack = []
self.inchannel = 0
self.initem = 0
self.incontent = 0
self.intextinput = 0
self.inimage = 0
self.contentmode = None
self.contenttype = None
self.contentlang = None
self.namespacemap = {}
sgmllib.SGMLParser.reset(self)
def push(self, element, expectingText):
self.elementstack.append([element, expectingText, []])
def pop(self, element):
if not self.elementstack: return
if self.elementstack[-1][0] != element: return
element, expectingText, pieces = self.elementstack.pop()
if not expectingText: return
output = "".join(pieces)
output = decodeEntities(output)
if self.incontent and self.initem:
if not self.items[-1].has_key(element):
self.items[-1][element] = []
self.items[-1][element].append({"language":self.contentlang, "type":self.contenttype, "value":output})
elif self.initem:
self.items[-1][element] = output
elif self.inchannel and (not self.intextinput) and (not self.inimage):
self.channel[element] = output
def _addNamespaces(self, attrs):
for prefix, value in attrs:
if not prefix.startswith("xmlns:"): continue
prefix = prefix[6:]
if prefix.find('backend.userland.com/rss') <> -1:
# match any backend.userland.com namespace
prefix = 'http://backend.userland.com/rss'
if self.namespaces.has_key(value):
self.namespacemap[prefix] = self.namespaces[value]
def _mapToStandardPrefix(self, name):
colonpos = name.find(':')
if colonpos <> -1:
prefix = name[:colonpos]
suffix = name[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
name = prefix + ':' + suffix
return name
def _getAttribute(self, attrs, name):
value = [v for k, v in attrs if self._mapToStandardPrefix(k) == name]
if value:
value = value[0]
else:
value = None
return value
def start_channel(self, attrs):
self.push('channel', 0)
self.inchannel = 1
def end_channel(self):
self.pop('channel')
self.inchannel = 0
def start_image(self, attrs):
self.inimage = 1
def end_image(self):
self.inimage = 0
def start_textinput(self, attrs):
self.intextinput = 1
def end_textinput(self):
self.intextinput = 0
def start_item(self, attrs):
self.items.append({})
self.push('item', 0)
self.initem = 1
def end_item(self):
self.pop('item')
self.initem = 0
def start_dc_language(self, attrs):
self.push('language', 1)
start_language = start_dc_language
def end_dc_language(self):
self.pop('language')
end_language = end_dc_language
def start_dc_creator(self, attrs):
self.push('creator', 1)
start_managingeditor = start_dc_creator
start_webmaster = start_dc_creator
def end_dc_creator(self):
self.pop('creator')
end_managingeditor = end_dc_creator
end_webmaster = end_dc_creator
def start_dc_rights(self, attrs):
self.push('rights', 1)
start_copyright = start_dc_rights
def end_dc_rights(self):
self.pop('rights')
end_copyright = end_dc_rights
def start_dc_date(self, attrs):
self.push('date', 1)
start_lastbuilddate = start_dc_date
start_pubdate = start_dc_date
def end_dc_date(self):
self.pop('date')
end_lastbuilddate = end_dc_date
end_pubdate = end_dc_date
def start_dc_subject(self, attrs):
self.push('category', 1)
def end_dc_subject(self):
self.pop('category')
def start_link(self, attrs):
self.push('link', self.inchannel or self.initem)
def end_link(self):
self.pop('link')
def start_guid(self, attrs):
self.guidislink = ('ispermalink', 'false') not in attrs
self.push('guid', 1)
def end_guid(self):
self.pop('guid')
if self.guidislink:
if not self.items[-1].has_key('link'):
# guid acts as link, but only if "ispermalink" is not present or is "true",
# and only if the item doesn't already have a link element
self.items[-1]['link'] = self.items[-1]['guid']
def start_title(self, attrs):
self.push('title', self.inchannel or self.initem)
def start_description(self, attrs):
self.push('description', self.inchannel or self.initem)
def start_content_encoded(self, attrs):
self.push('content_encoded', 1)
start_fullitem = start_content_encoded
def end_content_encoded(self):
self.pop('content_encoded')
end_fullitem = end_content_encoded
def start_admin_generatoragent(self, attrs):
self.push('generator', 1)
value = self._getAttribute(attrs, 'rdf:resource')
if value:
self.elementstack[-1][2].append(value)
self.pop('generator')
def start_feed(self, attrs):
self.inchannel = 1
def end_feed(self):
self.inchannel = 0
def start_entry(self, attrs):
self.items.append({})
self.push('item', 0)
self.initem = 1
def end_entry(self):
self.pop('item')
self.initem = 0
def start_subtitle(self, attrs):
self.push('subtitle', 1)
def end_subtitle(self):
self.pop('subtitle')
def start_summary(self, attrs):
self.push('summary', 1)
def end_summary(self):
self.pop('summary')
def start_modified(self, attrs):
self.push('modified', 1)
def end_modified(self):
self.pop('modified')
def start_created(self, attrs):
self.push('created', 1)
def end_created(self):
self.pop('created')
def start_issued(self, attrs):
self.push('issued', 1)
def end_issued(self):
self.pop('issued')
def start_id(self, attrs):
self.push('id', 1)
def end_id(self):
self.pop('id')
def start_content(self, attrs):
self.incontent = 1
if ('mode', 'escaped') in attrs:
self.contentmode = 'escaped'
elif ('mode', 'base64') in attrs:
self.contentmode = 'base64'
else:
self.contentmode = 'xml'
mimetype = [v for k, v in attrs if k=='type']
if mimetype:
self.contenttype = mimetype[0]
xmllang = [v for k, v in attrs if k=='xml:lang']
if xmllang:
self.contentlang = xmllang[0]
self.push('content', 1)
def end_content(self):
self.pop('content')
self.incontent = 0
self.contentmode = None
self.contenttype = None
self.contentlang = None
def start_body(self, attrs):
self.incontent = 1
self.contentmode = 'xml'
self.contenttype = 'application/xhtml+xml'
xmllang = [v for k, v in attrs if k=='xml:lang']
if xmllang:
self.contentlang = xmllang[0]
self.push('content', 1)
start_div = start_body
start_xhtml_body = start_body
start_xhtml_div = start_body
end_body = end_content
end_div = end_content
end_xhtml_body = end_content
end_xhtml_div = end_content
def unknown_starttag(self, tag, attrs):
if self.incontent and self.contentmode == 'xml':
self.handle_data("<%s%s>" % (tag, "".join([' %s="%s"' % t for t in attrs])))
return
self._addNamespaces(attrs)
colonpos = tag.find(':')
if colonpos <> -1:
prefix = tag[:colonpos]
suffix = tag[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
if prefix:
prefix = prefix + '_'
methodname = 'start_' + prefix + suffix
try:
method = getattr(self, methodname)
return method(attrs)
except AttributeError:
return self.push(prefix + suffix, 0)
return self.push(tag, 0)
def unknown_endtag(self, tag):
if self.incontent and self.contentmode == 'xml':
self.handle_data("</%s>" % tag)
return
colonpos = tag.find(':')
if colonpos <> -1:
prefix = tag[:colonpos]
suffix = tag[colonpos+1:]
prefix = self.namespacemap.get(prefix, prefix)
if prefix:
prefix = prefix + '_'
methodname = 'end_' + prefix + suffix
try:
method = getattr(self, methodname)
return method()
except AttributeError:
return self.pop(prefix + suffix)
return self.pop(tag)
def handle_charref(self, ref):
# called for each character reference, e.g. for "&#160;", ref will be "160"
# Reconstruct the original character reference.
if not self.elementstack: return
text = "&#%s;" % ref
if self.incontent and self.contentmode == 'xml':
text = cgi.escape(text)
self.elementstack[-1][2].append(text)
def handle_entityref(self, ref):
# called for each entity reference, e.g. for "&copy;", ref will be "copy"
# Reconstruct the original entity reference.
if not self.elementstack: return
text = "&%s;" % ref
if self.incontent and self.contentmode == 'xml':
text = cgi.escape(text)
self.elementstack[-1][2].append(text)
def handle_data(self, text):
# called for each block of plain text, i.e. outside of any tag and
# not containing any character or entity references
if not self.elementstack: return
if self.incontent and self.contentmode == 'xml':
text = cgi.escape(text)
self.elementstack[-1][2].append(text)
def handle_comment(self, text):
# called for each comment, e.g. <!-- insert message here -->
pass
def handle_pi(self, text):
# called for each processing instruction, e.g. <?instruction>
pass
def handle_decl(self, text):
# called for the DOCTYPE, if present, e.g.
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
# "http://www.w3.org/TR/html4/loose.dtd">
pass
_new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
def _scan_name(self, i, declstartpos):
rawdata = self.rawdata
n = len(rawdata)
if i == n:
return None, -1
m = self._new_declname_match(rawdata, i)
if m:
s = m.group()
name = s.strip()
if (i + len(s)) == n:
return None, -1 # end of buffer
return string.lower(name), m.end()
else:
self.updatepos(declstartpos, i)
self.error("expected name token")
def parse_declaration(self, i):
# override internal declaration handler to handle CDATA blocks
if self.rawdata[i:i+9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1: k = len(self.rawdata)
self.handle_data(cgi.escape(self.rawdata[i+9:k]))
return k+3
return sgmllib.SGMLParser.parse_declaration(self, i)
class FeedURLHandler(urllib2.HTTPRedirectHandler, urllib2.HTTPDefaultErrorHandler):
def http_error_default(self, req, fp, code, msg, headers):
if ((code / 100) == 3) and (code != 304):
return self.http_error_302(req, fp, code, msg, headers)
from urllib import addinfourl
infourl = addinfourl(fp, headers, req.get_full_url())
infourl.status = code
return infourl
# raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
def http_error_302(self, req, fp, code, msg, headers):
infourl = urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
infourl.status = code
return infourl
def http_error_301(self, req, fp, code, msg, headers):
infourl = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers)
infourl.status = code
return infourl
http_error_300 = http_error_302
http_error_307 = http_error_302
def open_resource(source, etag=None, modified=None, agent=None, referrer=None):
"""
URI, filename, or string --> stream
This function lets you define parsers that take any input source
(URL, pathname to local or network file, or actual data as a string)
and deal with it in a uniform manner. Returned object is guaranteed
to have all the basic stdio read methods (read, readline, readlines).
Just .close() the object when you're done with it.
If the etag argument is supplied, it will be used as the value of an
If-None-Match request header.
If the modified argument is supplied, it must be a tuple of 9 integers
as returned by gmtime() in the standard Python time module. This MUST
be in GMT (Greenwich Mean Time). The formatted date/time will be used
as the value of an If-Modified-Since request header.
If the agent argument is supplied, it will be used as the value of a
User-Agent request header.
If the referrer argument is supplied, it will be used as the value of a
Referer[sic] request header.
"""
if hasattr(source, "read"):
return source
## Not appropriate. I wish this module advertised that it did this.
## Hah! Incidentally, even if it were appropriate, Mr. Pilgrim forgot to
## import sys. Double suckage!
## 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
# jemfinch: yeah, right. Like we're going to allow that. Eat
# this, Mark Pilgrim!
pass
## # Dumb dumb dumb!
## # try to open with native open function (if source is a filename)
## try:
## return open(source)
## except:
## pass
# This just feels icky to me, I'm not allowing it.
# treat source as string
## return StringIO.StringIO(str(source))
return StringIO.StringIO('')
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
"""

View File

@ -1,218 +0,0 @@
"""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

View File

@ -1,785 +0,0 @@
#!/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)

File diff suppressed because it is too large Load Diff

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"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"
}

View File

@ -1,312 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Allows aliases for other commands.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jemfinch
import supybot.plugins as plugins
import os
import re
import sets
import supybot.conf as conf
import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
import supybot.structures as structures
import supybot.unpreserve as 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')
d = Owner.disambiguate(irc, tokens)
if d:
Owner.ambiguousError(irc, msg, d)
else:
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 '|' 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]"
arguments to the alias; they'll be filled with the first, second, etc.
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)
if ' ' not in alias:
# If it's a single word, they probably want $*.
alias += ' $*'
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:

View File

@ -1,600 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Amazon module, to use Amazon's Web Services.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jamessan
import getopt
import supybot.plugins as plugins
import amazon
import supybot.registry as registry
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.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:
super(Amazon, self).callCommand(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
if isinstance(s, unicode):
s = 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 categories. 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:

View File

@ -1,120 +0,0 @@
#!/usr/bin/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.
###
"""
Allows folks to talk through the bot anonymously.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
import supybot.ircmsgs as ircmsgs
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as 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 supybot.questions import expect, anything, something, yn
conf.registerPlugin('Anonymous', True)
conf.registerPlugin('Anonymous')
conf.registerChannelValue(conf.supybot.plugins.Anonymous,
'requirePresenceInChannel', registry.Boolean(True, """Determines whether
the bot should require people trying to use this plugin to be in the
channel they wish to anonymously send to."""))
conf.registerGlobalValue(conf.supybot.plugins.Anonymous, 'requireRegistration',
registry.Boolean(True, """Determines whether the bot should require people
trying to use this plugin to be registered."""))
conf.registerGlobalValue(conf.supybot.plugins.Anonymous, 'requireCapability',
registry.String('', """Determines what capability (if any) the bot should
require people trying to use this plugin to have."""))
class Anonymous(callbacks.Privmsg):
private = True
def _preCheck(self, irc, msg, channel):
if self.registryValue('requireRegistration'):
try:
_ = ircdb.users.getUser(msg.prefix)
except KeyError:
irc.errorNotRegistered(Raise=True)
if channel not in irc.state.channels:
irc.error('I\'m not in %s, chances are that I can\'t say anything '
'in there.' % channel, Raise=True)
capability = self.registryValue('requireCapability')
if capability:
if not ircdb.checkCapability(msg.prefix, capability):
irc.errorNoCapability(capability, Raise=True)
if self.registryValue('requirePresenceInChannel', channel) and \
msg.nick not in irc.state.channels[channel].users:
irc.error('You must be in %s to "say" in there.' % channel,
Raise=True)
c = ircdb.channels.getChannel(channel)
if c.lobotomized:
irc.error('I\'m lobotomized in %s.' % channel, Raise=True)
if not c.checkCapability(self.__class__.__name__):
irc.error('That channel has set its capabilities so as to '
'disallow the use of this plugin.', Raise=True)
def say(self, irc, msg, args):
"""<channel> <text>
Sends <text> to <channel>.
"""
(channel, text) = privmsgs.getArgs(args, required=2)
self._preCheck(irc, msg, channel)
self.log.info('Saying %r in %s due to %s.', text, channel, msg.prefix)
irc.queueMsg(ircmsgs.privmsg(channel, text))
def action(self, irc, msg, args):
"""<channel> <action>
Performs <action> in <channel>.
"""
(channel, action) = privmsgs.getArgs(args, required=2)
self._preCheck(irc, msg, channel)
self.log.info('Performing %r in %s due to %s.',
action, channel, msg.prefix)
irc.queueMsg(ircmsgs.action(channel, action))
Class = Anonymous
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,184 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Babelfish-related commands.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jamessan
import sets
import random
from itertools import imap
import babelfish
import supybot.conf as conf
import supybot.utils as utils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
class Languages(registry.OnlySomeStrings):
validStrings = tuple(map(str.capitalize, babelfish.available_languages))
normalize = staticmethod(str.capitalize)
class SpaceSeparatedListOfLanguages(registry.SeparatedListOf):
List = sets.Set
Value = Languages
def splitter(self, s):
return s.split()
joiner = ' '.join
conf.registerPlugin('Babelfish')
conf.registerChannelValue(conf.supybot.plugins.Babelfish, 'languages',
SpaceSeparatedListOfLanguages(babelfish.available_languages, """Determines
which languages are available for translation; valid input is a list of
languages separated by spaces."""))
class Babelfish(callbacks.Privmsg):
threaded = True
_abbrevs = utils.abbrev(imap(str.lower, babelfish.available_languages))
_abbrevs['de'] = 'german'
_abbrevs['jp'] = 'japanese'
_abbrevs['kr'] = 'korean'
_abbrevs['es'] = 'spanish'
_abbrevs['pt'] = 'portuguese'
_abbrevs['it'] = 'italian'
_abbrevs['zh'] = 'chinese'
for language in babelfish.available_languages:
_abbrevs[language] = language
def _getLang(self, fromLang, toLang, chan):
fromLang = self._abbrevs[fromLang.lower()]
toLang = self._abbrevs[toLang.lower()]
languages = map(str.lower, self.registryValue('languages',chan))
if fromLang not in languages:
fromLang = None
if toLang not in languages:
toLang = None
return (fromLang, toLang)
def languages(self, irc, msg, args):
"""takes no arguments
Returns the languages that Babelfish can translate to/from.
"""
irc.reply(utils.commaAndify(babelfish.available_languages))
def translate(self, irc, msg, args):
"""<from-language> [to] <to-language> <text>
Returns <text> translated from <from-language> into <to-language>.
"""
if len(args) >= 2 and args[1] == 'to':
args.pop(1)
(fromLang, toLang, text) = privmsgs.getArgs(args, required=3)
chan = msg.args[0]
try:
(fromLang, toLang) = self._getLang(fromLang, toLang, chan)
if not fromLang or not toLang:
langs = self.registryValue('languages', chan)
if not langs:
irc.error('I do not speak any other languages.')
return
else:
irc.error('I only speak %s.' % utils.commaAndify(langs,
And='or'))
return
translation = babelfish.translate(text, fromLang, toLang)
irc.reply(translation)
except (KeyError, babelfish.LanguageNotAvailableError), e:
irc.error('%s is not a valid language. Valid languages '
'include %s.' %
(e, self.registryValue('languages', chan)))
except babelfish.BabelizerIOError, e:
irc.error(str(e))
except babelfish.BabelfishChangedError, e:
irc.error('Babelfish has foiled our plans by changing its '
'webpage format.')
def babelize(self, irc, msg, args):
"""<from-language> <to-language> <text>
Translates <text> repeatedly between <from-language> and <to-language>
until it doesn't change anymore or 12 times, whichever is fewer. One
of the languages must be English.
"""
(fromLang, toLang, text) = privmsgs.getArgs(args, required=3)
chan = msg.args[0]
try:
(fromLang, toLang) = self._getLang(fromLang, toLang, chan)
if fromLang != 'english' and toLang != 'english':
irc.error('One language in babelize must be English.')
return
if not fromLang or not toLang:
langs = self.registryValue('languages', chan)
if not langs:
irc.error('I do not speak any other languages.')
return
else:
irc.error('I only speak %s.' % utils.commaAndify(langs,
And='or'))
return
translations = babelfish.babelize(text, fromLang, toLang)
irc.reply(utils.htmlToText(translations[-1]))
except (KeyError, babelfish.LanguageNotAvailableError), e:
irc.reply('%s is not a valid language. Valid languages '
'include %s.' % (e,
self.registryValue('languages', chan)))
except babelfish.BabelizerIOError, e:
irc.reply(e)
except babelfish.BabelfishChangedError, e:
irc.reply('Babelfish has foiled our plans by changing its '
'webpage format.')
def randomlanguage(self, irc, msg, args):
"""[<allow-english>]
Returns a random language supported by babelfish. If <allow-english>
is provided, will include English in the list of possible languages.
"""
allowEnglish = privmsgs.getArgs(args, required=0, optional=1)
languages = self.registryValue('languages', msg.args[0])
if not languages:
irc.error('I can\'t speak any other languages.')
language = random.choice(languages)
while not allowEnglish and language == 'English':
language = random.choice(languages)
irc.reply(language)
Class = Babelfish
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,183 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Filters bad words on outgoing messages from the bot, so the bot can't be made
to say bad words.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import re
import math
import sets
import time
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.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 __call__(self):
s = registry.String.__call__(self)
return s * (1024/len(s))
def __str__(self):
return self.value
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.filtering = True
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 inFilter(self, irc, msg):
self.filtering = True
return msg
def outFilter(self, irc, msg):
if self.filtering and msg.command == 'PRIVMSG':
if self.lastModified < self.words.lastModified:
self.makeRegexp(self.words())
self.lastModified = time.time()
s = msg.args[1]
s = ircutils.stripFormatting(s)
s = self.regexp.sub(self.sub, s)
msg = ircmsgs.privmsg(msg.args[0], s)
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 list(self, irc, msg, args):
"""takes no arguments
Returns the list of words being censored.
"""
L = list(self.words())
if L:
self.filtering = False
utils.sortBy(str.lower, L)
irc.reply(utils.commaAndify(L))
else:
irc.reply('I\'m not currently censoring any bad words.')
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:

View File

@ -1,464 +0,0 @@
#!/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 urllib
import xml.dom.minidom as minidom
from itertools import imap, ifilter
from htmlentitydefs import entitydefs as entities
import supybot.conf as conf
import supybot.utils as utils
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.webutils as webutils
import supybot.callbacks as callbacks
import supybot.structures as 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']
class BugzillaError(Exception):
"""A bugzilla error"""
pass
def configure(advanced):
from supybot.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
a Supybot sees such a URL, it 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 and bug ### 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."""))
conf.registerChannelValue(conf.supybot.plugins.Bugzilla, 'snarfTarget',
registry.String('', """Determines the bugzilla site to query when the
snarf command is triggered"""))
class Bugzillae(registry.SpaceSeparatedListOfStrings):
List = ircutils.IrcSet
conf.registerGlobalValue(conf.supybot.plugins.Bugzilla, 'bugzillae',
Bugzillae([], """Determines what bugzillae will be added to the bot when it
starts."""))
def registerBugzilla(name, url='', description=''):
conf.supybot.plugins.Bugzilla.bugzillae().add(name)
group = conf.registerGroup(conf.supybot.plugins.Bugzilla.bugzillae, name)
URL = conf.registerGlobalValue(group, 'url', registry.String(url, ''))
DESC = conf.registerGlobalValue(group, 'description',
registry.String(description, ''))
if url:
URL.setValue(url)
if description:
DESC.setValue(description)
class Bugzilla(callbacks.PrivmsgCommandAndRegexp):
"""Show a link to a bug report with a brief description"""
threaded = True
regexps = ['bzSnarfer', 'bugzSnarf']
def __init__(self):
callbacks.PrivmsgCommandAndRegexp.__init__(self)
self.entre = re.compile('&(\S*?);')
# Schema: {name, [url, description]}
self.db = ircutils.IrcDict()
for name in self.registryValue('bugzillae'):
registerBugzilla(name)
group = self.registryValue('bugzillae.%s' % name, value=False)
self.db[name] = [group.url(), group.description()]
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 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 bugzilla 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; if not given, it defaults to <name>.
"""
(name, url, description) = privmsgs.getArgs(args,required=2,optional=1)
if not description:
description = name
if url[-1] == '/':
url = url[:-1]
self.db[name] = [url, description]
registerBugzilla(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.registryValue('bugzillae').remove(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 bugzSnarf(self, irc, msg, match):
r"""\bbug\b(?:id|ids|#)?\s+(?:id|ids|#)?(?P<bug>\d+)"""
snarfTarget = self.registryValue('snarfTarget')
if not snarfTarget:
return
bugid = match.group('bug')
name = self.shorthand[snarfTarget]
try:
(url, description) = self.db[name]
except KeyError:
s = self.registryValue('replyNoBugzilla', name)
irc.error(s % name)
return
if not self.registryValue('bugSnarfer', name):
return
queryurl = '%s/xml.cgi?id=%s' % (url, bugid)
bold = self.registryValue('bold', name)
try:
summary = self._get_short_bug_summary(queryurl,description,bugid)
except BugzillaError, e:
irc.error(str(e))
return
except IOError, e:
s = '%s. Try yourself: %s' % (e, queryurl)
irc.error(s)
report = {}
report['id'] = bugid
report['url'] = str('%s/show_bug.cgi?id=%s' % (url, bugid))
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)
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
"""
bugs = {}
try:
url = '%s/buglist.cgi?%s' % (url, '&'.join(query))
u = webutils.getUrlFd(url)
except webutils.WebError, e:
return bugs
# actually read in the file
csvreader = csv.reader(u)
# read header
fields = csvreader.next()
# read the rest of the list
for bug in csvreader:
if isinstance(bug, basestring):
bugid = bug
else:
if bug:
bugid = bug[0]
else:
raise callbacks.Error, 'No bugs found.'
try:
bugid = int(bugid)
except ValueError:
pass
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, separated 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()
if not bugs:
irc.error('I could not find any bugs.')
return
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:
bugxml = webutils.getUrl(url)
except webutils.WebError, e:
raise IOError, 'Connection to %s bugzilla failed' % desc
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:

View File

@ -1,279 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Logs each channel to its own individual logfile.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jemfinch
import supybot.plugins as plugins
import time
from cStringIO import StringIO
import os
import supybot.conf as conf
import supybot.world as world
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.registry as registry
import supybot.callbacks as 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,
'stripFormatting', registry.Boolean(True, """Determines whether
formatting characters (such as bolding, color, etc.) are removed when
writing the logs to disk."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger, 'timestamp',
registry.Boolean(True, """Determines whether the logs for this channel are
timestamped with the timestamp in supybot.log.timestampFormat."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger, 'noLogPrefix',
registry.String('[nolog]', """Determines what string a message should be
prefixed with in order not to be logged. If you don't want any such
prefix, just set it to the empty string."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger,
'includeNetworkName', registry.Boolean(True, """Determines whether the bot
will include the name of the network in the filename for channel logs.
Since this is a channel-specific value, you can override for any channel.
You almost certainly want this to be True if you're relaying in a given
channel."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger, 'rotateLogs',
registry.Boolean(False, """Determines whether the bot will automatically
rotate the logs for this channel. The bot will rotate logs when the
timestamp for the log changes. The timestamp is set according to
the 'filenameTimestamp' configuration variable."""))
conf.registerChannelValue(conf.supybot.plugins.ChannelLogger,
'filenameTimestamp', registry.String('%d-%a-%Y', """Determines how to
represent the timestamp used for the filename in rotated logs. When this
timestamp changes, the old logfiles will be closed and a new one started.
The format characters for the timestamp are in the time.strftime docs at
python.org. In order for your logs to be rotated, you'll also have to
enable supybot.plugins.ChannelLogger.rotateLogs."""))
class FakeLog(object):
def flush(self):
return
def close(self):
return
def write(self, s):
return
class ChannelLogger(callbacks.Privmsg):
noIgnore = True
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 registryValue(self, name, channel=None, **kwargs):
if channel is not None:
# This handles the possible #channel@network channels we might be
# getting. It's a hack, because we should know what we're doing,
# but apparently we don't.
channel = channel.split('@')[0]
return callbacks.Privmsg.registryValue(self, name, channel, **kwargs)
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()
logDir = os.path.join(logDir, self.name())
if not os.path.exists(logDir):
os.makedirs(logDir)
name = self.getLogName(channel)
log = file(os.path.join(logDir, name), 'a')
self.logs[channel] = log
return log
except IOError:
self.log.exception('Error opening log:')
return FakeLog()
def timestamp(self, log):
format = conf.supybot.log.timestampFormat()
if format:
log.write(time.strftime(format))
log.write(' ')
def normalizeChannel(self, irc, channel):
if self.registryValue('includeNetworkName', channel):
channel = '%s@%s' % (channel, irc.network)
return ircutils.toLower(channel)
def doLog(self, irc, channel, s):
channel = self.normalizeChannel(irc, channel)
log = self.getLog(channel)
if self.registryValue('timestamp', channel):
self.timestamp(log)
if self.registryValue('stripFormatting', channel):
s = ircutils.stripFormatting(s)
log.write(s)
if self.registryValue('flushImmediately'):
log.flush()
def doPrivmsg(self, irc, msg):
(recipients, text) = msg.args
for channel in recipients.split(','):
if ircutils.isChannel(channel):
noLogPrefix = self.registryValue('noLogPrefix', channel)
if noLogPrefix and text.startswith(noLogPrefix):
text = '-= THIS MESSAGE NOT LOGGED =-'
nick = msg.nick or irc.nick
if ircmsgs.isAction(msg):
self.doLog(irc, channel,
'* %s %s\n' % (nick, ircmsgs.unAction(msg)))
else:
self.doLog(irc, channel, '<%s> %s\n' % (nick, text))
def doNotice(self, irc, msg):
(recipients, text) = msg.args
for channel in recipients.split(','):
if ircutils.isChannel(channel):
self.doLog(irc, channel, '-%s- %s\n' % (msg.nick, text))
def doJoin(self, irc, msg):
for channel in msg.args[0].split(','):
self.doLog(irc, channel,
'*** %s has joined %s\n' %
(msg.nick or msg.prefix, channel))
def doKick(self, irc, msg):
if len(msg.args) == 3:
(channel, target, kickmsg) = msg.args
else:
(channel, target) = msg.args
kickmsg = ''
if kickmsg:
self.doLog(irc, channel,
'*** %s was kicked by %s (%s)\n' %
(target, msg.nick, kickmsg))
else:
self.doLog(irc, channel,
'*** %s was kicked by %s\n' % (target, msg.nick))
def doPart(self, irc, msg):
for channel in msg.args[0].split(','):
self.doLog(irc, channel,
'*** %s has left %s\n' % (msg.nick, channel))
def doMode(self, irc, msg):
channel = msg.args[0]
if ircutils.isChannel(channel) and msg.args[1:]:
self.doLog(irc, channel,
'*** %s sets mode: %s %s\n' %
(msg.nick or msg.prefix, msg.args[1],
' '.join(msg.args[2:])))
def doTopic(self, irc, msg):
if len(msg.args) == 1:
return # It's an empty TOPIC just to get the current topic.
channel = msg.args[0]
self.doLog(irc, channel,
'*** %s changes topic to "%s"\n' % (msg.nick, msg.args[1]))
def doQuit(self, irc, msg):
for (channel, chan) in self.laststate.channels.iteritems():
if msg.nick in chan.users:
self.doLog(irc, channel, '*** %s has quit IRC\n' % msg.nick)
def outFilter(self, irc, msg):
# Gotta catch my own messages *somehow* :)
# Let's try this little trick...
if msg.command != 'PART':
m = ircmsgs.IrcMsg(msg=msg, prefix=irc.prefix)
self(irc, m)
return msg
Class = ChannelLogger
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,129 +0,0 @@
#!/usr/bin/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.
###
"""
This plugin is useful for relaying messages from one channel on a network to
another channel on the same network. If you're interested in relaying messages
between channels on different networks, check out the Relay plugin.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jemfinch
import supybot.plugins as plugins
import re
import supybot.conf as conf
import supybot.utils as utils
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks
class ValidChannelOrNothing(conf.ValidChannel):
"""Value must be either a valid IRC channel or the empty string."""
def setValue(self, v):
try:
conf.ValidChannel.setValue(self, v)
except registry.InvalidRegistryValue:
registry.Value.setValue(self, '')
conf.registerPlugin('ChannelRelay')
conf.registerGlobalValue(conf.supybot.plugins.ChannelRelay, 'source',
ValidChannelOrNothing('', """Determines the channel that the bot will look
for messages to relay from. Messages matching
supybot.plugins.ChannelRelay.regexp will be relayed to the target channel
specified by supybot.plugins.ChannelRelay.target."""))
conf.registerGlobalValue(conf.supybot.plugins.ChannelRelay, 'target',
ValidChannelOrNothing('', """Determines the channel that the bot will send
messages from the other channel. Messages matching
supybot.plugins.ChannelRelay.regexp will be relayed to this channel from
the source channel."""))
conf.registerGlobalValue(conf.supybot.plugins.ChannelRelay, 'regexp',
registry.Regexp(None, """Determines what regular expression
should be matched against messages to determine whether they should be
relayed from the source channel to the target channel. By default, the
value is m/./, which means that all non-empty messages will be
relayed."""))
if conf.supybot.plugins.ChannelRelay.regexp() is None:
conf.supybot.plugins.ChannelRelay.regexp.set('m/./')
conf.registerGlobalValue(conf.supybot.plugins.ChannelRelay, 'fancy',
registry.Boolean(True, """Determines whether the bot should relay the
messages in fancy form (i.e., including the nick of the sender of the
messages) or non-fancy form (i.e., without the nick of the sender of the
messages)."""))
conf.registerGlobalValue(conf.supybot.plugins.ChannelRelay, 'prefix',
registry.String('', """Determines what prefix should be prepended to the
relayed messages."""))
def configure(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 supybot.questions import expect, anything, something, yn, output
conf.registerPlugin('ChannelRelay', True)
class ChannelRelay(callbacks.Privmsg):
def shouldRelay(self, msg):
source = self.registryValue('source')
if source:
assert msg.command == 'PRIVMSG'
return msg.args[0] == source and \
bool(self.registryValue('regexp').search(msg.args[1]))
else:
return False
def doPrivmsg(self, irc, msg):
if self.shouldRelay(msg):
target = self.registryValue('target')
if target and target in irc.state.channels:
if self.registryValue('fancy'):
s = ircmsgs.prettyPrint(msg)
else:
s = msg.args[1]
s = self.registryValue('prefix') + s
irc.queueMsg(ircmsgs.privmsg(target, s))
def do376(self, irc, msg):
source = self.registryValue('source')
target = self.registryValue('target')
if source and target:
irc.queueMsg(ircmsgs.joins([source, target]))
Class = ChannelRelay
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,350 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
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 supybot.plugins as plugins
import os
import re
import sets
import time
import getopt
import string
from itertools import imap, ifilter
import supybot.log as log
import supybot.conf as conf
import supybot.utils as utils
import supybot.world as world
import supybot.ircdb as ircdb
import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as 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(utils.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 ircutils.strEqual(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:

View File

@ -1,89 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Handles standard CTCP responses to PING, TIME, SOURCE, VERSION, USERINFO,
and FINGER.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import os
import sys
import time
sys.path.append(os.pardir)
import supybot.conf as conf
import supybot.ircmsgs as ircmsgs
import supybot.callbacks as 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:

View File

@ -1,171 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2004, 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.
###
"""
Provides commands which interface with various websites to perform currency
conversions.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jamessan
import re
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.webutils as webutils
import supybot.callbacks as callbacks
class CurrencyCommand(registry.String):
def setValue(self, s):
m = Currency.currencyCommands
if s not in m:
raise registry.InvalidRegistryValue,\
'Command must be one of %s.' % utils.commaAndify(m)
else:
method = getattr(Currency, s)
Currency.convert.im_func.__doc__ = method.__doc__
registry.String.setValue(self, s)
class Currency(callbacks.Privmsg):
currencyCommands = ['xe', 'yahoo']
threaded = True
_symbolError = 'Currency must be denoted by its three-letter symbol.'
def convert(self, irc, msg, args):
# This specifically does not have a docstring.
channel = None
if ircutils.isChannel(msg.args[0]):
channel = msg.args[0]
realCommandName = self.registryValue('command', channel)
realCommand = getattr(self, realCommandName)
realCommand(irc, msg, args)
_xeCurrError = re.compile(r'The following error occurred:<BR><BR>\s+'
r'(.*)</body>', re.I | re.S)
_xeConvert = re.compile(r'<TD[^>]+><FONT[^>]+>\s+([\d.]+\s+\w{3}\s+='
r'\s+[\d.]+\s+\w{3})', re.I | re.S)
def xe(self, irc, msg, args):
"""[<number>] <currency1> to <currency2>
Converts from <currency1> to <currency2>. If number isn't given, it
defaults to 1.
"""
(number, curr1, curr2) = privmsgs.getArgs(args, required=2,
optional=1)
try:
number = float(number)
except ValueError:
curr2 = curr1
curr1 = number
number = 1
curr1 = curr1.lower()
curr2 = curr2.lower()
if curr2.startswith('to '):
curr2 = curr2[3:]
if len(curr1) != 3 and len(curr2) != 3:
irc.error(self._symbolError)
return
url = 'http://www.xe.com/ucc/convert.cgi?Amount=%s&From=%s&To=%s'
try:
text = webutils.getUrl(url % (number, curr1, curr2))
except webutils.WebError, e:
irc.error(str(e))
return
err = self._xeCurrError.search(text)
if err is not None:
irc.error('You used an incorrect currency symbol.')
return
conv = self._xeConvert.search(text)
if conv is not None:
resp = conv.group(1).split()
resp[0] = str(float(resp[0]) * number)
if resp[0].endswith('.0'):
resp[0] = '%s.00' % resp[0][:-2]
resp[3] = str(float(resp[3]) * number)
irc.reply(' '.join(resp))
return
else:
irc.error('XE must\'ve changed the format of their site.')
return
def yahoo(self, irc, msg, args):
"""[<number>] <currency1> to <currency2>
Converts from <currency1> to <currency2>. If number isn't given, it
defaults to 1.
"""
(number, curr1, curr2) = privmsgs.getArgs(args, required=2,
optional=1)
try:
number = float(number)
except ValueError:
curr2 = curr1
curr1 = number
number = 1
curr1 = curr1.upper()
curr2 = curr2.upper()
if curr2.startswith('TO '):
curr2 = curr2[3:]
if len(curr1) != 3 and len(curr2) != 3:
irc.error(self._symbolError)
return
url = r'http://finance.yahoo.com/d/quotes.csv?'\
r's=%s%s=X&f=sl1d1t1ba&e=.csv' % (curr1, curr2)
try:
text = webutils.getUrl(url)
except webutils.WebError, e:
irc.error(str(e))
return
if 'N/A' in text:
irc.error('You used an incorrect currency symbol.')
return
conv = text.split(',')[1]
conv = number * float(conv)
resp = [str(number), curr1.upper(), '=', str(conv), curr2.upper()]
if '.' not in resp[0] and 'e' not in resp[0]:
resp[0] = '%s.00' % resp[0]
irc.reply(' '.join(resp))
conf.registerPlugin('Currency')
conf.registerChannelValue(conf.supybot.plugins.Currency, 'command',
CurrencyCommand('yahoo', """Sets the default command to use when retrieving
the currency conversion. Command must be one of %s.""" %
utils.commaAndify(Currency.currencyCommands, And='or')))
Class = Currency
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

View File

@ -1,122 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Supports various DCC things.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import socket
import textwrap
import threading
import supybot.conf as conf
import supybot.utils as utils
import supybot.world as world
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.callbacks as 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:
host = ircutils.hostFromHostmask(irc.prefix)
try:
sock = utils.getSocket(host)
except socket.error, e:
s = 'Error connecting to %s: %s'
self.log.warning(s, host, e)
irc.replyError()
return
sock.settimeout(60)
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)
try:
sock.bind((host, 0))
except socket.error, e:
irc.error('Unable to initiate DCC CHAT.')
return
port = sock.getsockname()[1]
self.log.info('DCC CHAT port opened at (%s, %s)', host, port)
sock.listen(1)
m = ircmsgs.privmsg(msg.nick,
'\x01DCC CHAT chat %s %s\x01' % (i, port))
irc.queueMsg(m)
try:
(realSock, addr) = sock.accept()
except socket.timeout:
self.log.info('DCC CHAT timed out.')
return
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.')
try:
sock.close()
except UnboundLocalError:
pass
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:

View File

@ -1,391 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
This is a module to contain Debian-specific commands.
"""
import supybot
__revision__ = "$Id$"
__author__ = supybot.authors.jamessan
import re
import gzip
import sets
import getopt
import popen2
import socket
import urllib
import fnmatch
import os.path
import BeautifulSoup
from itertools import imap, ifilter
import supybot.conf as conf
import supybot.utils as utils
import supybot.plugins as plugins
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.webutils as webutils
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.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.""")
capabilities = conf.supybot.capabilities()
capabilities.add('-Debian.file')
conf.supybot.capabilities.set(capabilities)
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.')
capabilities = conf.supybot.capabilities()
capabilities.add('-Debian.file')
conf.supybot.capabilities.set(capabilities)
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 = ".*%s.* " % 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', '-ie', 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:
if hasattr(line, 'group'): # we're actually using
line = line.group(0) # pythonZgrep :(
(filename, pkg_list) = line.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
_deblistre = re.compile(r'<h3>Package ([^<]+)</h3>(.*?)</ul>', _debreflags)
_debBranches = ('stable', 'testing', 'unstable', 'experimental')
def version(self, irc, msg, args):
"""[--exact] [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 --exact is
specified, only packages whose name exactly matches <package name>
will be reported.
"""
url = 'http://packages.debian.org/cgi-bin/search_packages.pl?keywords'\
'=%s&searchon=names&version=%s&release=all&subword=1'
if not args:
raise callbacks.ArgumentError
(optlist, rest) = getopt.getopt(args, '', ['exact'])
for (option, _) in optlist:
if option == '--exact':
url = url.replace('&subword=1','')
if rest and rest[0] in self._debBranches:
branch = rest.pop(0)
else:
branch = 'all'
if not rest:
irc.error('You must give a package name.')
return
responses = []
package = privmsgs.getArgs(rest)
if '*' in package:
irc.error('Wildcard characters can not be specified.')
return
package = urllib.quote(package)
url = url % (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]
soup = BeautifulSoup.BeautifulSoup()
soup.feed(pkg[1])
liBranches = soup.fetch('li')
branches = []
versions = []
def branchVers(br):
vers = [b.next.string.strip() for b in br]
return [rsplit(v, ':', 1)[0] for v in vers]
for li in liBranches:
branches.append(li.first('a').string)
versions.append(branchVers(li.fetch('br')))
if branches and versions:
for pairs in zip(branches, versions):
branch = pairs[0]
ver = ', '.join(pairs[1])
s = '%s (%s)' % (pkgMatch, ': '.join([branch, ver]))
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(str(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(str(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:

View File

@ -1,158 +0,0 @@
#!/usr/bin/env python
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Commands that use the dictd protocol to snag stuff off a server.
"""
__revision__ = "$Id$"
import supybot.plugins as plugins
import sets
import random
import socket
import dictclient
import supybot.conf as conf
import supybot.utils as utils
import supybot.plugins as plugins
import supybot.registry as registry
import supybot.ircutils as ircutils
import supybot.privmsgs as privmsgs
import supybot.callbacks as callbacks
def configure(advanced):
from supybot.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)
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."""))
conf.registerChannelValue(conf.supybot.plugins.Dict, 'default',
registry.String('', """Determines the default dictionary the bot will
ask for definitions in. If this value is '*' (without the quotes) the bot
will use all dictionaries to define words."""))
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.error, e:
irc.error(webutils.strError(e))
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.error, e:
irc.error(webutils.strError(e))
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.error, e:
irc.error(webutils.strError(e))
return
dbs = sets.Set(conn.getdbdescs())
if args[0] in dbs:
dictionary = args.pop(0)
else:
default = self.registryValue('default', msg.args[0])
if default in dbs:
dictionary = default
else:
if default:
self.log.info('Default dict for %s is not a supported '
'dictionary: %s.', msg.args[0], default)
dictionary = '*'
word = privmsgs.getArgs(args)
if not word:
irc.error('You must give a word to define.', Raise=True)
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:

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