Compare commits

...

No commits in common. "master-2014-11-24" and "gh-pages" have entirely different histories.

740 changed files with 1516 additions and 128577 deletions

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

4
.gitattributes vendored
View File

@ -1,3 +1 @@
test export-ignore
sandbox export-ignore
.git* export-ignore
* 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

17
.gitignore vendored
View File

@ -24,14 +24,9 @@ test-conf/
test-data/
test-logs/
src/version.py
INSTALL
README.txt
conf/
data/
logs/
*.conf
*.conf.bak
# Intellij PyCharm / IDEA related files
*.iml
.idea
_site
.sass-cache
vendor/
.bundle
node_modules/
pnpm-lock.yaml

View File

@ -1,22 +0,0 @@
<jamessan@users.sourceforge.net> <vega.james@gmail.com>
<jemfinch@users.sourceforge.net> <jemfinch@finchers.us>
<jemfinch@users.sourceforge.net> <jfincher@monoid.(none)>
<mikaela.suomalainen@outlook.com> <Mkaysi@users.noreply.github.com>
<nyuszika7h@openmailbox.org> <liemininyuszika@gmail.com>
<nyuszika7h@openmailbox.org> <litemininyuszika@gmail.com>
<nyuszika7h@openmailbox.org> <nyuszika7h@cadoth.co>
<nyuszika7h@openmailbox.org> <nyuszika7h@cadoth.net>
<nyuszika7h@openmailbox.org> <nyuszika7h@gmail.com>
<nyuszika7h@openmailbox.org> <nyuszika7h@outlook.com>
<progval@progval.net> <progval+github@progval.net>
<progval@progval.net> <progval@gmail.com>
<shadowninja@minetest.net> <ShadowNinja@users.noreply.github.com>
Daniel Folkinshteyn <nanotube@users.sourceforge.net> Daniel F <dfolkins@a90.(none)>
Daniel Folkinshteyn <nanotube@users.sourceforge.net> nanotube <nanotube@users.sf.net>
James McCoy <jamessan@users.sourceforge.net>
Mikaela Suomalainen <mikaela.suomalainen@outlook.com> Mika Suomalainen <mika.henrik.mainio@hotmail.com>
Mikaela Suomalainen <mikaela.suomalainen@outlook.com> Mika Suomalainen <mkaysi@outlook.com>
Mikaela Suomalainen <mikaela.suomalainen@outlook.com> Mika Suomalainen <mkaysi@users.sourceforge.net>
Mikaela Suomalainen <mikaela.suomalainen@outlook.com> Mika Suomalainen <s.mika95@gmail.com>
Mikaela Suomalainen <mikaela.suomalainen@outlook.com> Mikaela Suomalainen <mkaysi@outlook.com>
Tannn3r <tannn3r@gmail.com> <tanman8r@gmail.com>

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

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
# This script does the things that we want Travis to do only once, not in
# every possible build.
# Care about exit status
set -x
set -e
# Set environment
# Which branch are we on?
branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
# Install requirements required for only this file.
if [[ $TRAVIS == "true" ]]; then
sudo pip install sphinx msgcheck --upgrade
else
pip install sphinx msgcheck --upgrade --user
fi
# Check translations
sandbox/check_trans.py plugins/
sandbox/check_trans.py --core
msgcheck -flwW locales/*.po || true
msgcheck -flwW plugins/*/*/*.po || true
# Check documentation
cd docs
# Add -W to spinx-build when the documentation doesn't error!
sphinx-build -n -b html -d _build/doctrees . _build/html
cd ..

View File

@ -1,23 +1,4 @@
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "pypy"
- "pypy3"
# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
install:
- pip install -vr requirements.txt
# command to run tests, e.g. python setup.py test
script:
- echo $TRAVIS_PYTHON_VERSION
- python setup.py install
- supybot-test test --plugins-dir=./build/lib*/supybot/plugins/ --no-network --disable-multiprocessing --exclude=./build/lib*/supybot/plugins/Scheduler --exclude=./build/lib*/supybot/plugins/Filter
after_success:
- if [ "$TRAVIS_PYTHON_VERSION" = "3.4" ]; then bash .travis.runonce.bash;fi
notifications:
email: false
matrix:
fast_finish: true
# @format
language: ruby
script: "bundle exec jekyll build"

View File

View File

@ -1,22 +0,0 @@
"""Fixer for iteritems -> items methods."""
# Author: Valentin Lorentz
# Code modified from fix_nonzero by Collin Winter
from lib2to3 import fixer_base
from lib2to3.fixer_util import Name, syms
class FixDefIteritems(fixer_base.BaseFix):
BM_compatible = True
PATTERN = """
classdef< 'class' any+ ':'
suite< any*
funcdef< 'def' name='iteritems'
parameters< '(' NAME ')' > any+ >
any* > >
"""
def transform(self, node, results):
name = results["name"]
new = Name("items", prefix=name.prefix)
name.replace(new)

View File

@ -1,22 +0,0 @@
"""Fixer for iterkeys -> keys methods."""
# Author: Valentin Lorentz
# Code modified from fix_nonzero by Collin Winter
from lib2to3 import fixer_base
from lib2to3.fixer_util import Name, syms
class FixDefIterkeys(fixer_base.BaseFix):
BM_compatible = True
PATTERN = """
classdef< 'class' any+ ':'
suite< any*
funcdef< 'def' name='iterkeys'
parameters< '(' NAME ')' > any+ >
any* > >
"""
def transform(self, node, results):
name = results["name"]
new = Name("keys", prefix=name.prefix)
name.replace(new)

View File

@ -1,22 +0,0 @@
"""Fixer for itervalues -> values methods."""
# Author: Valentin Lorentz
# Code modified from fix_nonzero by Collin Winter
from lib2to3 import fixer_base
from lib2to3.fixer_util import Name, syms
class FixDefItervalues(fixer_base.BaseFix):
BM_compatible = True
PATTERN = """
classdef< 'class' any+ ':'
suite< any*
funcdef< 'def' name='itervalues'
parameters< '(' NAME ')' > any+ >
any* > >
"""
def transform(self, node, results):
name = results["name"]
new = Name("values", prefix=name.prefix)
name.replace(new)

View File

@ -1,106 +0,0 @@
"""Fixer for import statements.
If spam is being imported from the local directory, this import:
from spam import eggs
Becomes:
from .spam import eggs
And this import:
import spam
Becomes:
from . import spam
"""
# Local imports
from lib2to3 import fixer_base
from os.path import dirname, join, exists, sep, split, isdir
from os import listdir
from lib2to3.fixer_util import FromImport, syms, token
def traverse_imports(names):
"""
Walks over all the names imported in a dotted_as_names node.
"""
pending = [names]
while pending:
node = pending.pop()
if node.type == token.NAME:
yield node.value
elif node.type == syms.dotted_name:
yield "".join([ch.value for ch in node.children])
elif node.type == syms.dotted_as_name:
pending.append(node.children[0])
elif node.type == syms.dotted_as_names:
pending.extend(node.children[::-2])
else:
raise AssertionError("unkown node type")
class FixImport(fixer_base.BaseFix):
BM_compatible = True
PATTERN = """
import_from< 'from' imp=any 'import' ['('] any [')'] >
|
import_name< 'import' imp=any >
"""
def start_tree(self, tree, name):
super(FixImport, self).start_tree(tree, name)
self.skip = "absolute_import" in tree.future_features
def transform(self, node, results):
if self.skip:
return
imp = results['imp']
if node.type == syms.import_from:
# Some imps are top-level (eg: 'import ham')
# some are first level (eg: 'import ham.eggs')
# some are third level (eg: 'import ham.eggs as spam')
# Hence, the loop
while not hasattr(imp, 'value'):
imp = imp.children[0]
if self.probably_a_local_import(imp.value):
imp.value = "." + imp.value
imp.changed()
else:
have_local = False
have_absolute = False
for mod_name in traverse_imports(imp):
if self.probably_a_local_import(mod_name):
have_local = True
else:
have_absolute = True
if have_absolute:
if have_local:
# We won't handle both sibling and absolute imports in the
# same statement at the moment.
self.warning(node, "absolute and local imports together")
return
new = FromImport(".", [imp])
new.prefix = node.prefix
return new
def probably_a_local_import(self, imp_name):
if imp_name.startswith("."):
# Relative imports are certainly not local imports.
return False
imp_name = imp_name.split(".", 1)[0]
base_path = dirname(self.filename)
base_path = join(base_path, imp_name)
# If there is no __init__.py next to the file its not in a package
# so can't be a relative import.
if not exists(join(dirname(base_path), "__init__.py")):
return False
(path, filename) = split(base_path)
if isdir(base_path) and filename in listdir(path):
# We use listdir too because of case-insensitivity on Windows
return True
for ext in [".py", ".pyc", ".so", ".sl", ".pyd"]:
if (filename + ext) in listdir(path):
# We use this instead of os.path.exists because of case-insensitivity
# on Windows
return True
return False

View File

@ -1,27 +0,0 @@
# Based on fix_intern.py. Original copyright:
# Copyright 2006 Georg Brandl.
# Licensed to PSF under a Contributor Agreement.
"""Fixer for intern().
intern(s) -> sys.intern(s)"""
# Local imports
from lib2to3 import pytree
from lib2to3 import fixer_base
from lib2to3.fixer_util import Name, Attr, touch_import
class FixReload(fixer_base.BaseFix):
BM_compatible = True
order = "pre"
PATTERN = """
power< 'reload'
after=any*
>
"""
def transform(self, node, results):
touch_import('imp', 'reload', node)
return node

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python
import os
import sys
import shutil
from glob import glob
try:
from lib2to3.main import main
except ImportError:
print('Error: you need the 2to3 tool to run this script.')
os.chdir(os.path.join(os.path.dirname(__file__), '..'))
try:
os.unlink('src/version.py')
except OSError:
pass
try:
shutil.rmtree('py3k')
except OSError:
pass
os.mkdir('py3k')
for dirname in ('locales', 'docs', 'plugins', 'src', 'test', 'scripts'):
shutil.copytree(dirname, os.path.join('py3k', dirname))
for filename in ('setup.py',):
shutil.copyfile(filename, os.path.join('py3k', filename))
os.chdir('py3k')
files = ['run.py', 'src', 'plugins', 'test', 'setup.py'] + glob('scripts/*')
args = ['-wn']
fixers = []
for fix in ['all', 'def_iteritems', 'def_itervalues', 'def_iterkeys', 'reload', 'import']:
fixers += ['-f', fix]
sys.argv = files + args + fixers + sys.argv
sys.argc = len(sys.argv)
from . import fix_def_iteritems, fix_def_itervalues, fix_def_iterkeys, fix_reload, fix_import
# Hacks
sys.modules['lib2to3.fixes.fix_def_iteritems'] = fix_def_iteritems
sys.modules['lib2to3.fixes.fix_def_itervalues'] = fix_def_itervalues
sys.modules['lib2to3.fixes.fix_def_iterkeys'] = fix_def_iterkeys
sys.modules['lib2to3.fixes.fix_reload'] = fix_reload
sys.modules['lib2to3.fixes.fix_import'] = fix_import
sys.exit(main("lib2to3.fixes"))

12
ACKS
View File

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

1
CNAME Normal file
View File

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

View File

@ -1,31 +0,0 @@
# Contributing to Limnoria
## Guidelines
Follow the [Style Guidelines].
When adding a string that will be shown on IRC, always internationalize
it (wrap it in a call to `_()`).
When making a trivial change to an internationalized string that does not
affect the meaning of the string (typo fix, etc.), please update the
`msgid` entry in localization file. It helps preserve the translation
without the translator having to review it.
[Style Guidelines]:http://supybot.aperio.fr/doc/develop/style.html
## Sending patches
When you send a pull request, **send it to the testing branch**.
It will be merged to master when it's considered to be enough stable to be
supported.
Don't fear that you spam Limnoria by sending many pull requests. According
to @ProgVal, it's easier for them to accept pull requests than to
cherry-pick everything manually.
See also [Contributing to Limnoria] at [Limnoria documentation].
[Contributing to Limnoria]:http://supybot.aperio.fr/doc/contribute/index.html#contributing-to-limnoria
[Limnoria documentation]:http://supybot.aperio.fr/doc/index.html

2022
ChangeLog

File diff suppressed because it is too large Load Diff

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

View File

@ -1,234 +0,0 @@
# Common
**Note: there is easier [installation guide in documentation!](http://doc.supybot.aperio.fr/en/latest/use/install.html)**
First things first: Supybot *requires* at least Python 2.6. There
isn't any way to get around it. You can get it from [Python homepage].
[Python homepage]:http://python.org/
# Recommended Software
The following libraries are not needed for running Limnoria, but enable
extra features you may want. (Order by decreasing estimated usefulness)
[charade] -- enables better encoding handling
[pytz] and [python-dateutil] -- enable additional features of the `Time` plugin
[python-gnupg] -- enables user authentication with GPG
[charade]:https://pypi.python.org/pypi/charade
[pytz]:https://pypi.python.org/pypi/pytz
[python-dateutil]:https://pypi.python.org/pypi/python-dateutil
[python-gnupg]:https://pypi.python.org/pypi/python-gnupg
To install them, run
`pip install -r requirements.txt`
or if you don't have or don't want to use root,
`pip install -r requirements.txt --user`
For more information and help on how to use Supybot, checkout
the documents under [docs/], especially [GETTING_STARTED] and
[CONFIGURATION], or on [the website]
[docs/]:docs/index.rst
[GETTING_STARTED]:docs/GETTING_STARTED.rst
[CONFIGURATION]:docs/CONFIGURATION.rst
[the website]:http://supybot.aperio.fr/doc/use/index.html
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.
# UNIX/Linux/BSD
If you use [Debian or Ubuntu, click here](INSTALL.md#debian) or [Fedora, click here.](INSTALL.md#fedora)
If you're installing Python using your distributor's packages, you may
need a python-dev or python3-dev package installed, too. If you don't have
a '/usr/lib/python2.x/distutils' directory or
'/usr/lib/python2.x/config/Makefile' or with Python 3
'/usr/lib/python3.x/distutils' or '/usr/lib/python3.x/config/Makefile' (assuming '/usr/lib/python2.x' or '/usr/lib/python3.x' is where your Python
libs are installed), then you will need a python-dev or python3-dev package.
## git
First start by git cloning Limnoria and moving to the cloned repository.
```
git clone https://github.com/ProgVal/Limnoria.git
cd Limnoria
```
The rest depends on do you have root access and do you want to perform global or local install.
### Global install
Run
```
python setup.py install
```
`python` can be replaced with `python2` (if your distribution
uses Python 3 by default) or `python3` if you want to use Python 3
version.
Now you 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.
### Local install
Run
```
python setup.py install --user
```
`python` can be replaced with `python2` (if your distribution
uses Python 3 by default) or `python3` if you want to use Python 3
version.
and you will have new programs installed in ~/.local/bin. 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.
By default you must run the bot with full path to the binary unless you specify $PATH.
Run the following command to fix your PATH. We presume that you use bash
and if you don't, you most probably know how to do this with other shell.
```
echo 'PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
## Debian
*Debian packages are automatically build nightly at 00:00Z.*
For Debian and other distributions based on it (Ubuntu etc.), there are
packages which you can install with
```
wget http://builds.progval.net/limnoria/debian/python2/limnoria-master-HEAD.deb
sudo dpkg -i limnoria-master-HEAD.deb
```
## Fedora
*Fedora packages are automatically build nightly at 00:00Z.*
For Fedora and other distributions using rpm (RHEL, CentOS etc.), there are
packages which you can install with
```
yum install http://builds.progval.net/limnoria/fedora/python2/limnoria-master-HEAD.noarch.rpm
```
## Pip
To install with pip run
```
sudo pip install -r https://raw.githubusercontent.com/ProgVal/Limnoria/master/requirements.txt
sudo pip install git+https://github.com/ProgVal/Limnoria.git@master
```
or without root if you don't have it or don't want to use it.
```
pip install -r https://raw.githubusercontent.com/ProgVal/Limnoria/master/requirements.txt --user
pip install git+https://github.com/ProgVal/Limnoria.git@master --user
```
If you wish to use Python 3 or 2 instead of default of your distribution
run `pipX` where X is either 2 or 3 instead of `pip`.
If pip gives error immediately instead of doing anything and you have git$ installd, try upgrading pip with `sudo pip install pip --upgrade` or without
root, `pip install pip --upgrade --user`.
### Upgrading
#### git
To upgrade, return to the cloned Limnoria repository and run:
```
git pull
```
and then install Limnoria normally. "python setup.py install" doesn't affect config files of the bot any way.
If you don't have the cloned Limnoria repository, clone it again using the installation instructions.
### Debian/Fedora
Run the same commands as before on [Debian](INSTALL.md#debian) or
[Fedora](INSTALL.md#fedora) section of this file.
### Pip
Run the first install command again, but add `--upgrade` to the
end. Then run the second install command.
## Upgrading to Python 3
Upgrading Python3 happens the same way, but if you want to move from 2 to 3
or 3 to 2, you must remove the build/ directory and the executable
supybot* files first. The build/ directory is on same directory as this
file and supybot* are usually in /usr/local/bin or ~/.local/bin
```
rm -rf build/
rm /usr/local/bin/supybot*
rm ~/.local/bin/supybot*
```
## Windows
**Note**: 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=C:\Python2x\;%PATH%
```
You should now be able to type 'python' to start the Python
interpreter. Exit by pressing CTRL-Z and then Return. 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:\Python2x\'. You will now have several new
programs installed in 'C:\Python2x\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.

View File

@ -1,28 +0,0 @@
Copyright (c) 2002-2009 Jeremiah Fincher and others
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions, and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author of this software nor the name of
contributors to this software may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Portions of the included source code are copyright by its original author(s)
and remain subject to its associated license.

View File

@ -1 +0,0 @@
recursive-include 2to3 *

View File

@ -1,36 +0,0 @@
PYTHON=`which python`
DESTDIR=/
BUILDIR=$(CURDIR)/debian/limnoria
PROJECT=limnoria
VERSION=0.83.4.1+limnoria1
all:
@echo "make source - Create source package"
@echo "make install - Install on local system"
@echo "make buildrpm - Generate a rpm package"
@echo "make builddeb - Generate a deb package"
@echo "make clean - Get rid of scratch and byte files"
source:
$(PYTHON) setup.py sdist $(COMPILE)
install:
$(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE)
buildrpm:
$(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall
builddeb:
# build the source package in the parent directory
# then rename it to project_version.orig.tar.gz
$(PYTHON) setup.py sdist $(COMPILE) --dist-dir=../ --prune
rename -f 's/$(PROJECT)-(.*)\.tar\.gz/$(PROJECT)_$$1\.orig\.tar\.gz/' ../*
# build the package
dpkg-buildpackage -i -I -rfakeroot
clean:
$(PYTHON) setup.py clean
$(MAKE) -f $(CURDIR)/debian/rules clean
rm -rf build/ MANIFEST
find . -name '*.pyc' -delete

View File

@ -1,51 +1,11 @@
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.
<!-- @format -->
Limnoria is a project which continues development of Supybot (you can
call it a fork) by fixing bugs and adding features (see the
[list of added features](https://github.com/ProgVal/Limnoria/wiki/LGC) for
more details).
# Mikaela's fork of Limnoria.
# Build status
There are mainly two branches. This one which you are looking at, gh-pages which
is the source of <https://supybot.mikaela.info/>.
Master branch: [![Build Status (master branch)](https://travis-ci.org/ProgVal/Limnoria.png?branch=master)](https://travis-ci.org/ProgVal/Limnoria)
Testing branch: [![Build Status (testing branch)](https://travis-ci.org/ProgVal/Limnoria.png?branch=testing)](https://travis-ci.org/ProgVal/Limnoria)
Limnoria is tested with Python 2.6, 2.7, 3.2, 3.3, 3.4, Pypy and Pypy3. Python 2.5 and
older versions are not supported.
# Support
## Documentation
If this is your first install, there is an [install guide](http://doc.supybot.aperio.fr/en/latest/use/install.html).
You will probably be pointed to it if you ask on IRC how to install Limnoria.
There is extensive documentation at [supybot.aperio.fr] and at [Gribble wiki].
We took the time to write it; you should take the time to read it.
[supybot.aperio.fr]:http://doc.supybot.aperio.fr/
[Gribble wiki]:https://sourceforge.net/apps/mediawiki/gribble/index.php?title=Main_Page
## IRC channels
### In English
If you have any trouble, feel free to swing by [#supybot and #limnoria](ircs://chat.freenode.net:6697/#supybot,#limnoria) on
[freenode](https://freenode.net/) or [#supybot](ircs://irc.oftc.net:6697/#supybot) at [OFTC](http://oftc.net/) (we have a Limnoria 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 the [issue tracker] so we can improve the bot!
[issue tracker]:https://github.com/ProgVal/Limnoria/issues
### In Other languages
Only in French at the moment, located at [#supybot-fr on freenode](irc://chat.freenode.net/#supybot-fr).
**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

435
RELNOTES
View File

@ -1,435 +0,0 @@
Version 0.83.5
The minimum supported Python version has been bumped to 2.6.
utils.str.perlVariableSubstitute is deprecated in favor of using Python's
string.Template directly. perlVariableSubstitute will be removed in a future
release.
Factoids' config variable supybot.plugins.Factoids.factoidPrefix has been
replaced by supybot.plugins.Factoids.format, which allows the user to
determine exactly how replies to Factoid queries are formatted.
supybot-botchk no longer runs supybot inside an instance of /bin/sh.
Version 0.83.4.1
Simple bug-fix release for a couple changes that were introduced last minute
before the previous release.
No incompatibilities.
Version 0.83.4
This release contains fixes for Python2.6 compability as well as a re-written
Google plugin which uses the AJAX API. The re-written plugin gained a
translate command and no longer needs an API key.
ChannelLogger has a new config variable, supybot.plugins.ChannelLogger.enable,
which can be used on a per-channel basis to determine whether that channel is
logged.
The RSS announce command has been restructured into the commands "announce
list", "announce remove", and "announce add" in order to simplify the
command's use.
The supybot.directories.plugins config variable now determines its global
directory dynamically instead of adding the directory to the value. This
means values like '/usr/lib/python2.5/site-packages/supybot/plugins' can be
removed from the variable. This should help ease transitioning a Supybot
config from one Python release to another.
Incompatibilities:
supybot.plugins.Google.safeSearch has been renamed to
supybot.plugins.Google.searchFilter
supybot.plugins.Channel.banmask has been removed in favor of a new
supybot.protocols.irc.banmask config variable. This general config variable
is used by the various commands which would need to know what style of banmask
to use.
Version 0.83.3
Overdue bug fix and Python2.5-compatible release. No significant changes to
worry about from the user perspective.
Version 0.83.2
Mainly bug fix release. The most noticeable change being a value of
'default' for supybot.drivers.module will use Socket instead of Twisted.
Version 0.83.1
No incompatibilities, just fixing an important bug with plugin loading.
Version 0.83.0
This far overdue release contains mostly bug-fixes.
Incompatibilities:
Changed the prefixName keyword argument (which appears in various places
in callbacks.py and the reply-related irc methods) to prefixNick.
Version 0.83.0rc3
This release candidate contains mostly bug-fixes.
Incompatibilities:
utils.changeFunctionName was moved to utils.python.changeFunctionName
Version 0.83.0rc2
This release candidate contains a few bug-fixes and some plugins we
forgot in the last RC. Otherwise, everything is compatible.
Version 0.83.0rc1
There have been some fairly significant changes since our last release.
This means that you should uninstall your previous version before
installing this version.
First, plugins are now a directory of files rather than a single file.
This makes it much easier for an individual plugin to supply any
3rd-party modules it may depend on and resolves some bugs we had with
reloading plugins. supybot-plugin-create (nee supybot-newplugin) has
been updated to reflect this. A side effect of using a directory-based
plugin is that @load/@reload are now case-sensitive. "@load foo" is not
the same as "@load Foo".
As part of the conversion to the new plugin format, some plugins were
broken up into more focused plugins. Also, not all of the plugins that
we used to ship are part of this release. Some we moved to the
supybot-plugins package and some others (which would be candidates for
supybot-plugins) have yet to be converted to the new format.
Second, we've updated the scripts that ship with Supybot. As noted in
the previous section, supybot-newplugin is now named
supybot-plugin-create. We've also added the following scripts:
supybot-botchk - Handy script for starting the bot and keeping it
running. Ideal for having cron automatically start the
bot.
supybot-plugin-doc - Generates documentation for the specified
plugin(s). Currently, the documentation is
generated using Structured TeXt so that it can
easily be uploaded to our website.
supybot-plugin-package - The beginning of a script to make a plugin
package which can be uploaded to our website
for general consumption.
supybot-test - Runs a plugin's test suite.
Third, we've broken supybot.utils into focused sub-modules. There's no
longer a supybot.fix module and we now have the following modules:
supybot.utils.file - utilities for dealing with files (e.g. the old
supybot.utils.transactionalFile is now
supybot.utils.file.AtomicFile)
supybot.utils.iter - utilities for dealing with iterables (all, any,
partition, groupBy, choice, etc)
supybot.utils.gen - general purpose utilities which are imported into
the supybot.utils namespace
supybot.utils.net - utilities for dealing with the network
supybot.utils.python - utilities for dealing with Python
supybot.utils.seq - utilities for dealing with sequences
supybot.utils.str - utilities for dealing with strings (including our
new format() function)
supybot.utils.structures - general purpose structures used in Supybot
supybot.utils.web - utilities for dealing with the web (used to be
supybot.webutils)
Fourth, we've added source-nested plugins (using the class
callbacks.Commands). This allows you to group similar commands
together. Some examples are:
Channel.{ban add,ban list,ban remove}
User.{hostmask add,hostmask list,hostmask remove}
Fifth, we've removed the privmsgs module. All of the functionality
that was offered in that module is now available by using commands.wrap.
Use of this is documented at:
http://supybot.com/documentation/help/tutorial/wrap
Sixth, we've renamed some plugin-related API changes. Some classes had
their names changed. The old names are still available for
backwards-compatibility.
callbacks.Privmsg -> callbacks.Plugin
callbacks.PrivmsgCommandAndRegexp -> callbacks.PluginRegexp
callbacks.IrcObjectProxy -> callbacks.NestedCommandsIrcProxy
callbacks.PrivmsgRegexp was removed since its functionality is covered
by setting using PluginRegexp.
Also, the prototype for a plugin's __init__ method changed:
def __init__(self): -> def __init__(self, irc):
Remember to pass the irc object on when you call the parent class's
__init__ method.
Version 0.80.0
We *finally* hit 0.80.0! This release is completely compatible with
the last release candidate.
An update to Babelfish may cause an error message to be displayed in
the console when the bot is first run. The error message should be
gone when the bot is restarted.
We also have a new community website at http://www.supybot.com/ where
our users can submit their own plugins, view/download other people's
plugins and discuss all things Supybot-related.
Version 0.80.0rc3
Another bugfix release. This one was pretty important as it actually
makes supybot.database.plugins.channelSpecific work properly.
Version 0.80.0rc2
supybot.databases.plugins.channelSpecific.channel was renamed to
supybot.databases.plugins.channelSpecific.link.
supybot.databases.plugins.channelSpecific.link.allow was added, which
determines whether a channel will allow other channels to link to its
database.
Infobot is no longer deprecated and the following changes were made to
its config variables:
supybot.plugins.Infobot.answerUnaddressedQuestions was renamed to
supybot.plugins.Infobot.unaddressed.answerQuestions.
supybot.plugins.Infobot.snarfUnaddressedDefinitions was renamed to
supybot.plugins.Infobot.unaddressed.snarfDefinitions.
supybot.plugins.Infobot.unaddressed.replyExistingFactoid was added to
determine whether the bot will reply when someone attempts to create a
duplicate factoid.
Version 0.80.0pre6
Another bugfix release. No incompatibilities known. The only
registry change is that supybot.databases.users.hash has been
removed.
Version 0.80.0pre5
Completely bugfix release. No incompatibilies known.
Version 0.80.0pre4
Mainly a bug fix release. This will likely be the last release before
0.80.0final, but we're gonna let it stew for a couple weeks to attempt
to catch any lingering bugs.
ansycoreDrivers is now deprecated in favor of socketDrivers or
twistedDrivers.
supybot.databases.plugins.channelSpecific.channel is now a channelValue
so that you can link specific channels together (instead of all channels
being linked together).
For those of you that use eval and/or exec, they have been removed from
the Owner plugin and are now in sandbox/Debug.py (which you'll have to
grab from CVS).
Version 0.80.0pre3
The database format for the Note plugin has changed to a flatfile
format; use tools/noteConvert.py to convert it to the new format.
Ditto that for the URL database.
FunDB is deprecated and will be removed at the next major release;
use tools/fundbConvert.py to convert your old FunDB databases to Lart
and Praise databases.
If you had turned off supybot.databases.plugins.channelSpecific, your
non-channel-specific database files had gone directly into your data/
directory. We had some problems with poor interactions between that
configuration variable and channel capabilities, though, so we
changed the implementation so that non-channel-specific databases are
considered databases of a single (configurable) channel (defaulting
to "#"). This will also help others who are converting from
channel-specific to non-channel-specific databases, but for you
who've already made the switch, you'll need to move your database
files again, from data/ to data/# (or whatever channel you might
change that variable to).
supybot.channels doesn't exist anymore; now the only list of channels
to join is per-network, in supybot.networks.<network>.channels.
We weren't serializing supybot.replies.* properly in older versions.
Now we are, but the old, improperly serialized versions won't work
properly. Remove from your configuration file all variables
beginning with "supybot.replies" before you start the bot.
The URL database has been changed again, but it will use a different
filename so you shouldn't run into conflicts, just a newly-empty
database.
We upgraded the SOAP stuff in others; you may do well to do a
setup.py install --clean this time around.
Version 0.80.0pre2
Many more bugs have been fixed. A few more plugins have been updated
to use our new-style database abstraction. If it seems like your
databases are suddenly empty, look for a new database file named
Plugin.dbtype.db. We've also added a few more configuration variables.
Version 0.80.0pre1
Tons of bugs fixed, many features and plugins added. Everything
should be entirely compatible; many more configuration variables have
been added.
Version 0.79.9999
Some more bugs fixed, added a few features and a couple configuration
variabless. This should hopefully be the last release before 0.80.0,
which will finally bring us to pure Beta status.
Version 0.79.999
Some bugs fixed, but the ones that were fixed were pretty big. This
is, of course, completely compatible with the last release.
Version 0.79.99
Many bugs fixed, thanks to the users who reported them. We're
getting asymptotically closer to 0.80.0 -- maybe this'll be the last
one, maybe we'll have to release an 0.79.999 -- either way, we're
getting close :) Check out the ChangeLog for the fixes and a few new
features.
Version 0.79.9
We've changed so much stuff in this release that we've given up on
users upgrading their configuration files for the new release. So
do a clean install (python2.3 setup.py install --clean), run the
wizard again, and kick some butt.
(It's rumored that you can save most of your old configuration by
appending your new configuration at the end of your old configuration
and running supybot with that new configuration file. This, of
course, comes with no warranty or guarantee of utility -- try it if
you want, but backup your original configuration file!)
Version 0.77.2
This is a drop-in replacement for 0.77.1, with two exceptions. The
configuration variable formerly known as
"supybot.plugins.Services.password" is now known as
"supybot.plugins.Services.NickServ.password", due to the fact that
there might be different passwords for NickServ and ChanServ (and
ChanServ passwords are per-channel, whereas NickServ passwords are
global). If you're using the Services plugin, you'll need to make
this change in order to continue identifying with services. The
configuration variable formerly known as
"supybot.plugins.Babelfish.disabledLanguages" is now known as
"supybot.plugins.Babelfish.languages". The configuration variable now
accepts the languages that *will* be translated as opposed to ones
that are *not* translated.
Tests and the developer sandbox are not longer delivered with our
release tarballs. If you're a developer and you want these, you
should either check out CVS or download one of our weekly CVS
snapshots, available at http://supybot.sourceforge.net/snapshots/ .
Version 0.77.1
This is a drop-in replacement for 0.77.0 -- no incompatibilities, to
out knowledge. Simply install over your old installation and restart
your bot :)
Version 0.77.0
Setup.py will automatically remove your old installations for you, no
need to worry about that yourself.
Configuration has been *entirely* redone. Read the new
GETTING_STARTED document to see how to work with configuration
variables now. Your old botscripts from earlier versions *will not*
work with the new configuration method. We'd appreciate it if you'd
rerun the wizard in order for us to find any bugs that remain in it
before we officially declare ourselves Beta. Note also that because
of the new configuration method, the interface for plugins' configure
function has changed: there are no longer any onStart or afterConnect
arguments, so all configuration should be performed via the registry.
Channel capabilities have been changed; rather than being
#channel.capability, they're now #channel,capability. It's a bit
uglier, we know, but dots can be valid in channel names, and we
needed the dot for handling plugin.command capabilities.
tools/ircdbConvert.py should update this for you.
The on-disk format of the user/channel databases has changed to be far
more readable. A conversion utility is included, as mentioned before:
tools/ircdbConvert.py. Run this with no arguments to see the
directions for using it.
Uh, we were just kidding about the upgrade script in 0.76.0 :) It'll
be a little while longer. We do have several little upgrade scripts,
though.
Version 0.76.1
Almost entirely bugfixes, just some minor (and some less minor) bugs
that need to get in before we really start hacking on the next
version. Should be *entirely* compatible with 0.76.0.
Version 0.76.0
Major bugfix release. A great number of bugs fixed. This is the last
release without an upgrade script.
The only hiccup in the upgrade from 0.75.0 should be that you'll need
to update your botscript to reflect the removal of the debug module.
We'd rather you use supybot-wizard to generate a new botscript, of
course, but if you insist on modifying your existing botscript, take a
look at
<http://cvs.sourceforge.net/viewcvs.py/supybot/supybot/src/template.py?r1=1.20&r2=1.21>
to see what you need to do.
Version 0.75.0
Don't forget to reinstall (i.e., run "python setup.py install" as
root). Sometimes it even does good to remove the old installation;
$PYTHON/site-packages/supybot can be removed with no problems
whatsoever.
You will need to re-run supybot-wizard and generate a new botscript.
The Infobot plugin has been removed from this release; it's not ready
for prime time. If you're interested in getting it running (i.e., you
want full Infobot compatibility and aren't satisfied with either
MoobotFactoids or Factoids) then swing over to #supybot and we can
discuss the tests. We simply don't know enough about Infobot to make
sure our Infobot plugin is an exact replica, and need someone's help
with making the changes necessary for that.

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)

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;
}
}

11
debian/changelog vendored
View File

@ -1,11 +0,0 @@
limnoria (0.83.4.1+limnoria2) unstable; urgency=low
* New upstream version.
-- Valentin Lorentz <progval@progval.net> Sun, 23 Jun 2012 17:47:00 +0200
limnoria (0.83.4.1+limnoria1) unstable; urgency=low
* Initial release.
-- Valentin Lorentz <progval@gmail.com> Sun, 21 Aug 2011 20:58:51 +0200

24
debian/control vendored
View File

@ -1,24 +0,0 @@
Source: limnoria
Section: net
Priority: optional
Maintainer: Valentin Lorentz <progval@progval.net>
Build-Depends: debhelper (>=3.9.2), python-support (>= 0.6), cdbs (>= 0.4.49), python (>=2.6), python-setuptools
XS-Python-Version: >=2.6
Standards-Version: 3.9.2
Package: limnoria
Architecture: all
Depends: python (>= 2.6), python-support (>= 0.90.0), ${misc:Depends}
Recommends: python-simplejson, python-feedparser, python-sqlite3
Suggests: python-twisted-core, python-twisted-names, python-dictclient, python-dateutil, python-gnupg, python-sqlalchemy
Conflicts: supybot
Provides: supybot
Replaces: supybot
Section: net
Priority: optional
Homepage: https://github.com/ProgVal/Limnoria
Description: Fork of the robust and user-friendly Python IRC bot Supybot.
It provides several enhancements, such as internationalization and
embedded HTTP server available to plugins. All plugins written for
Supybot are still compatible with Limnoria.

45
debian/copyright vendored
View File

@ -1,45 +0,0 @@
Upstream Author:
Jeremiah Fincher and others
Files: *
Copyright:
2002-2011 Jeremiah Fincher and others
License: BSD
Files: debian/*
Copyright:
2002-2009, James Vega
2011, Valentin Lorentz
License: BSD
License: BSD
Copyright (c) 2002-2009 Jeremiah Fincher and others
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions, and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author of this software nor the name of
contributors to this software may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Portions of the included source code are copyright by its original author(s)
and remain subject to its associated license.

3
debian/postinst vendored
View File

@ -1,3 +0,0 @@
#!/bin/sh
set -e
#DEBHELPER#

3
debian/prerm vendored
View File

@ -1,3 +0,0 @@
#!/bin/sh
set -e
#DEBHELPER#

11
debian/rules vendored
View File

@ -1,11 +0,0 @@
#!/usr/bin/make -f
# -*- makefile -*-
DEB_PYTHON_SYSTEM := pysupport
include /usr/share/cdbs/1/rules/debhelper.mk
include /usr/share/cdbs/1/class/python-distutils.mk
clean::
rm -rf build build-stamp configure-stamp build/ MANIFEST
dh_clean

View File

@ -1,342 +0,0 @@
Advanced Plugin Config
----------------------
This tutorial covers some of the more advanced plugin config features available
to Supybot plugin authors.
What's This Tutorial For?
=========================
Brief overview of what this tutorial covers and the target audience.
Want to know the crazy advanced features available to you, the Supybot plugin
author? Well, this is the tutorial for you. This article assumes you've read
the Supybot plugin author tutorial since all the basics of plugin config are
handled there first.
In this tutorial we'll cover:
* Using the configure function more effectively by using the functions
provided in supybot.questions
* Creating config variable groups and config variables underneath those
groups.
* The built-in config variable types ("registry types") for use with config
variables
* Creating custom registry types to handle config variable values more
effectively
Using 'configure' effectively
=============================
How to use 'configure' effectively using the functions from
'supybot.questions'
In the original Supybot plugin author tutorial you'll note that we gloss over
the configure portion of the config.py file for the sake of keeping the
tutorial to a reasonable length. Well, now we're going to cover it in more
detail.
The supybot.questions module is a nice little module coded specifically to help
clean up the configure section of every plugin's config.py. The boilerplate
config.py code imports the four most useful functions from that module:
* "expect" is a very general prompting mechanism which can specify certain
inputs that it will accept and also specify a default response. It takes
the following arguments:
- prompt: The text to be displayed
- possibilities: The list of possible responses (can be the empty
list, [])
- default (optional): Defaults to None. Specifies the default value
to use if the user enters in no input.
- acceptEmpty (optional): Defaults to False. Specifies whether or not
to accept no input as an answer.
* "anything" is basically a special case of expect which takes anything
(including no input) and has no default value specified. It takes only
one argument:
- prompt: The text to be displayed
* "something" is also a special case of expect, requiring some input and
allowing an optional default. It takes the following arguments:
- prompt: The text to be displayed
- default (optional): Defaults to None. The default value to use if
the user doesn't input anything.
* "yn" is for "yes or no" questions and basically forces the user to input
a "y" for yes, or "n" for no. It takes the following arguments:
- prompt: The text to be displayed
- default (optional): Defaults to None. Default value to use if the
user doesn't input anything.
All of these functions, with the exception of "yn", return whatever string
results as the answer whether it be input from the user or specified as the
default when the user inputs nothing. The "yn" function returns True for "yes"
answers and False for "no" answers.
For the most part, the latter three should be sufficient, but we expose expect
to anyone who needs a more specialized configuration.
Let's go through a quick example configure that covers all four of these
functions. First I'll give you the code, and then we'll go through it,
discussing each usage of a supybot.questions function just to make sure you
realize what the code is actually doing. Here it is:
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself 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
WorldDom = conf.registerPlugin('WorldDom', True)
if yn("""The WorldDom plugin allows for total world domination
with simple commands. Would you like these commands to
be enabled for everyone?""", default=False):
WorldDom.globalWorldDominationRequires.setValue("")
else:
cap = something("""What capability would you like to require for
this command to be used?""", default="Admin")
WorldDom.globalWorldDominationRequires.setValue(cap)
dir = expect("""What direction would you like to attack from in
your quest for world domination?""",
["north", "south", "east", "west", "ABOVE"],
default="ABOVE")
WorldDom.attackDirection.setValue(dir)
As you can see, this is the WorldDom plugin, which I am currently working on.
The first thing our configure function checks is to see whether or not the bot
owner would like the world domination commands in this plugin to be available
to everyone. If they say yes, we set the globalWorldDominationRequires
configuration variable to the empty string, signifying that no specific
capabilities are necessary. If they say no, we prompt them for a specific
capability to check for, defaulting to the "Admin" capability. Here they can
create their own custom capability to grant to folks which this plugin will
check for if they want, but luckily for the bot owner they don't really have to
do this since Supybot's capabilities system can be flexed to take care of this.
Lastly, we check to find out what direction they want to attack from as they
venture towards world domination. I prefer "death from above!", so I made that
the default response, but the more boring cardinal directions are available as
choices as well.
Using Config Groups
===================
A brief overview of how to use config groups to organize config variables
Supybot's Hierarchical Configuration
Supybot's configuration is inherently hierarchical, as you've probably already
figured out in your use of the bot. Naturally, it makes sense to allow plugin
authors to create their own hierarchies to organize their configuration
variables for plugins that have a lot of plugin options. If you've taken a look
at the plugins that Supybot comes with, you've probably noticed that several of
them take advantage of this. In this section of this tutorial we'll go over how
to make your own config hierarchy for your plugin.
Here's the brilliant part about Supybot config values which makes hierarchical
structuring all that much easier - values are groups. That is, any config value
you may already defined in your plugins can already be treated as a group, you
simply need to know how to add items to that group.
Now, if you want to just create a group that doesn't have an inherent value you
can do that as well, but you'd be surprised at how rarely you have to do that.
In fact if you look at most of the plugins that Supybot comes with, you'll only
find that we do this in a handful of spots yet we use the "values as groups"
feature quite a bit.
Creating a Config Group
=======================
As stated before, config variables themselves are groups, so you can create a
group simply by creating a configuration variable:
conf.registerGlobalValue(WorldDom, 'globalWorldDominationRequires',
registry.String('', """Determines the capability required to access the
world domination commands in this plugin."""))
As you probably know by now this creates the config variable
supybot.plugins.WorldDom.globalWorldDominationRequires which you can access/set
using the Config plugin directly on the running bot. What you may not have
known prior to this tutorial is that that variable is also a group.
Specifically, it is now the WorldDom.globalWorldDominationRequires group, and
we can add config variables to it! Unfortunately, this particular bit of
configuration doesn't really require anything underneath it, so let's create a
new group which does using the "create only a group, not a value" command.
Let's create a configurable list of targets for different types of attacks
(land, sea, air, etc.). We'll call the group attackTargets. Here's how you
create just a config group alone with no value assigned:
conf.registerGroup(WorldDom, 'attackTargets')
The first argument is just the group under which you want to create your new
group (and we got WorldDom from conf.registerPlugin which was in our
boilerplate code from the plugin creation wizard). The second argument is, of
course, the group name. So now we have WorldDom.attackTargets (or, fully,
supybot.plugins.WorldDom.attackTargets).
Adding Values to a Group
========================
Actually, you've already done this several times, just never to a custom group
of your own. You've always added config values to your plugin's config group.
With that in mind, the only slight modification needed is to simply point to
the new group:
conf.registerGlobalValue(WorldDom.attackTargets, 'air',
registry.SpaceSeparatedListOfStrings('', """Contains the list of air
targets."""))
And now we have a nice list of air targets! You'll notice that the first
argument is WorldDom.attackTargets, our new group. Make sure that the
conf.registerGroup call is made before this one or else you'll get a nasty
AttributeError.
The Built-in Registry Types
===========================
A rundown of all of the built-in registry types available for use with config
variables.
The "registry" module defines the following config variable types for your use
(I'll include the 'registry.' on each one since that's how you'll refer to it in
code most often). Most of them are fairly self-explanatory, so excuse the
boring descriptions:
* registry.Boolean - A simple true or false value. Also accepts the
following for true: "true", "on" "enable", "enabled", "1", and the
following for false: "false", "off", "disable", "disabled", "0",
* registry.Integer - Accepts any integer value, positive or negative.
* registry.NonNegativeInteger - Will hold any non-negative integer value.
* registry.PositiveInteger - Same as above, except that it doesn't accept 0
as a value.
* registry.Float - Accepts any floating point number.
* registry.PositiveFloat - Accepts any positive floating point number.
* registry.Probability - Accepts any floating point number between 0 and 1
(inclusive, meaning 0 and 1 are also valid).
* registry.String - Accepts any string that is not a valid Python command
* registry.NormalizedString - Accepts any string (with the same exception
above) but will normalize sequential whitespace to a single space..
* registry.StringSurroundedBySpaces - Accepts any string but assures that
it has a space preceding and following it. Useful for configuring a
string that goes in the middle of a response.
* registry.StringWithSpaceOnRight - Also accepts any string but assures
that it has a space after it. Useful for configuring a string that
begins a response.
* registry.Regexp - Accepts only valid (Perl or Python) regular expressions
* registry.SpaceSeparatedListOfStrings - Accepts a space-separated list of
strings.
There are a few other built-in registry types that are available but are not
usable in their current state, only by creating custom registry types, which
we'll go over in the next section.
Custom Registry Types
=====================
How to create and use your own custom registry types for use in customizing
plugin config variables.
Why Create Custom Registry Types?
For most configuration, the provided types in the registry module are
sufficient. However, for some configuration variables it's not only convenient
to use custom registry types, it's actually recommended. Customizing registry
types allows for tighter restrictions on the values that get set and for
greater error-checking than is possible with the provided types.
What Defines a Registry Type?
First and foremost, it needs to subclass one of the existing registry types
from the registry module, whether it be one of the ones in the previous section
or one of the other classes in registry specifically designed to be subclassed.
Also it defines a number of other nice things: a custom error message for your
type, customized value-setting (transforming the data you get into something
else if wanted), etc.
Creating Your First Custom Registry Type
As stated above, priority number one is that you subclass one of the types in
the registry module. Basically, you just subclass one of those and then
customize whatever you want. Then you can use it all you want in your own
plugins. We'll do a quick example to demonstrate.
We already have registry.Integer and registry.PositiveInteger, but let's say we
want to accept only negative integers. We can create our own NegativeInteger
registry type like so:
class NegativeInteger(registry.Integer):
"""Value must be a negative integer."""
def setValue(self, v):
if v >= 0:
self.error()
registry.Integer.setValue(self, v)
All we need to do is define a new error message for our custom registry type
(specified by the docstring for the class), and customize the setValue
function. Note that all you have to do when you want to signify that you've
gotten an invalid value is to call self.error(). Finally, we call the parent
class's setValue to actually set the value.
What Else Can I Customize?
Well, the error string and the setValue function are the most useful things
that are available for customization, but there are other things. For examples,
look at the actual built-in registry types defined in registry.py (in the src
directory distributed with the bot).
What Subclasses Can I Use?
Chances are one of the built-in types in the previous section will be
sufficient, but there are a few others of note which deserve mention:
* registry.Value - Provides all the core functionality of registry types
(including acting as a group for other config variables to reside
underneath), but nothing more.
* registry.OnlySomeStrings - Allows you to specify only a certain set of
strings as valid values. Simply override validStrings in the inheriting
class and you're ready to go.
* registry.SeparatedListOf - The generic class which is the parent class to
registry.SpaceSeparatedListOfStrings. Allows you to customize four
things: the type of sequence it is (list, set, tuple, etc.), what each
item must be (String, Boolean, etc.), what separates each item in the
sequence (using custom splitter/joiner functions), and whether or not
the sequence is to be sorted. Look at the definitions of
registry.SpaceSeparatedListOfStrings and
registry.CommaSeparatedListOfStrings at the bottom of registry.py for
more information. Also, there will be an example using this in the
section below.
Using My Custom Registry Type
Using your new registry type is relatively straightforward. Instead of using
whatever registry built-in you might have used before, now use your own custom
class. Let's say we define a registry type to handle a comma-separated list of
probabilities:
class CommaSeparatedListOfProbabilities(registry.SeparatedListOf):
Value = registry.Probability
def splitter(self, s):
return re.split(r'\s*,\s*', s)
joiner = ', '.join
Now, to use that type we simply have to specify it whenever we create a config
variable using it:
conf.registerGlobalValue(SomePlugin, 'someConfVar',
CommaSeparatedListOfProbabilities('0.0, 1.0', """Holds the list of
probabilities for whatever."""))
Note that we initialize it just the same as we do any other registry type, with
two arguments: the default value, and then the description of the config
variable.

View File

@ -1,283 +0,0 @@
Advanced Plugin Testing
-----------------------
The complete guide to writing tests for your plugins.
Why Write Tests?
================
Why should I write tests for my plugin? Here's why.
For those of you asking "Why should I write tests for my plugin? I tried it
out, and it works!", read on. For those of you who already realize that
Testing is Good (TM), skip to the next section.
Here are a few quick reasons why to test your Supybot plugins.
* When/if we rewrite or change certain features in Supybot, tests make
sure your plugin will work with these changes. It's much easier to run
supybot-test MyPlugin after upgrading the code and before even reloading
the bot with the new code than it is to load the bot with new code and
then load the plugin only to realize certain things don't work. You may
even ultimately decide you want to stick with an older version for a while
as you patch your custom plugin. This way you don't have to rush a patch
while restless users complain since you're now using a newer version that
doesn't have the plugin they really like.
* Running the automated tests takes a few seconds, testing plugins in IRC
on a live bot generally takes quite a bit longer. We make it so that
writing tests generally doesn't take much time, so a small initial
investment adds up to lots of long-term gains.
* If you want your plugin to be included in any of our releases (the core
Supybot if you think it's worthy, or our supybot-plugins package), it has
to have tests. Period.
For a bigger list of why to write unit tests, check out this article:
http://www.onjava.com/pub/a/onjava/2003/04/02/javaxpckbk.html
and also check out what the Extreme Programming folks have to say about unit
tests:
http://www.extremeprogramming.org/rules/unittests.html
Plugin Tests
============
How to write tests for commands in your plugins.
Introduction
This tutorial assumes you've read through the plugin author tutorial, and that
you used supybot-plugin-create to create your plugin (as everyone should). So,
you should already have all the necessary imports and all that boilerplate
stuff in test.py already, and you have already seen what a basic plugin test
looks like from the plugin author tutorial. Now we'll go into more depth about
what plugin tests are available to Supybot plugin authors.
Plugin Test Case Classes
Supybot comes with two plugin test case classes, PluginTestCase and
ChannelPluginTestCase. The former is used when it doesn't matter whether or
not the commands are issued in a channel, and the latter is used for when it
does. For the most part their API is the same, so unless there's a distinction
between the two we'll treat them as one and the same when discussing their
functionality.
The Most Basic Plugin Test Case
At the most basic level, a plugin test case requires three things:
* the class declaration (subclassing PluginTestCase or
ChannelPluginTestCase)
* a list of plugins that need to be loaded for these tests (does not
include Owner, Misc, or Config, those are always automatically loaded) -
often this is just the name of the plugin that you are writing tests for
* some test methods
Here's what the most basic plugin test case class looks like (for a plugin
named MyPlugin):
class MyPluginTestCase(PluginTestCase):
plugins = ('MyPlugin',)
def testSomething(self):
# assertions and such go here
Your plugin test case should be named TestCase as you see above, though it
doesn't necessarily have to be named that way (supybot-plugin-create puts that
in place for you anyway). As you can see we elected to subclass PluginTestCase
because this hypothetical plugin apparently doesn't do anything
channel-specific.
As you probably noticed, the plugins attribute of the class is where the list
of necessary plugins goes, and in this case just contains the plugin that we
are testing. This will be the case for probably the majority of plugins. A lot
of the time test writers will use a bot function that performs some function
that they don't want to write code for and they will just use command nesting
to feed the bot what they need by using that plugin's functionality. If you
choose to do this, only do so with core bot plugins as this makes distribution
of your plugin simpler. After all, we want people to be able to run your
plugin tests without having to have all of your plugins!
One last thing to note before moving along is that each of the test methods
should describe what they are testing. If you want to test that your plugin
only responds to registered users, don't be afraid to name your test method
testOnlyRespondingToRegisteredUsers or testNotRespondingToUnregisteredUsers.
You may have noticed some rather long and seemingly unwieldy test method names
in our code, but that's okay because they help us know exactly what's failing
when we run our tests. With an ambiguously named test method we may have to
crack open test.py after running the tests just to see what it is that failed.
For this reason you should also test only one thing per test method. Don't
write a test method named testFoobarAndBaz. Just write two test methods,
testFoobar and testBaz. Also, it is important to note that test methods must
begin with test and that any method within the class that does begin with test
will be run as a test by the supybot-test program. If you want to write
utility functions in your test class that's fine, but don't name them
something that begins with test or they will be executed as tests.
Including Extra Setup
Some tests you write may require a little bit of setup. For the most part it's
okay just to include that in the individual test method itself, but if you're
duplicating a lot of setup code across all or most of your test methods it's
best to use the setUp method to perform whatever needs to be done prior to
each test method.
The setUp method is inherited from the whichever plugin test case class you
chose for your tests, and you can add whatever functionality you want to it.
Note the important distinction, however: you should be adding to it and not
overriding it. Just define setUp in your own plugin test case class and it
will be run before all the test methods are invoked.
Let's do a quick example of one. Let's write a setUp method which registers a
test user for our test bot:
def setUp(self):
ChannelPluginTestCase.setUp(self) # important!!
# Create a valid user to use
self.prefix = 'foo!bar@baz'
self.feedMsg('register tester moo', to=self.nick, frm=self.prefix))
m = self.getMsg() # Response to registration.
Now notice how the first line calls the parent class's setUp method first?
This must be done first. Otherwise several problems are likely to arise. For
one, you wouldn't have an irc object at self.irc that we use later on nor
would self.nick be set.
As for the rest of the method, you'll notice a few things that are available
to the plugin test author. self.prefix refers to the hostmask of the
hypothetical test user which will be "talking" to the bot, issuing commands.
We set it to some generically fake hostmask, and then we use feedMsg to send
a private message (using the bot's nick, accessible via self.nick) to the bot
registering the username "tester" with the password "moo". We have to do it
this way (rather than what you'll find out is the standard way of issuing
commands to the bot in test cases a little later) because registration must be
done in private. And lastly, since feedMsg doesn't dequeue any messages from
the bot after being fed a message, we perform a getMsg to get the response.
You're not expected to know all this yet, but do take note of it since using
these methods in test-writing is not uncommon. These utility methods as well as
all of the available assertions are covered in the next section.
So, now in any of the test methods we write, we'll be able to count on the
fact that there will be a registered user "tester" with a password of "moo",
and since we changed our prefix by altering self.prefix and registered after
doing so, we are now identified as this user for all messages we send unless
we specify that they are coming from some other prefix.
The Opposite of Setting-up: Tearing Down
If you did some things in your setUp that you want to clean up after, then
this code belongs in the tearDown method of your test case class. It's
essentially the same as setUp except that you probably want to wait to invoke
the parent class's tearDown until after you've done all of your tearing down.
But do note that you do still have to invoke the parent class's tearDown
method if you decide to add in your own tear-down stuff.
Setting Config Variables for Testing
Before we delve into all of the fun assertions we can use in our test methods
it's worth noting that each plugin test case can set custom values for any
Supybot config variable they want rather easily. Much like how we can simply
list the plugins we want loaded for our tests in the plugins attribute of our
test case class, we can set config variables by creating a mapping of
variables to values with the config attribute.
So if, for example, we wanted to disable nested commands within our plugin
testing for some reason, we could just do this:
class MyPluginTestCase(PluginTestCase):
config = {'supybot.commands.nested': False}
def testThisThing(self):
# stuff
And now you can be assured that supybot.commands.nested is going to be off for
all of your test methods in this test case class.
Plugin Test Methods
===================
The full list of test methods and how to use them.
Introduction
You know how to make plugin test case classes and you know how to do just
about everything with them except to actually test stuff. Well, listed below
are all of the assertions used in tests. If you're unfamiliar with what an
assertion is in code testing, it is basically a requirement of something that
must be true in order for that test to pass. It's a necessary condition. If
any assertion within a test method fails the entire test method fails and it
goes on to the next one.
Assertions
All of these are methods of the plugin test classes themselves and hence are
accessed by using self.assertWhatever in your test methods. These are sorted
in order of relative usefulness.
* assertResponse(query, expectedResponse) - Feeds query to the bot as a
message and checks to make sure the response is expectedResponse. The
test fails if they do not match (note that prefixed nicks in the
response do not need to be included in the expectedResponse).
* assertError(query) - Feeds query to the bot and expects an error in
return. Fails if the bot doesn't return an error.
* assertNotError(query) - The opposite of assertError. It doesn't matter
what the response to query is, as long as it isn't an error. If it is
not an error, this test passes, otherwise it fails.
* assertRegexp(query, regexp, flags=re.I) - Feeds query to the bot and
expects something matching the regexp (no m// required) in regexp with
the supplied flags. Fails if the regexp does not match the bot's
response.
* assertNotRegexp(query, regexp, flags=re.I) - The opposite of
assertRegexp. Fails if the bot's output matches regexp with the
supplied flags.
* assertHelp(query) - Expects query to return the help for that command.
Fails if the command help is not triggered.
* assertAction(query, expectedResponse=None) - Feeds query to the bot and
expects an action in response, specifically expectedResponse if it is
supplied. Otherwise, the test passes for any action response.
* assertActionRegexp(query, regexp, flags=re.I) - Basically like
assertRegexp but carries the extra requirement that the response must
be an action or the test will fail.
Utilities
* feedMsg(query, to=None, frm=None) - Simply feeds query to whoever is
specified in to or to the bot itself if no one is specified. Can also
optionally specify the hostmask of the sender with the frm keyword.
Does not actually perform any assertions.
* getMsg(query) - Feeds query to the bot and gets the response.
Other Tests
===========
If you had to write helper code for a plugin and want to test it, here's
how.
Previously we've only discussed how to test stuff in the plugin that is
intended for IRC. Well, we realize that some Supybot plugins will require
utility code that doesn't necessarily require all of the overhead of setting
up IRC stuff, and so we provide a more lightweight test case class,
SupyTestCase, which is a very very light wrapper around unittest.TestCase
(from the standard unittest module) that basically just provides a little
extra logging. This test case class is what you should use for writing those
test cases which test things that are independent of IRC.
For example, in the MoobotFactoids plugin there is a large chunk of utility
code dedicating to parsing out random choices within a factoid using a class
called OptionList. So, we wrote the OptionListTestCase as a SupyTestCase for
the MoobotFactoids plugin. The setup for test methods is basically the same as
before, only you don't have to define plugins since this is independent of
IRC.
You still have the choice of using setUp and tearDown if you wish, since those
are inherited from unittest.TestCase. But, the same rules about calling the
setUp or tearDown method from the parent class still apply.
With all this in hand, now you can write great tests for your Supybot plugins!

View File

@ -1,136 +0,0 @@
============
Capabilities
============
Introduction
------------
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.
User Capabilities
-----------------
What the heck can these capabilities DO?
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 scenes 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'
(anti)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. This can be further refined by
limiting the (anti)capability to a command in a specific plugin or even an
entire plugin. For example, the rot13 command is in the Filter plugin. If a
user should be able to use another rot13 command, but not the one in the Format
plugin, they would simply need to be given '-Format.rot13' anticapability.
Similarly, if a user were to be banned from using the Filter plugin altogether,
they would simply need to be given the '-Filter' anticapability.
Channel Capabilities
--------------------
What if #linux wants completely different capabilities from #windows?
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 errors right then and there, telling the user that
they lack the 'bar' capability. 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, it tells the user that they lack the 'bar'
capability. If neither of these anticapabilities are present, then the bot
just responds to the user like normal.
Default Capabilities
--------------------
So what capabilities am I dealing with already?
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. It's so important, in
fact, that the bot will not allow you to add it with a command--you'll have you
edit the users file directly to give it to someone.
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, cause the bot to ignore a given user, make the bot join or part channels,
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 them 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 'icalc' in the 'Math' plugin, which can
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 the 're' command in the 'Utilities' plugin, 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]' It would basically replace every character in the output of
'dict go' (14,896 characters!) with the entire output of 'dict go', resulting
in 221MB of memory allocated! And that's not even the worst example!
Final Word
----------
From a programmer's perspective, capabilties are flexible and easy to use. Any
command can check if a user has any capability, even ones not thought of when
the bot was originally written. Plugins can easily 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 user's perspective, capabilities remove a lot of the mystery and
esotery of bot control, in addition to giving a bot owner absolutely
finegrained control over what users are allowed to do with the bot.
Additionally, defaults can be set by the bot owner for both individual channels
and for the bot as a whole, letting an end-user set the policy they want the
bot to follow for users that haven't yet registered in its user database. It's
really a revolution!

View File

@ -1,195 +0,0 @@
=============
Configuration
=============
Introduction
------------
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, take a look at the
GETTING_STARTED document.
Configuration Registry
----------------------
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.
Configuration Groups
--------------------
Using the `Config` plugin, you can list values in a subgroup and get or
set any of the values anywhere in the configuration hierarchy. For
example, let's say you wanted to see what configuration values were
under the `supybot` (the base group) hierarchy. You would simply issue
this command::
<jemfinch|lambda> @config list supybot
<supybot> jemfinch|lambda: @abuse, @capabilities, @commands,
@databases, @debug, @directories, @drivers, @log, @networks,
@nick, @plugins, @protocols, @replies, @reply,
alwaysJoinOnInvite, channels, defaultIgnore,
defaultSocketTimeout, externalIP, flush,
followIdentificationThroughNickChanges, 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.' prepended to 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. The '@' symbol (like the '#' symbol we'll discuss
later) is simply a visual cue and is not actually part of the name.
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 which the bot was running). 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.
Default Values
--------------
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?
Searching the Registry
----------------------
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|lamda> @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.max,
supybot.plugins.Relay.topicSync
Sure, it showed 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 are currently 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.
Channel-Specific Configuration
------------------------------
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::
<jemfinch|lambda> @config channel supybot.reply.whenAddressedBy.chars !
<supybot> jemfinch|lambda: The operation succeeded.
That'll set the prefix chars in the channel from which the message was
sent to '!'. Voila, channel-specific values! Also, note that when
using the `Config` plugin's ``list`` command, channel-specific values are
preceeded by a '#' character to indicate such (similar to how '@' is
used to indicate a group of values).
Editing the Configuration Values by Hand
----------------------------------------
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 its configuration files and database 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' while editing
the files, and no automatic flushing will occur.

View File

@ -1,207 +0,0 @@
==========================
Frequently Asked Questions
==========================
How do I make my Supybot connect to multiple servers?
Just use the `connect` command in the `Network` plugin.
Why does my bot not recognize me or tell me that I don't have the
'owner' capability?
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 hostmask
add`` 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::
hostmask add 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).
What is a hostmask?
Each user on IRC is uniquely identified by a string which we call a
`hostmask`. The IRC RFC refers to it as a prefix. Either way, it
consists of a nick, a user, and a host, in the form
``nick!user@host``. If your Supybot complains that something you've
given to it isn't a hostmask, make sure that you have those three
components and that they're joined in the appropriate manner.
My bot can't handle nicks with brackets in them!
It always complains about something not being a valid command, or
about spurious or missing right brackets, etc.
You should quote arguments (using double quotes, like this:
``"foo[bar]"``) that have brackets in them that you don't wish to be
evaluated as nested commands. Otherwise, you can turn off nested
commands by setting `supybot.commands.nested` to False, or change the
brackets that nest commands, by setting
`supybot.commands.nested.brackets` to some other value (like ``<>``,
which can't occur in IRC nicks).
I added an alias, but it doesn't work!
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.
What does 'lobotomized' mean?
I see this word in commands and in my `channels.conf`, but I don't
know what it means. What does Supybot mean when it says "lobotomized"?
A lobotomy is an operation that removes the frontal lobe of the brain,
the part that does most of a person's thinking. To "lobotomize" a bot
is to tell it to stop thinking--thus, a lobotomized bot will not
respond to anything said by anyone other than its owner in whichever
channels it is lobotomized.
The term is certainly suboptimal, but remains in use because it was
historically used by certain other IRC bots, and we wanted to ease the
transition to Supybot from those bots by reusing as much terminology
as possible.
Is there a way to load all the plugins Supybot has?
No, there isn't. Even if there were, some plugins conflict with other
plugins, so it wouldn't make much sense to load them. For instance,
what would a bot do with `Factoids`, `MoobotFactoids`, and `Infobot`
all loaded? Probably just annoy people :)
If you want to know more about the plugins that are available, check
out our `plugin index`_ at our `website`_.
Is there a command that can tell me what capability another command
requires?
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.
Why doesn't `Karma` seem to work for me?
`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".
Why won't Supybot respond to private messages?
The most likely cause is that you are running your bot on the Freenode
network. Around Sept. 2005, Freenode added a user mode which
registered user could set that `blocks`_ private messages from
unregistered users. So, the reason you aren't seeing a response from
your Supybot is:
* Your Supybot is not registered with NickServ, you are registered,
and you have set the +E user mode for yourself.
* or you have registered your Supybot with NickServ, you aren't
registered, and your Supybot has the +E user mode set.
Can users with the "admin" capability change configuration?
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.
How can I make my Supybot log my IRC channel?
To log all the channels your Supybot is in, simply load the
`ChannelLogger` plugin, which is included in the main distribution.
How do I find out channel modes?
I want to know who's an op in a certain channel, or who's voiced, or
what the modes on the channel are. How do I do that?
Everything you need is kept in a `ChannelState` object in an
`IrcState` object in the `Irc` object your plugin is given. To see
the ops in a given channel, for instance, you would do this::
irc.state.channels['#channel'].ops
To see a dictionary mapping mode chars to values (if any), you would
do this::
irc.state.channels['#channel'].modes
From there, things should be self-evident.
Can Supybot connect through a proxy server?
Supybot is not designed to be allowed to connect to an IRC server via
a proxy server, however there are transparent proxy server helpers
like tsocks_ that are designed to proxy-enable all network
applications, and Supybot does work with these.
Why can't Supybot find the plugin I want to load?
Why does my bot say that 'No plugin "foo" exists.' when I try to load
the foo plugin?
First, make sure you are typing the plugin name correctly. ``@load
foo`` is not the same as ``@load Foo`` [#plugindir]_. If that is not
the problem,
.. [#plugindir] Yes, it used to be the same, but then we moved to using
directories for plugins instead of a single file. Apparently, that
makes a difference to Python.
I've found a bug, what do I do?
Submit your bug on `Sourceforge`_ through our `project page`_.
Why does @tell command from Later deliver messages immediatly?
This is probably because the @tell command defaults to the one in the Misc
plugin, which is intended to do this. Consider using ``@later tell``
instead, or change the default with ``@owner defaultplugin``.
Is Python installed?
I run Windows, and I'm not sure if Python is installed on my computer.
How can I find out for sure?
Python isn't commonly installed by default on Windows computers. If
you don't see it in your start menu somewhere, it's probably not
installed.
The easiest way to find out if Python is installed is simply to
`download it`_ and try to install it. If the installer complains, you
probably already have it installed. If it doesn't, well, now you have
Python installed.
.. _plugin index: http://supybot.com/plugins.html
.. _website: http://supybot.com/
.. _blocks: http://freenode.net/faq.shtml#blockingmessages
.. _tsocks: http://tsocks.sourceforge.net
.. _Sourceforge: http://sourceforge.net/
.. _project page: http://sourceforge.net/projects/supybot
.. _download it: http://python.org/download/

View File

@ -1,181 +0,0 @@
============================
Getting Started with Supybot
============================
Introduction
------------
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 :)
You should have already read through our install document (if you had to
manually install) before reading any further. Now we'll give you a whirlwind
tour as to how you can get Supybot setup and use Supybot effectively.
Initial Setup
-------------
Now that you have Supybot installed, you'll want to get it running. The first
thing you'll want to do is run supybot-wizard. Before running supybot-wizard,
you should be in the directory in which you want your bot-related files to
reside. The wizard will walk you through setting up a base config file for
your Supybot. Once you've completed the wizard, you will have a config file
called botname.conf. In order to get the bot running, run ``supybot
botname.conf``.
Listing Commands
----------------
Ok, so let's assume your bot connected to the server 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 it 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'.
Making Supybot Recognize You
----------------------------
If you ran the wizard, then it is almost certainly the case that 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, 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 'myowneruser' and 'myuserpassword' 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 'hostmask add' command: it lets you add a hostmask to your
user record so the bot recognizes you by your hostmask instead of requiring
you always to identify with it before it recognizes you. Use the 'help'
command to see how this command works. Here's how I often use it::
hostmask add 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.
Loading Plugins
---------------
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.
If you do want to play around with loading plugins, you're going to need to
have the owner capability.
Remember earlier when I told you to try ``help load``? That's the very command
you'll be using. Basically, if you want to load, say, the Games plugin, then
``load Games``. Simple, right? If you need a list of the plugins you can load,
you'll have to list the directory the plugins are in (using whatever command
is appropriate for your operating system, either 'ls' or 'dir').
Getting More From Your Supybot
------------------------------
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.
So basically, the bot keeps, for each person it sees, a list of "chunks" which
are "released" one at a time by the `more` command. In fact, you can even get
the more chunks for another user: if you want to see another chunk in the last
command jemfinch gave, for instance, you would just say `more jemfinch` after
which, his "chunks" now belong to you. So, you would just need to say `more`
to continue seeing chunks from jemfinch's initial command.
Final Word
----------
You should now have a solid foundation for using Supybot. You can use the
`list` command to see what plugins your bot has loaded and what commands are
in those plugins; you can use the 'help' command to see how to use a specific
command, and you can use the 'more' command to continue a long response from
the bot. With these three commands, you should have a strong basis with which
to discover the rest of the features of Supybot!
Do be sure to read our other documentation and make use of the resources we
provide for assistance; this website and, of course, #supybot on
irc.freenode.net if you run into any trouble!

View File

@ -1,89 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Supybot.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Supybot.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -1,557 +0,0 @@
=================================
Writing Your First Supybot Plugin
=================================
Introduction
============
Ok, so you want to write a plugin 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. This will help you
understand crucial things like the way the various commands work and it is
essential prior to embarking upon the plugin-development excursion detailed in
the following pages. If you haven't used Supybot, come back to this document
after you've used it for a while and gotten a feel for it.
So, now that we know you've used Supybot, we'll start getting into details.
We'll go through this tutorial by actually writing a new plugin, named Random
with just a few simple commands.
Caveat: you'll need to have Supybot installed on the machine you
intend to develop plugins on. This will not only allow you to test
the plugins with a live bot, but it will also provide you with
several nice scripts which aid the development of plugins. Most
notably, it provides you with the supybot-plugin-create script which
we will use in the next section... Creating a minimal plugin This
section describes using the 'supybot-plugin-create' script to create
a minimal plugin which we will enhance in later sections.
The recommended way to start writing a plugin is to use the wizard provided,
:command:`supybot-plugin-create`. Run this from within your local plugins
directory, so we will be able to load the plugin and test it out.
It's very easy to follow, because basically all you have to do is answer three
questions. Here's an example session::
[ddipaolo@quinn ../python/supybot]% supybot-plugin-create
What should the name of the plugin be? Random
Sometimes you'll want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a significant amount
of time to run, you'll want to thread them so they don't block the
entire bot.
Does your plugin need to be threaded? [y/n] n
What is your real name, so I can fill in the copyright and license
appropriately? Daniel DiPaolo
Your new plugin template is in the Random directory.
It's that simple! Well, that part of making the minimal plugin is that simple.
You should now have a directory with a few files in it, so let's take a look at
each of those files and see what they're used for.
README.txt
==========
In :file:`README.txt` you put exactly what the boilerplate text says to put in
there:
Insert a description of your plugin here, with any notes, etc. about
using it.
A brief overview of exactly what the purpose of the plugin is supposed to do is
really all that is needed here. Also, if this plugin requires any third-party
Python modules, you should definitely mention those here. You don't have to
describe individual commands or anything like that, as those are defined within
the plugin code itself as you'll see later. You also don't need to acknowledge
any of the developers of the plugin as those too are handled elsewhere.
For our Random plugin, let's make :file:`README.txt` say this:
This plugin contains commands relating to random numbers, and
includes: a simple random number generator, the ability to pick a
random number from within a range, a command for returning a random
sampling from a list of items, and a simple dice roller.
And now you know what's in store for the rest of this tutorial, we'll be
writing all of that in one Supybot plugin, and you'll be surprised at just how
simple it is!
__init__.py
===========
The next file we'll look at is :file:`__init__.py`. If you're familiar with
the Python import mechanism, you'll know what this file is for. If you're not,
think of it as sort of the "glue" file that pulls all the files in this
directory together when you load the plugin. It's also where there are a few
administrative items live that you really need to maintain.
Let's go through the file. For the first 30 lines or so, you'll see the
copyright notice that we use for our plugins, only with your name in place (as
prompted in :command:`supybot-plugin-create`). Feel free to use whatever
license you choose, we don't feel particularly attached to the boilerplate
code so it's yours to license as you see fit even if you don't modify it. For
our example, we'll leave it as is.
The plugin docstring immediately follows the copyright notice and it (like
:file:`README.txt`) tells you precisely what it should contain:
Add a description of the plugin (to be presented to the user inside
the wizard) here. This should describe *what* the plugin does.
The "wizard" that it speaks of is the :command:`supybot-wizard` script that is
used to create working Supybot config file. I imagine that in meeting the
prerequisite of "using a Supybot" first, most readers will have already
encountered this script. Basically, if the user selects to look at this plugin
from the list of plugins to load, it prints out that description to let the
user know what it does, so make sure to be clear on what the purpose of the
plugin is. This should be an abbreviated version of what we put in our
:file:`README.txt`, so let's put this::
Provides a number of commands for selecting random things.
Next in :file:`__init__.py` you see a few imports which are necessary, and
then four attributes that you need to modify for your bot and preferably keep
up with as you develop it: ``__version__``, ``__author__``,
``__contributors__``, ``__url__``.
``__version__`` is just a version string representing the current working
version of the plugin, and can be anything you want. If you use some sort of
RCS, this would be a good place to have it automatically increment the version
string for any time you edit any of the files in this directory. We'll just
make ours "0.1".
``__author__`` should be an instance of the :class:`supybot.Author` class. A
:class:`supybot.Author` is simply created by giving it a full name, a short
name (preferably IRC nick), and an e-mail address (all of these are optional,
though at least the second one is expected). So, for example, to create my
Author user (though I get to cheat and use supybot.authors.strike since I'm a
main dev, muahaha), I would do::
__author__ = supybot.Author('Daniel DiPaolo', 'Strike',
'somewhere@someplace.xxx')
Keep this in mind as we get to the next item...
``__contributors__`` is a dictionary mapping supybot.Author instances to lists
of things they contributed. If someone adds a command named foo to your
plugin, the list for that author should be ``["foo"]``, or perhaps even
``["added foo command"]``. The main author shouldn't be referenced here, as it
is assumed that everything that wasn't contributed by someone else was done by
the main author. For now we have no contributors, so we'll leave it blank.
Lastly, the ``__url__`` attribute should just reference the download URL for
the plugin. Since this is just an example, we'll leave this blank.
The rest of :file:`__init__.py` really shouldn't be touched unless you are
using third-party modules in your plugin. If you are, then you need to take
special note of the section that looks like this::
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them
# to be reloaded when this plugin is reloaded. Don't forget to
# import them as well!
As the comment says, this is one place where you need to make sure you import
the third-party modules, and that you call :func:`reload` on them as well.
That way, if we are reloading a plugin on a running bot it will actually
reload the latest code. We aren't using any third-party modules, so we can
just leave this bit alone.
We're almost through the "boring" part and into the guts of writing Supybot
plugins, let's take a look at the next file.
config.py
=========
:file:`config.py` is, unsurprisingly, where all the configuration stuff
related to your plugin goes. If you're not familiar with Supybot's
configuration system, I recommend reading the config tutorial before going any
further with this section.
So, let's plow through config.py line-by-line like we did the other files.
Once again, at the top is the standard copyright notice. Again, change it to
how you see fit.
Then, some standard imports which are necessary.
Now, the first peculiar thing we get to is the configure function. This
function is what is called by the supybot-wizard whenever a plugin is selected
to be loaded. Since you've used the bot by now (as stated on the first page of
this tutorial as a prerequisite), you've seen what this script does to
configure plugins. The wizard allows the bot owner to choose something
different from the default plugin config values without having to do it through
the bot (which is still not difficult, but not as easy as this). Also, note
that the advanced argument allows you to differentiate whether or not the
person configuring this plugin considers themself an advanced Supybot user. Our
plugin has no advanced features, so we won't be using it.
So, what exactly do we do in this configure function for our plugin? Well, for
the most part we ask questions and we set configuration values. You'll notice
the import line with supybot.questions in it. That provides some nice
convenience functions which are used to (you guessed it) ask questions. The
other line in there is the conf.registerPlugin line which registers our plugin
with the config and allows us to create configuration values for the plugin.
You should leave these two lines in even if you don't have anything else to put
in here. For the vast majority of plugins, you can leave this part as is, so we
won't go over how to write plugin configuration functions here (that will be
handled in a separate article). Our plugin won't be using much configuration,
so we'll leave this as is.
Next, you'll see a line that looks very similar to the one in the configure
function. This line is used not only to register the plugin prior to being
called in configure, but also to store a bit of an alias to the plugin's config
group to make things shorter later on. So, this line should read::
Random = conf.registerPlugin('Random')
Now we get to the part where we define all the configuration groups and
variables that our plugin is to have. Again, many plugins won't require any
configuration so we won't go over it here, but in a separate article dedicated
to sprucing up your config.py for more advanced plugins. Our plugin doesn't
require any config variables, so we actually don't need to make any changes to
this file at all.
Configuration of plugins is handled in depth at the Advanced Plugin Config
Tutorial
plugin.py
=========
Here's the moment you've been waiting for, the overview of plugin.py and how to
make our plugin actually do stuff.
At the top, same as always, is the standard copyright block to be used and
abused at your leisure.
Next, some standard imports. Not all of them are used at the moment, but you
probably will use many (if not most) of them, so just let them be. Since
we'll be making use of Python's standard 'random' module, you'll need to add
the following line to the list of imports::
import random
Now, the plugin class itself. What you're given is a skeleton: a simple
subclass of callbacks.Plugin for you to start with. The only real content it
has is the boilerplate docstring, which you should modify to reflect what the
boilerplate text says - it should be useful so that when someone uses the
plugin help command to determine how to use this plugin, they'll know what they
need to do. Ours will read something like::
"""This plugin provides a few random number commands and some
commands for getting random samples. Use the "seed" command to seed
the plugin's random number generator if you like, though it is
unnecessary as it gets seeded upon loading of the plugin. The
"random" command is most likely what you're looking for, though
there are a number of other useful commands in this plugin. Use
'list random' to check them out. """
It's basically a "guide to getting started" for the plugin. Now, to make the
plugin do something. First of all, to get any random numbers we're going to
need a random number generator (RNG). Pretty much everything in our plugin is
going to use it, so we'll define it in the constructor of our plugin, __init__.
Here we'll also seed it with the current time (standard practice for RNGs).
Here's what our __init__ looks like::
def __init__(self, irc):
self.__parent = super(Random, self)
self.__parent.__init__(irc)
self.rng = random.Random() # create our rng
self.rng.seed() # automatically seeds with current time
Now, the first two lines may look a little daunting, but it's just
administrative stuff required if you want to use a custom __init__. If we
didn't want to do so, we wouldn't have to, but it's not uncommon so I decided
to use an example plugin that did. For the most part you can just copy/paste
those lines into any plugin you override the __init__ for and just change them
to use the plugin name that you are working on instead.
So, now we have a RNG in our plugin, let's write a command to get a random
number. We'll start with a simple command named random that just returns a
random number from our RNG and takes no arguments. Here's what that looks
like::
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()))
random = wrap(random)
And that's it. Now here are the important points.
First and foremost, all plugin commands must have all-lowercase function
names. If they aren't all lowercase they won't show up in a plugin's list of
commands (nor will they be useable in general). If you look through a plugin
and see a function that's not in all lowercase, it is not a plugin command.
Chances are it is a helper function of some sort, and in fact using capital
letters is a good way of assuring that you don't accidentally expose helper
functions to users as commands.
You'll note the arguments to this class method are (self, irc, msg, args). This
is what the argument list for all methods that are to be used as commands must
start with. If you wanted additional arguments, you'd append them onto the end,
but since we take no arguments we just stop there. I'll explain this in more
detail with our next command, but it is very important that all plugin commands
are class methods that start with those four arguments exactly as named.
Next, in the docstring there are two major components. First, the very first
line dictates the argument list to be displayed when someone calls the help
command for this command (i.e., help random). Then you leave a blank line and
start the actual help string for the function. Don't worry about the fact that
it's tabbed in or anything like that, as the help command normalizes it to
make it look nice. This part should be fairly brief but sufficient to explain
the function and what (if any) arguments it requires. Remember that this should
fit in one IRC message which is typically around a 450 character limit.
Then we have the actual code body of the plugin, which consists of a single
line: irc.reply(str(self.rng.random())). The irc.reply function issues a reply
to wherever the PRIVMSG it received the command from with whatever text is
provided. If you're not sure what I mean when I say "wherever the PRIVMSG it
received the command from", basically it means: if the command is issued in a
channel the response is sent in the channel, and if the command is issued in a
private dialog the response is sent in a private dialog. The text we want to
display is simply the next number from our RNG (self.rng). We get that number
by calling the random function, and then we str it just to make sure it is a
nice printable string.
Lastly, all plugin commands must be 'wrap'ed. What the wrap function does is
handle argument parsing for plugin commands in a very nice and very powerful
way. With no arguments, we simply need to just wrap it. For more in-depth
information on using wrap check out the wrap tutorial (The astute Python
programmer may note that this is very much like a decorator, and that's
precisely what it is. However, we developed this before decorators existed and
haven't changed the syntax due to our earlier requirement to stay compatible
with Python 2.3. As we now require Python 2.6 or greater, this may eventually
change to support work via decorators.)
Now let's create a command with some arguments and see how we use those in our
plugin commands. Let's allow the user to seed our RNG with their own seed
value. We'll call the command seed and take just the seed value as the argument
(which we'll require be a floating point value of some sort, though technically
it can be any hashable object). Here's what this command looks like::
def seed(self, irc, msg, args, seed):
"""<seed>
Sets the internal RNG's seed value to <seed>. <seed> must be a
floating point number.
"""
self.rng.seed(seed)
irc.replySuccess()
seed = wrap(seed, ['float'])
You'll notice first that argument list now includes an extra argument, seed. If
you read the wrap tutorial mentioned above, you should understand how this arg
list gets populated with values. Thanks to wrap we don't have to worry about
type-checking or value-checking or anything like that. We just specify that it
must be a float in the wrap portion and we can use it in the body of the
function.
Of course, we modify the docstring to document this function. Note the syntax
on the first line. Arguments go in <> and optional arguments should be
surrounded by [] (we'll demonstrate this later as well).
The body of the function should be fairly straightforward to figure out, but it
introduces a new function - irc.replySuccess. This is just a generic "I
succeeded" command which responds with whatever the bot owner has configured to
be the success response (configured in supybot.replies.success). Note that we
don't do any error-checking in the plugin, and that's because we simply don't
have to. We are guaranteed that seed will be a float and so the call to our
RNG's seed is guaranteed to work.
Lastly, of course, the wrap call. Again, read the wrap tutorial for fuller
coverage of its use, but the basic premise is that the second argument to wrap
is a list of converters that handles argument validation and conversion and it
then assigns values to each argument in the arg list after the first four
(required) arguments. So, our seed argument gets a float, guaranteed.
With this alone you'd be able to make some pretty usable plugin commands, but
we'll go through two more commands to introduce a few more useful ideas. The
next command we'll make is a sample command which gets a random sample of items
from a list provided by the user::
def sample(self, irc, msg, args, n, items):
"""<number of items> <item1> [<item2> ...]
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.
"""
if n > len(items):
irc.error('<number of items> must be less than the number '
'of arguments.')
return
sample = self.rng.sample(items, n)
sample.sort()
irc.reply(utils.str.commaAndify(sample))
sample = wrap(sample, ['int', many('anything')])
This plugin command introduces a few new things, but the general structure
should look fairly familiar by now. You may wonder why we only have two extra
arguments when obviously this plugin can accept any number of arguments. Well,
using wrap we collect all of the remaining arguments after the first one into
the items argument. If you haven't caught on yet, wrap is really cool and
extremely useful.
Next of course is the updated docstring. Note the use of [] to denote the
optional items after the first item.
The body of the plugin should be relatively easy to read. First we check and
make sure that n (the number of items the user wants to sample) is not larger
than the actual number of items they gave. If it does, we call irc.error with
the error message you see. irc.error is kind of like irc.replySuccess only it
gives an error message using the configured error format (in
supybot.replies.error). Otherwise, we use the sample function from our RNG to
get a sample, then we sort it, and we reply with the 'utils.str.commaAndify'ed
version. The utils.str.commaAndify function basically takes a list of strings
and turns it into "item1, item2, item3, item4, and item5" for an arbitrary
length. More details on using the utils module can be found in the utils
tutorial.
Now for the last command that we will add to our plugin.py. This last command
will allow the bot users to roll an arbitrary n-sided die, with as many sides
as they so choose. Here's the code for this command::
def diceroll(self, irc, msg, args, n):
"""[<number of sides>]
Rolls a die with <number of sides> sides. The default number of sides
is 6.
"""
s = 'rolls a %s' % self.rng.randrange(1, n)
irc.reply(s, action=True)
diceroll = wrap(diceroll, [additional(('int', 'number of sides'), 6)])
The only new thing learned here really is that the irc.reply method accepts an
optional argument action, which if set to True makes the reply an action
instead. So instead of just crudely responding with the number, instead you
should see something like * supybot rolls a 5. You'll also note that it uses a
more advanced wrap line than we have used to this point, but to learn more
about wrap, you should refer to the wrap tutorial
And now that we're done adding plugin commands you should see the boilerplate
stuff at the bottom, which just consists of::
Class = Random
And also some vim modeline stuff. Leave these as is, and we're finally done
with plugin.py!
test.py
=======
Now that we've gotten our plugin written, we want to make sure it works. Sure,
an easy way to do a somewhat quick check is to start up a bot, load the plugin,
and run a few commands on it. If all goes well there, everything's probably
okay. But, we can do better than "probably okay". This is where written plugin
tests come in. We can write tests that not only assure that the plugin loads
and runs the commands fine, but also that it produces the expected output for
given inputs. And not only that, we can use the nifty supybot-test script to
test the plugin without even having to have a network connection to connect to
IRC with and most certainly without running a local IRC server.
The boilerplate code for test.py is a good start. It imports everything you
need and sets up RandomTestCase which will contain all of our tests. Now we
just need to write some test methods. I'll be moving fairly quickly here just
going over very basic concepts and glossing over details, but the full plugin
test authoring tutorial has much more detail to it and is recommended reading
after finishing this tutorial.
Since we have four commands we should have at least four test methods in our
test case class. Typically you name the test methods that simply checks that a
given command works by just appending the command name to test. So, we'll have
testRandom, testSeed, testSample, and testDiceRoll. Any other methods you want
to add are more free-form and should describe what you're testing (don't be
afraid to use long names).
First we'll write the testRandom method::
def testRandom(self):
# difficult to test, let's just make sure it works
self.assertNotError('random')
Since we can't predict what the output of our random number generator is going
to be, it's hard to specify a response we want. So instead, we just make sure
we don't get an error by calling the random command, and that's about all we
can do.
Next, testSeed. In this method we're just going to check that the command
itself functions. In another test method later on we will check and make sure
that the seed produces reproducible random numbers like we would hope it would,
but for now we just test it like we did random in 'testRandom'::
def testSeed(self):
# just make sure it works
self.assertNotError('seed 20')
Now for testSample. Since this one takes more arguments it makes sense that we
test more scenarios in this one. Also this time we have to make sure that we
hit the error that we coded in there given the right conditions::
def testSample(self):
self.assertError('sample 20 foo')
self.assertResponse('sample 1 foo', 'foo')
self.assertRegexp('sample 2 foo bar', '... and ...')
self.assertRegexp('sample 3 foo bar baz', '..., ..., and ...')
So first we check and make sure trying to take a 20-element sample of a
1-element list gives us an error. Next we just check and make sure we get the
right number of elements and that they are formatted correctly when we give 1,
2, or 3 element lists.
And for the last of our basic "check to see that it works" functions,
testDiceRoll::
def testDiceRoll(self):
self.assertActionRegexp('diceroll', 'rolls a \d')
We know that diceroll should return an action, and that with no arguments it
should roll a single-digit number. And that's about all we can test reliably
here, so that's all we do.
Lastly, we wanted to check and make sure that seeding the RNG with seed
actually took effect like it's supposed to. So, we write another test method::
def testSeedActuallySeeds(self):
# now to make sure things work repeatably
self.assertNotError('seed 20')
m1 = self.getMsg('random')
self.assertNotError('seed 20')
m2 = self.getMsg('random')
self.failUnlessEqual(m1, m2)
m3 = self.getMsg('random')
self.failIfEqual(m2, m3)
So we seed the RNG with 20, store the message, and then seed it at 20 again. We
grab that message, and unless they are the same number when we compare the two,
we fail. And then just to make sure our RNG is producing random numbers, we get
another random number and make sure it is distinct from the prior one.
Conclusion
==========
You are now very well-prepared to write Supybot plugins. Now for a few words of
wisdom with regards to Supybot plugin-writing.
* Read other people's plugins, especially the included plugins and ones by
the core developers. We (the Supybot dev team) can't possibly document
all the awesome things that Supybot plugins can do, but we try.
Nevertheless there are some really cool things that can be done that
aren't very well-documented.
* Hack new functionality into existing plugins first if writing a new
plugin is too daunting.
* Come ask us questions in #supybot on Freenode or OFTC. Going back to the
first point above, the developers themselves can help you even more than
the docs can (though we prefer you read the docs first).
* Share your plugins with the world and make Supybot all that more
attractive for other users so they will want to write their plugins for
Supybot as well.
* Read, read, read all the documentation.
* And of course, have fun writing your plugins.

View File

@ -1,213 +0,0 @@
================
Style Guidelines
================
**Note:** 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 ``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 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 % (i.e., str.__mod__)
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.
* While still on the topic of logs, it's also important to pick the
appropriate log level for given information.
* DEBUG: Appropriate to tell a programmer *how* we're doing something
(i.e., debugging printfs, basically). If you're trying to figure out why
your code doesn't work, DEBUG is the new printf -- use that, and leave the
statements in your code.
* INFO: Appropriate to tell a user *what* we're doing, when what we're
doing isn't important for the user to pay attention to. A user who likes
to keep up with things should enjoy watching our logging at the INFO
level; it shouldn't be too low-level, but it should give enough
information that it keeps them relatively interested at peak times.
* WARNING: Appropriate to tell a user when we're doing something that they
really ought to pay attention to. Users should see WARNING and think,
"Hmm, should I tell the Supybot developers about this?" Later, they should
decide not to, but it should give the user a moment to pause and think
about what's actually happening with their bot.
* ERROR: Appropriate to tell a user when something has gone wrong.
Uncaught exceptions are ERRORs. Conditions that we absolutely want to
hear about should be errors. Things that should *scare* the user should
be errors.
* CRITICAL: Not really appropriate. I can think of no absolutely critical
issue yet encountered in Supybot; the only possible thing I can imagine is
to notify the user that the partition on which Supybot is running has
filled up. That would be a CRITICAL condition, but it would also be hard
to log :)
* 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.
* Common variable names
- L => an arbitrary list.
- t => an arbitrary tuple.
- x => an arbitrary float.
- s => an arbitrary string.
- f => an arbitrary function.
- p => an arbitrary predicate.
- i,n => an arbitrary integer.
- cb => an arbitrary callback.
- db => a database handle.
- fd => a file-like object.
- msg => an ircmsgs.IrcMsg object.
- irc => an irclib.Irc object (or proxy)
- nick => a string that is an IRC nick.
- channel => a string that is an IRC channel.
- hostmask => a string that is a user's IRC prefix.
When the semantic functionality (that is, the "meaning" of a variable is
obvious from context), one of these names should be used. This just makes it
easier for people reading our code to know what a variable represents
without scouring the surrounding code.
* Multiple variable assignments should always be surrounded with parentheses
-- i.e., if you're using the partition function, then your assignment
statement should look like::
(good, bad) = partition(p, L)
The parentheses make it obvious that you're doing a multiple assignment, and
that's important because I hate reading code and wondering where a variable
came from.

View File

@ -1,379 +0,0 @@
============================
Using Supybot's utils module
============================
Supybot provides a wealth of utilities for plugin writers in the supybot.utils
module, this tutorial describes these utilities and shows you how to use them.
str.py
======
The Format Function
-------------------
The supybot.utils.str module provides a bunch of utility functions for
handling string values. This section contains a quick rundown of all of the
functions available, along with descriptions of the arguments they take. First
and foremost is the format function, which provides a lot of capability in
just one function that uses string-formatting style to accomplish a lot. So
much so that it gets its own section in this tutorial. All other functions
will be in other sections. format takes several arguments - first, the format
string (using the format characters described below), and then after that,
each individual item to be formatted. Do not attempt to use the % operator to
do the formatting because that will fall back on the normal string formatting
operator. The format function uses the following string formatting characters.
* % - literal ``%``
* i - integer
* s - string
* f - float
* r - repr
* b - form of the verb ``to be`` (takes an int)
* h - form of the verb ``to have`` (takes an int)
* L - commaAndify (takes a list of strings or a tuple of ([strings], and))
* p - pluralize (takes a string)
* q - quoted (takes a string)
* n - n items (takes a 2-tuple of (n, item) or a 3-tuple of (n, between, item))
* t - time, formatted (takes an int)
* u - url, wrapped in braces
Here are a few examples to help elaborate on the above descriptions::
>>> format("Error %q has been reported %n. For more information, see %u.",
"AttributeError", (5, "time"), "http://supybot.com")
'Error "AttributeError" has been reported 5 times. For more information,
see <http://supybot.com>.'
>>> i = 4
>>> format("There %b %n at this time. You are only allowed %n at any given
time", i, (i, "active", "thread"), (5, "active", "thread"))
'There are 4 active threads at this time. You are only allowed 5 active
threads at any given time'
>>> i = 1
>>> format("There %b %n at this time. You are only allowed %n at any given
time", i, (i, "active", "thread"), (5, "active", "thread"))
'There is 1 active thread at this time. You are only allowed 5 active
threads at any given time'
>>> ops = ["foo", "bar", "baz"]
>>> format("The following %n %h the %s capability: %L", (len(ops), "user"),
len(ops), "op", ops)
'The following 3 users have the op capability: foo, bar, and baz'
As you can see, you can combine all sorts of combinations of formatting
strings into one. In fact, that was the major motivation behind format. We
have specific functions that you can use individually for each of those
formatting types, but it became much easier just to use special formatting
chars and the format function than concatenating a bunch of strings that were
the result of other utils.str functions.
The Other Functions
-------------------
These are the functions that can't be handled by format. They are sorted in
what I perceive to be the general order of usefulness (and I'm leaving the
ones covered by format for the next section).
* ellipsisify(s, n) - Returns a shortened version of a string. Produces up to
the first n chars at the nearest word boundary.
- s: the string to be shortened
- n: the number of characters to shorten it to
* perlReToPythonRe(s) - Converts a Perl-style regexp (e.g., "/abcd/i" or
"m/abcd/i") to an actual Python regexp (an re object)
- s: the regexp string
* perlReToReplacer(s) - converts a perl-style replacement regexp (eg,
"s/foo/bar/g") to a Python function that performs such a replacement
- s: the regexp string
* dqrepr(s) - Returns a repr() of s guaranteed to be in double quotes.
(Double Quote Repr)
- s: the string to be double-quote repr()'ed
* toBool(s) - Determines whether or not a string means True or False and
returns the appropriate boolean value. True is any of "true", "on",
"enable", "enabled", or "1". False is any of "false", "off", "disable",
"disabled", or "0".
- s: the string to determine the boolean value for
* rsplit(s, sep=None, maxsplit=-1) - functionally the same as str.split in the
Python standard library except splitting from the right instead of the left.
Python 2.4 has str.rsplit (which this function defers to for those versions
>= 2.4), but Python 2.3 did not.
- s: the string to be split
- sep: the separator to split on, defaults to whitespace
- maxsplit: the maximum number of splits to perform, -1 splits all possible
splits.
* normalizeWhitespace(s) - reduces all multi-spaces in a string to a single
space
- s: the string to normalize
* depluralize(s) - the opposite of pluralize
- s: the string to depluralize
* unCommaThe(s) - Takes a string of the form "foo, the" and turns it into "the
foo"
- s: string, the
* distance(s, t) - computes the levenshtein distance (or "edit distance")
between two strings
- s: the first string
- t: the second string
* soundex(s, length=4) - computes the soundex for a given string
- s: the string to compute the soundex for
- length: the length of the soundex to generate
* matchCase(s1, s2) - Matches the case of the first string in the second
string.
- s1: the first string
- s2: the string which will be made to match the case of the first
The Commands Format Already Covers
----------------------------------
These commands aren't necessary because you can achieve them more easily by
using the format command, but they exist if you decide you want to use them
anyway though it is greatly discouraged for general use.
* commaAndify(seq, comma=",", And="and") - transforms a list of items into a
comma separated list with an "and" preceding the last element. For example,
["foo", "bar", "baz"] becomes "foo, bar, and baz". Is smart enough to
convert two-element lists to just "item1 and item2" as well.
- seq: the sequence of items (don't have to be strings, but need to be
'str()'-able)
- comma: the character to use to separate the list
- And: the word to use before the last element
* pluralize(s) - Returns the plural of a string. Put any exceptions to the
general English rules of pluralization in the plurals dictionary in
supybot.utils.str.
- s: the string to pluralize
* nItems(n, item, between=None) - returns a string that describes a given
number of an item (with any string between the actual number and the item
itself), handles pluralization with the pluralize function above. Note that
the arguments here are in a different order since between is optional.
- n: the number of items
- item: the type of item
- between: the optional string that goes between the number and the type of
item
* quoted(s) - Returns the string surrounded by double-quotes.
- s: the string to quote
* be(i) - Returns the proper form of the verb "to be" based on the number
provided (be(1) is "is", be(anything else) is "are")
- i: the number of things that "be"
* has(i) - Returns the proper form of the verb "to have" based on the number
provided (has(1) is "has", has(anything else) is "have")
- i: the number of things that "has"
structures.py
=============
Intro
-----
This module provides a number of useful data structures that aren't found in
the standard Python library. For the most part they were created as needed for
the bot and plugins themselves, but they were created in such a way as to be
of general use for anyone who needs a data structure that performs a like
duty. As usual in this document, I'll try and order these in order of
usefulness, starting with the most useful.
The queue classes
-----------------
The structures module provides two general-purpose queue classes for you to
use. The "queue" class is a robust full-featured queue that scales up to
larger sized queues. The "smallqueue" class is for queues that will contain
fewer (less than 1000 or so) items. Both offer the same common interface,
which consists of:
* a constructor which will optionally accept a sequence to start the queue off
with
* enqueue(item) - adds an item to the back of the queue
* dequeue() - removes (and returns) the item from the front of the queue
* peek() - returns the item from the front of the queue without removing it
* reset() - empties the queue entirely
In addition to these general-use queue classes, there are two other more
specialized queue classes as well. The first is the "TimeoutQueue" which holds
a queue of items until they reach a certain age and then they are removed from
the queue. It features the following:
* TimeoutQueue(timeout, queue=None) - you must specify the timeout (in
seconds) in the constructor. Note that you can also optionally pass it a
queue which uses any implementation you wish to use whether it be one of the
above (queue or smallqueue) or if it's some custom queue you create that
implements the same interface. If you don't pass it a queue instance to use,
it will build its own using smallqueue.
- reset(), enqueue(item), dequeue() - all same as above queue classes
- setTimeout(secs) - allows you to change the timeout value
And for the final queue class, there's the "MaxLengthQueue" class. As you may
have guessed, it's a queue that is capped at a certain specified length. It
features the following:
* MaxLengthQueue(length, seq=()) - the constructor naturally requires that you
set the max length and it allows you to optionally pass in a sequence to be
used as the starting queue. The underlying implementation is actually the
queue from before.
- enqueue(item) - adds an item onto the back of the queue and if it would
push it over the max length, it dequeues the item on the front (it does
not return this item to you)
- all the standard methods from the queue class are inherited for this class
The Other Structures
--------------------
The most useful of the other structures is actually very similar to the
"MaxLengthQueue". It's the "RingBuffer", which is essentially a MaxLengthQueue
which fills up to its maximum size and then circularly replaces the old
contents as new entries are added instead of dequeuing. It features the
following:
* RingBuffer(size, seq=()) - as with the MaxLengthQueue you specify the size
of the RingBuffer and optionally give it a sequence.
- append(item) - adds item to the end of the buffer, pushing out an item
from the front if necessary
- reset() - empties out the buffer entirely
- resize(i) - shrinks/expands the RingBuffer to the size provided
- extend(seq) - append the items from the provided sequence onto the end of
the RingBuffer
The next data structure is the TwoWayDictionary, which as the name implies is
a dictionary in which key-value pairs have mappings going both directions. It
features the following:
* TwoWayDictionary(seq=(), \**kwargs) - Takes an optional sequence of (key,
value) pairs as well as any key=value pairs specified in the constructor as
initial values for the two-way dict.
- other than that, no extra features that a normal Python dict doesn't
already offer with the exception that any (key, val) pair added to the
dict is also added as (val, key) as well, so the mapping goes both ways.
Elements are still accessed the same way you always do with Python
'dict's.
There is also a MultiSet class available, but it's very unlikely that it will
serve your purpose, so I won't go into it here. The curious coder can go check
the source and see what it's all about if they wish (it's only used once in our
code, in the Relay plugin).
web.py
======
The web portion of Supybot's utils module is mainly used for retrieving data
from websites but it also has some utility functions pertaining to HTML and
email text as well. The functions in web are listed below, once again in order
of usefulness.
* getUrl(url, size=None, headers=None) - gets the data at the URL provided and
returns it as one large string
- url: the location of the data to be retrieved or a urllib2.Request object
to be used in the retrieval
- size: the maximum number of bytes to retrieve, defaults to None, meaning
that it is to try to retrieve all data
- headers: a dictionary mapping header types to header data
* getUrlFd(url, headers=None) - returns a file-like object for a url
- url: the location of the data to be retrieved or a urllib2.Request object
to be used in the retrieval
- headers: a dictionary mapping header types to header data
* htmlToText(s, tagReplace=" ") - strips out all tags in a string of HTML,
replacing them with the specified character
- s: the HTML text to strip the tags out of
- tagReplace: the string to replace tags with
* strError(e) - pretty-printer for web exceptions, returns a descriptive
string given a web-related exception
- e: the exception to pretty-print
* mungeEmail(s) - a naive e-mail obfuscation function, replaces "@" with "AT"
and "." with "DOT"
- s: the e-mail address to obfuscate
* getDomain(url) - returns the domain of a URL
- url: the URL in question
The Best of the Rest
====================
Intro
-----
Rather than document each of the remaining portions of the supybot.utils
module, I've elected to just pick out the choice bits from specific parts and
document those instead. Here they are, broken out by module name.
supybot.utils.file - file utilities
-----------------------------------
* touch(filename) - updates the access time of a file by opening it for
writing and immediately closing it
* mktemp(suffix="") - creates a decent random string, suitable for a temporary
filename with the given suffix, if provided
* the AtomicFile class - used for files that need to be atomically written,
i.e., if there's a failure the original file remains unmodified. For more
info consult file.py in src/utils
supybot.utils.gen - general utilities
-------------------------------------
* timeElapsed(elapsed, [lots of optional args]) - given the number of seconds
elapsed, returns a string with the English description of the amount of time
passed, consult gen.py in src/utils for the exact argument list and
documentation if you feel you could use this function.
* exnToString(e) - improved exception-to-string function. Provides nicer
output than a simple str(e).
* InsensitivePreservingDict class - a dict class that is case-insensitive when
accessing keys
supybot.utils.iter - iterable utilities
---------------------------------------
* len(iterable) - returns the length of a given iterable
* groupby(key, iterable) - equivalent to the itertools.groupby function
available as of Python 2.4. Provided for backwards compatibility.
* any(p, iterable) - Returns true if any element in the iterable satisfies the
predicate p
* all(p, iterable) - Returns true if all elements in the iterable satisfy the
predicate p
* choice(iterable) - Returns a random element from the iterable

View File

@ -1,499 +0,0 @@
Using commands.wrap to parse your command's arguments.
------------------------------------------------------
This document illustrates how to use the new 'wrap' function present in Supybot
0.80 to handle argument parsing and validation for your plugin's commands.
Introduction
============
To plugin developers for older (pre-0.80) versions of Supybot, one of the more
annoying aspects of writing commands was handling the arguments that were
passed in. In fact, many commands often had to duplicate parsing and
verification code, resulting in lots of duplicated code for not a whole lot of
action. So, instead of forcing plugin writers to come up with their own ways of
cleaning it up, we wrote up the wrap function to handle all of it.
It allows a much simpler and more flexible way of checking things than before
and it doesn't require that you know the bot internals to do things like check
and see if a user exists, or check if a command name exists and whatnot.
If you are a plugin author this document is absolutely required reading, as it
will massively ease the task of writing commands.
Using Wrap
==========
First off, to get the wrap function, it is recommended (strongly) that you use
the following import line::
from supybot.commands import *
This will allow you to access the wrap command (and it allows you to do it
without the commands prefix). Note that this line is added to the imports of
plugin templates generated by the supybot-plugin-create script.
Let's write a quickie command that uses wrap to get a feel for how it makes our
lives better. Let's write a command that repeats a string of text a given
number of times. So you could say "repeat 3 foo" and it would say "foofoofoo".
Not a very useful command, but it will serve our purpose just fine. Here's how
it would be done without wrap::
def repeat(self, irc, msg, args):
"""<num> <text>
Repeats <text> <num> times.
"""
(num, text) = privmsg.getArgs(args, required=2)
try:
num = int(num)
except ValueError:
raise callbacks.ArgumentError
irc.reply(num * text)
Note that all of the argument validation and parsing takes up 5 of the 6 lines
(and you should have seen it before we had privmsg.getArgs!). Now, here's what
our command will look like with wrap applied::
def repeat(self, irc, msg, args, num, text):
"""<num> <text>
Repeats <text> <num> times.
"""
irc.reply(text * num)
repeat = wrap(repeat, ['int', 'text'])
Pretty short, eh? With wrap all of the argument parsing and validation is
handled for us and we get the arguments we want, formatted how we want them,
and converted into whatever types we want them to be - all in one simple
function call that is used to wrap the function! So now the code inside each
command really deals with how to execute the command and not how to deal with
the input.
So, now that you see the benefits of wrap, let's figure out what stuff we have
to do to use it.
Syntax Changes
==============
There are two syntax changes to the old style that are implemented. First, the
definition of the command function must be changed. The basic syntax for the
new definition is::
def commandname(self, irc, msg, args, <arg1>, <arg2>, ...):
Where arg1 and arg2 (up through as many as you want) are the variables that
will store the parsed arguments. "Now where do these parsed arguments come
from?" you ask. Well, that's where the second syntax change comes in. The
second syntax change is the actual use of the wrap function itself to decorate
our command names. The basic decoration syntax is::
commandname = wrap(commandname, [converter1, converter2, ...])
.. note::
This should go on the line immediately following the body of the command's
definition, so it can easily be located (and it obviously must go after the
command's definition so that commandname is defined).
Each of the converters in the above listing should be one of the converters in
commands.py (I will describe each of them in detail later.) The converters are
applied in order to the arguments given to the command, generally taking
arguments off of the front of the argument list as they go. Note that each of
the arguments is actually a string containing the NAME of the converter to use
and not a reference to the actual converter itself. This way we can have
converters with names like int and not have to worry about polluting the
builtin namespace by overriding the builtin int.
As you will find out when you look through the list of converters below, some
of the converters actually take arguments. The syntax for supplying them (since
we aren't actually calling the converters, but simply specifying them), is to
wrap the converter name and args list into a tuple. For example::
commandname = wrap(commandname, [(converterWithArgs, arg1, arg2),
converterWithoutArgs1, converterWithoutArgs2])
For the most part you won't need to use an argument with the converters you use
either because the defaults are satisfactory or because it doesn't even take
any.
Customizing Wrap
================
Converters alone are a pretty powerful tool, but for even more advanced (yet
simpler!) argument handling you may want to use contexts. Contexts describe how
the converters are applied to the arguments, while the converters themselves
do the actual parsing and validation.
For example, one of the contexts is "optional". By using this context, you're
saying that a given argument is not required, and if the supplied converter
doesn't find anything it likes, we should use some default. Yet another
example is the "reverse" context. This context tells the supplied converter to
look at the last argument and work backwards instead of the normal
first-to-last way of looking at arguments.
So, that should give you a feel for the role that contexts play. They are not
by any means necessary to use wrap. All of the stuff we've done to this point
will work as-is. However, contexts let you do some very powerful things in very
easy ways, and are a good thing to know how to use.
Now, how do you use them? Well, they are in the global namespace of
src/commands.py, so your previous import line will import them all; you can
call them just as you call wrap. In fact, the way you use them is you simply
call the context function you want to use, with the converter (and its
arguments) as arguments. It's quite simple. Here's an example::
commandname = wrap(commandname, [optional('int'), many('something')])
In this example, our command is looking for an optional integer argument first.
Then, after that, any number of arguments which can be anything (as long as
they are something, of course).
Do note, however, that the type of the arguments that are returned can be
changed if you apply a context to it. So, optional("int") may very well return
None as well as something that passes the "int" converter, because after all
it's an optional argument and if it is None, that signifies that nothing was
there. Also, for another example, many("something") doesn't return the same
thing that just "something" would return, but rather a list of "something"s.
Converter List
==============
Below is a list of all the available converters to use with wrap. If the
converter accepts any arguments, they are listed after it and if they are
optional, the default value is shown.
* id, kind="integer"
- Returns something that looks like an integer ID number. Takes an optional
"kind" argument for you to state what kind of ID you are looking for,
though this doesn't affect the integrity-checking. Basically requires that
the argument be an integer, does no other integrity-checking, and provides
a nice error message with the kind in it.
* ip
- Checks and makes sure the argument looks like a valid IP and then returns
it.
* int, type="integer", p=None
- Gets an integer. The "type" text can be used to customize the error message
received when the argument is not an integer. "p" is an optional predicate
to test the integer with. If p(i) fails (where i is the integer arg parsed
out of the argument string), the arg will not be accepted.
* index
- Basically ("int", "index"), but with a twist. This will take a 1-based
index and turn it into a 0-based index (which is more useful in code). It
doesn't transform 0, and it maintains negative indices as is (note that it
does allow them!).
* color
- Accepts arguments that describe a text color code (e.g., "black", "light
blue") and returns the mIRC color code for that color. (Note that many
other IRC clients support the mIRC color code scheme, not just mIRC)
* now
- Simply returns the current timestamp as an arg, does not reference or
modify the argument list.
* url
- Checks for a valid URL.
* httpUrl
- Checks for a valid HTTP URL.
* email
- Checks for a syntactically valid email address.
* long, type="long"
- Basically the same as int minus the predicate, except that it converts the
argument to a long integer regardless of the size of the int.
* float, type="floating point number"
- Basically the same as int minus the predicate, except that it converts the
argument to a float.
* nonInt, type="non-integer value"
- Accepts everything but integers, and returns them unchanged. The "type"
value, as always, can be used to customize the error message that is
displayed upon failure.
* positiveInt
- Accepts only positive integers.
* nonNegativeInt
- Accepts only non-negative integers.
* letter
- Looks for a single letter. (Technically, it looks for any one-element
sequence).
* haveOp, action="do that"
- Simply requires that the bot have ops in the channel that the command is
called in. The action parameter completes the error message: "I need to be
opped to ...".
* expiry
- Takes a number of seconds and adds it to the current time to create an
expiration timestamp.
* literal, literals, errmsg=None
- Takes a required sequence or string (literals) and any argument that
uniquely matches the starting substring of one of the literals is
transformed into the full literal. For example, with ``("literal", ("bar",
"baz", "qux"))``, you'd get "bar" for "bar", "baz" for "baz", and "qux"
for any of "q", "qu", or "qux". "b" and "ba" would raise errors because
they don't uniquely identify one of the literals in the list. You can
override errmsg to provide a specific (full) error message, otherwise the
default argument error message is displayed.
* to
- Returns the string "to" if the arg is any form of "to" (case-insensitive).
* nick
- Checks that the arg is a valid nick on the current IRC server.
* seenNick
- Checks that the arg is a nick that the bot has seen (NOTE: this is limited
by the size of the history buffer that the bot has).
* channel
- Gets a channel to use the command in. If the channel isn't supplied, uses
the channel the message was sent in. If using a different channel, does
sanity-checking to make sure the channel exists on the current IRC network.
* inChannel
- Requires that the command be called from within any channel that the bot
is currently in or with one of those channels used as an argument to the
command.
* onlyInChannel
- Requires that the command be called from within any channel that the bot
is currently in.
* nickInChannel
- Requires that the argument be a nick that is in the current channel, and
returns that nick.
* networkIrc, errorIfNoMatch=False
- Returns the IRC object of the specified IRC network. If one isn't
specified, the IRC object of the IRC network the command was called on is
returned.
* callerInGivenChannel
- Takes the given argument as a channel and makes sure that the caller is in
that channel.
* plugin, require=True
- Returns the plugin specified by the arg or None. If require is True, an
error is raised if the plugin cannot be retrieved.
* boolean
- Converts the text string to a boolean value. Acceptable true values are:
"1", "true", "on", "enable", or "enabled" (case-insensitive). Acceptable
false values are: "0", false", "off", "disable", or "disabled"
(case-insensitive).
* lowered
- Returns the argument lowered (NOTE: it is lowered according to IRC
conventions, which does strange mapping with some punctuation characters).
* anything
- Returns anything as is.
* something, errorMsg=None, p=None
- Takes anything but the empty string. errorMsg can be used to customize the
error message. p is any predicate function that can be used to test the
validity of the input.
* filename
- Used to get a filename argument.
* commandName
- Returns the canonical command name version of the given string (ie, the
string is lowercased and dashes and underscores are removed).
* text
- Takes the rest of the arguments as one big string. Note that this differs
from the "anything" context in that it clobbers the arg string when it's
done. Using any converters after this is most likely incorrect.
* glob
- Gets a glob string. Basically, if there are no wildcards (``*``, ``?``) in
the argument, returns ``*string*``, making a glob string that matches
anything containing the given argument.
* somethingWithoutSpaces
- Same as something, only with the exception of disallowing spaces of course.
* capability
- Used to retrieve an argument that describes a capability.
* channelDb
- Sets the channel appropriately in order to get to the databases for that
channel (handles whether or not a given channel uses channel-specific
databases and whatnot).
* hostmask
- Returns the hostmask of any provided nick or hostmask argument.
* banmask
- Returns a generic banmask of the provided nick or hostmask argument.
* user
- Requires that the caller be a registered user.
* matches, regexp, errmsg
- Searches the args with the given regexp and returns the matches. If no
match is found, errmsg is given.
* public
- Requires that the command be sent in a channel instead of a private
message.
* private
- Requires that the command be sent in a private message instead of a
channel.
* otherUser
- Returns the user specified by the username or hostmask in the argument.
* regexpMatcher
- Gets a matching regexp argument (m// or //).
* validChannel
- Gets a channel argument once it makes sure it's a valid channel.
* regexpReplacer
- Gets a replacing regexp argument (s//).
* owner
- Requires that the command caller has the "owner" capability.
* admin
- Requires that the command caller has the "admin" capability.
* checkCapability, capability
- Checks to make sure that the caller has the specified capability.
* checkChannelCapability, capability
- Checks to make sure that the caller has the specified capability on the
channel the command is called in.
* op
- Checks whether the user has the op mode (+o) set.
* halfop
- Checks whether the user has the halfop mode (+h) set.
* voice
- Checks whether the user has the voice mode (+v) set.
Contexts List
=============
What contexts are available for me to use?
The list of available contexts is below. Unless specified otherwise, it can be
assumed that the type returned by the context itself matches the type of the
converter it is applied to.
any
Looks for any number of arguments matching the supplied converter. Will
return a sequence of converted arguments or None.
many
Looks for multiple arguments matching the supplied converter. Expects at
least one to work, otherwise it will fail. Will return the sequence of
converted arguments.
optional
Look for an argument that satisfies the supplied converter, but if it's not
the type I'm expecting or there are no arguments for us to check, then use
the default value. Will return the converted argument as is or None.
additional
Look for an argument that satisfies the supplied converter, making sure
that it's the right type. If there aren't any arguments to check, then use
the default value. Will return the converted argument as is or None.
rest
Treat the rest of the arguments as one big string, and then convert. If the
conversion is unsuccessful, restores the arguments.
getopts
Handles --option style arguments. Each option should be a key in a
dictionary that maps to the name of the converter that is to be used on
that argument. To make the option take no argument, use "" as the converter
name in the dictionary. For no conversion, use None as the converter name
in the dictionary.
first
Tries each of the supplied converters in order and returns the result of
the first successfully applied converter.
reverse
Reverse the argument list, apply the converters, and then reverse the
argument list back.
commalist
Looks for a comma separated list of arguments that match the supplied
converter. Returns a list of the successfully converted arguments. If any
of the arguments fail, this whole context fails.
Final Word
==========
Now that you know how to use wrap, and you have a list of converters and
contexts you can use, your task of writing clean, simple, and safe plugin code
should become much easier. Enjoy!

View File

@ -1,195 +0,0 @@
# -*- coding: utf-8 -*-
#
# Supybot documentation build configuration file, created by
# sphinx-quickstart on Sat Feb 27 12:42:30 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('../src'))
#sys.path.append(os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Supybot'
copyright = u'2010, Jeremiah Fincher and James McCoy'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.83.4.1'
# The full version, including alpha/beta/rc tags.
release = '0.83.4.1+git+fr3'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['supybot']
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'Supybotdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Supybot.tex', u'Supybot Documentation',
u'Jeremiah Fincher and James McCoy', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True

View File

@ -1,24 +0,0 @@
.. Supybot documentation master file, created by
sphinx-quickstart on Sat Feb 27 12:42:30 2010.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Supybot's documentation!
===================================
Contents:
.. toctree::
:maxdepth: 0
:glob:
*
plugins
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -1,42 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-adduser.1
.\"
.TH SUPYBOT-ADDUSER 1 "APRIL 2005"
.SH NAME
supybot-adduser \- Adds a user to a Supybot users.conf file
.SH SYNOPSIS
.B supybot-adduser
.RI [ options ] " users.conf
.SH DESCRIPTION
.B supybot-adduser
adds a user to the specified users.conf file.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-u " NAME" "\fR,\fP \-\^\-username=" NAME
Specifies the username to use for the new user.
.TP
.BR \-p " PASSWORD" "\fR,\fP \-\^\-password=" PASSWORD
Specifies the password to use for the new user.
.TP
.BR \-c " CAPABILITY" "\fR,\fP \-\^\-capability=" CAPABILITY
Capability the user should have; this option may be given
multiple times.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot-test (1),
.IR supybot-botchk (1),
.IR supybot-wizard (1),
.IR supybot-plugin-doc (1),
.IR supybot-plugin-create (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

View File

@ -1,54 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-botchk.1
.\"
.TH SUPYBOT-BOTCHK 1 "APRIL 2005"
.SH NAME
supybot-botchk \- A script to start Supybot if it's not already running.
.SH SYNOPSIS
.B supybot-botchk
.RI [ options ]
.SH DESCRIPTION
.B supybot-botchk
is a script that will start Supybot if it detects that one is not currently
running. This can be useful for scheduling
.IR supybot (1)
to run via
.IR cron (8).
.SH OPTIONS
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-v ", " \-\^\-verbose
Use verbose output when running the script.
.TP
.BI \-\^\-botdir= BOTDIR
Determines which directory the bot be started in.
.TP
.BI \-\^\-pidfile= PIDFILE
Specifies the name of the pidfile to look for. This should be relative
to the given botdir.
.TP
.BI \-\^\-supybot= SUPYBOT
Specifies the location of
.IR supybot (1).
If this is not given, it is assumed that
.IR supybot (1)
is in the user's $PATH.
.TP
.BI \-\^\-conffile= CONFFILE
Specifies the path to the bot's configuration file. This will be used
when (re)starting the bot.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot-test (1),
.IR supybot-wizard (1),
.IR supybot-adduser (1),
.IR supybot-plugin-doc (1),
.IR supybot-plugin-create (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

View File

@ -1,43 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-plugin-create.1
.\"
.TH SUPYBOT-PLUGIN-CREATE 1 "APRIL 2005"
.SH NAME
supybot-plugin-create \- A wizard for creating Supybot plugins
.SH SYNOPSIS
.B supybot-plugin-create
.RI [ options ]
.SH DESCRIPTION
.B supybot-plugin-create
is a wizard that creates a template python source file for a new
.IR supybot (1)
plugin.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BI \-n " NAME" "\fR,\fP \-\^\-name=" NAME
Sets the name for the plugin.
.TP
.BR \-t ", " \-\^\-thread
Makes the plugin threaded.
.TP
.BI \-\^\-real\-name= REALNAME
Specify what real name the copyright is assigned to.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot-test (1),
.IR supybot-botchk (1),
.IR supybot-wizard (1),
.IR supybot-adduser (1),
.IR supybot-plugin-doc (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

View File

@ -1,48 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-plugin-doc.1
.\"
.TH SUPYBOT-PLUGIN-DOC 1 "May 2009"
.SH NAME
supybot-plugin-doc \- Generates the documentation for a Supybot plugin
.SH SYNOPSIS
.B supybot-plugin-doc
.RI [ options ]
.SH DESCRIPTION
.B supybot-plugin-doc
is used to generate documentation (StructuredText or reStructuredText format)
for a
.IR supybot (1)
plugin.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-c ", " \-\^\-clean
Clean the various data/conf/log directories after generating the docs.
.TP
.BR \-o ", " \-\^\-output\-dir= \fIOUTPUTDIR
Specifies the directory in which to write the documentation for the plugin.
.TP
.BR \-f ", " \-\^\-format= \fIFORMAT
Specifies which output format to use. Choices are 'rst' or 'stx'.
.TP
.BI \-\^\-plugins\-dir= PLUGINSDIRS
Looks in the given directory for plugins and generates documentation for all of
them.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot-test (1),
.IR supybot-botchk (1),
.IR supybot-wizard (1),
.IR supybot-adduser (1),
.IR supybot-plugin-create (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

View File

@ -1,51 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-test.1
.\"
.TH SUPYBOT-TEST 1 "OCTOBER 2005"
.SH NAME
supybot-test \- Runs the test suite for a Supybot plugin
.SH SYNOPSIS
.B supybot-test
.RI [ options ] " plugins
.SH DESCRIPTION
.B supybot-test
Runs the test suite for a Supybot plugin
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-c ", " \-\^\-clean
Cleans the various data/conf/logs directories before running tests.
.TP
.BR \-t " TIMEOUT" "\fR,\fP \-\^\-timeout=" TIMEOUT
Specifies the timeout for tests to return responses.
.TP
.BR \-v ", " \-\^\-verbose
Sets the verbose flag, logging extra information about each test that runs.
.TP
.BR \-\^\-no\-network
Prevents the network-based tests from being run.
.TP
.BR \-\^\-trace
Traces all calls made. Unless you're really in a pinch, you probably
shouldn't do this; it results in copious amounts of output.
.TP
.BR "\fR,\fP \-\^\-plugins\-dir=" PLUGINSDIR
Looks in the given directory for plugins and loads the tests for all of them.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot-botchk (1),
.IR supybot-wizard (1),
.IR supybot-adduser (1),
.IR supybot-plugin-doc (1),
.IR supybot-plugin-create (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

View File

@ -1,42 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot-wizard.1
.\"
.TH SUPYBOT-WIZARD 1 "SEPTEMBER 2004"
.SH NAME
supybot-wizard \- A wizard for creating Supybot configuration files
.SH SYNOPSIS
.B supybot-wizard
.RI [ options ]
.SH DESCRIPTION
.B supybot-wizard
is an in-depth wizard that provides a nice user interface for creating
configuration files for
.IR supybot (1).
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.B \-\^\-allow\-root
Determines whether the wizard will be allowed to run as root. You do not
want this. Do not do it. Even if you think you want it, you do not.
.TP
.B \-\^\-no\-network
Determines whether the wizard will be allowed to run without a network
connection.
.SH "SEE ALSO"
.IR python (1),
.IR supybot (1),
.IR supybot-test (1),
.IR supybot-botchk (1),
.IR supybot-adduser (1),
.IR supybot-plugin-doc (1),
.IR supybot-plugin-create (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

View File

@ -1,66 +0,0 @@
.\" Process this file with
.\" groff -man -Tascii supybot.1
.\"
.TH SUPYBOT 1 "JULY 2009"
.SH NAME
supybot - A robust and user friendly Python IRC bot
.SH SYNOPSIS
.B supybot
.RI [ options ] " configFile
.SH DESCRIPTION
.B Supybot
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 powerful ACL system for controlling access
to commands, as well as more than 50 builtin plugins providing around
400 actual commands.
.SH OPTIONS
.TP
.B \-\^\-version
Show version of program.
.TP
.BR \-h ", " \-\^\-help
Show summary of options.
.TP
.BR \-P ", " \-\^\-profile
Enable profiling.
.TP
.BI \-n " NICK" "\fR,\fP \-\^\-nick=" NICK
Nick the bot should use.
.TP
.BI \-u " USER" "\fR,\fP \-\^\-user=" USER
Full username the bot should use.
.TP
.BI \-i " IDENT" "\fR,\fP \-\^\-ident=" IDENT
Ident the bot should use.
.TP
.BR \-d ", " \-\^\-daemon
Determines whether the bot will daemonize. This is a no-op on
non-POSIX systems.
.TP
.B \-\^\-allow\-default\-owner
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 to
enable it.
.TP
.B \-\^\-allow\-root
Determines whether the bot will be allowed to run as root. You do not
want this. Do not do it. Even if you think you want it, you do not.
.TP
.B \-\^\-debug
Determines whether some extra debugging stuff will be logged by this
script.
.SH "SEE ALSO"
.IR python (1),
.IR supybot-test (1),
.IR supybot-botchk (1),
.IR supybot-wizard (1),
.IR supybot-adduser (1),
.IR supybot-plugin-doc (1),
.IR supybot-plugin-create (1)
.SH AUTHOR
This manual page was originally written by James McCoy
<vega dot james at gmail dot com>. Permission is granted to copy,
distribute and/or modify this document under the terms of the Supybot
license, a BSD-style license.

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
# -*- encoding: utf8 -*-
###
# Copyright (c) 2010, Valentin Lorentz
# 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.
###
"""
Supybot utility functions localization in French.
"""
def pluralize(s):
"""Returns the plural of s.
"""
lowered = s.lower()
if lowered.endswith('ou') and \
lowered in ['bijou', 'caillou', 'chou', 'genou', 'hibou', 'joujou',
'pou']:
return s + 'x'
elif lowered.endswith('al') and \
lowered not in ['bal', 'carnaval', 'chacal', 'festival', 'récital',
'régal', 'cal', 'étal', 'aval', 'caracal', 'val', 'choral',
'corral', 'galgal', 'gayal']:
return s[0:-2] + 'aux'
elif lowered.endswith('ail') and \
lowered not in ['bail', 'corail', 'émail', 'soupirail', 'travail',
'ventail', 'vitrail', 'aspirail', 'fermail']:
return s[0:-3] + 'aux'
elif lowered.endswith('eau'):
return s + 'x'
elif lowered == 'pare-feu':
return s
elif lowered.endswith('eu') and \
lowered not in ['bleu', 'pneu', 'émeu', 'enfeu']:
# Note: when 'lieu' is a fish, it has a 's' ; else, it has a 'x'
return s + 'x'
else:
return s + 's'
def depluralize(s):
"""Returns the singular of s."""
lowered = s.lower()
if lowered.endswith('aux') and \
lowered in ['baux', 'coraux', 'émaux', 'soupiraux', 'travaux',
'ventaux', 'vitraux', 'aspiraux', 'fermaux']:
return s[0:-3] + 'ail'
elif lowered.endswith('aux'):
return s[0:-3] + 'al'
else:
return s[0:-1]
def ordinal(i):
"""Returns i + the ordinal indicator for the number.
Example: ordinal(3) => '3ème'
"""
i = int(i)
if i == 1:
return '1er'
else:
return '%sème' % i
def be(i):
"""Returns the form of the verb 'être' based on the number i."""
# Note: this function is used only for the third person
if i == 1:
return 'est'
else:
return 'sont'
def has(i):
"""Returns the form of the verb 'avoir' based on the number i."""
# Note: this function is used only for the third person
if i == 1:
return 'a'
else:
return 'ont'

File diff suppressed because it is too large Load Diff

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,56 +0,0 @@
###
# Copyright (c) 2004-2005, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
These are commands useful for administrating the bot; they all require their
caller to have the 'admin' capability. This plugin is loaded by default.
"""
import supybot
import supybot.world as world
__author__ = supybot.authors.jemfinch
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you\'re keeping the plugin in CVS or some similar system.
__version__ = "%%VERSION%%"
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
import config
import plugin
reload(plugin) # In case we're being reloaded.
if world.testing:
import test
Class = plugin.Class
configure = config.configure

View File

@ -1,50 +0,0 @@
###
# Copyright (c) 2004-2005, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Admin')
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Admin', True)
Admin = conf.registerPlugin('Admin')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(Admin, 'someConfigVariableName',
# registry.Boolean(False, """Help for someConfigVariableName."""))
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,241 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Supybot\n"
"POT-Creation-Date: 2014-08-17 13:46+CEST\n"
"PO-Revision-Date: 2011-10-31 13:37+0100\n"
"Last-Translator: Florian Besser <fbesser@gmail.com>\n"
"Language-Team: German <fbesser@gmail.com>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Poedit-Language: German\n"
"X-Poedit-Country: Germany\n"
#: plugin.py:54
msgid "Nick/channel temporarily unavailable."
msgstr "Nick/Kanal temporär nicht verfügbar."
#: plugin.py:72
msgid "Cannot join %s, it's full."
msgstr "Kann %s nicht beitreten, der Kanal ist voll."
#: plugin.py:80
msgid "Cannot join %s, I was not invited."
msgstr "Kann %s nicht beitreten, ich wurde nicht eingeladen."
#: plugin.py:88
msgid "Cannot join %s, I am banned."
msgstr "Ich kann %s nicht betreten, ich bin gebannt."
#: plugin.py:96
msgid "Cannot join %s, my keyword was wrong."
msgstr "Ich kann %s nicht beitreten, mein Schlüsselwort ist falsch."
#: plugin.py:104 plugin.py:113
msgid "Cannot join %s, I'm not identified with NickServ."
msgstr "Ich kann %s nicht betreten, ich bin nicht mit NickServ identifiziert."
#: plugin.py:143
msgid ""
"<channel> [<key>]\n"
"\n"
" Tell the bot to join the given channel. If <key> is given, it is "
"used\n"
" when attempting to join the channel.\n"
" "
msgstr ""
"<Kanal> [<Schlüssel>]\n"
"\n"
"Sagt dem Bot dem angegeben Kanal beizutreten. Falls <Schlüssel> angegeben "
"wird, wird dieser benutzt um zu versuchen den Kanal zu betreten."
#: plugin.py:156
msgid "I'm already too close to maximum number of channels for this network."
msgstr "Ich bin schon zu nah an den maximalen Kanälen für dieses Netzwerk."
#: plugin.py:165
msgid ""
"takes no arguments\n"
"\n"
" Returns the channels the bot is on. Must be given in private, in "
"order\n"
" to protect the secrecy of secret channels.\n"
" "
msgstr ""
"hat keine Argumenten\n"
"\n"
"Gibt die Kanäle aus in denen der Bot sich befindet. Dieser Befehl muss "
"privat gegeben werden, um das Geheimnis der geheimen Kanale zu wahren."
#: plugin.py:175
msgid "I'm not currently in any channels."
msgstr "Momentan bin ich in keinen Kanälen."
#: plugin.py:181
msgid "My connection is restricted, I can't change nicks."
msgstr "Meine Verbindung ist begrenzt, I kann meinen Nick nicht wechseln."
#: plugin.py:188
msgid "Someone else is already using that nick."
msgstr "Jemand anderes benutzt diesen Nick schon."
#: plugin.py:195
#, fuzzy
msgid "I can't change nick, I'm currently banned in %s."
msgstr "Ich kann meinen Nick nicht ändern, der Server sagte %q."
#: plugin.py:203
msgid "I can't change nicks, the server said %q."
msgstr "Ich kann meinen Nick nicht ändern, der Server sagte %q."
#: plugin.py:217
#, fuzzy
msgid ""
"[<nick>] [<network>]\n"
"\n"
" Changes the bot's nick to <nick>. If no nick is given, returns the\n"
" bot's current nick.\n"
" "
msgstr ""
"[<Nick>]\n"
"\n"
"Ändert den Nick des Bots zu <Nick>. Falls <Nick> nicht angegeben wird, wird "
"der momentane Botnick zurückgegeben."
#: plugin.py:234
msgid ""
"[<channel>] [<reason>]\n"
"\n"
" Tells the bot to part the list of channels you give it. <channel> "
"is\n"
" only necessary if you want the bot to part a channel other than the\n"
" current channel. If <reason> is specified, use it as the part\n"
" message.\n"
" "
msgstr ""
"[<Kanal>] [<Grund>]\n"
"\n"
"Sagt dem Bot die Liste von angebenen Kanälen zu verlassen. <Kanal> ist nur "
"notwendig, falls der Bot einen anderen Kanal als den Momentanen verlassen "
"soll. Falls <Grund> angegeben wird, wird dies als Verlassensnachricht "
"verwendet."
#: plugin.py:252
msgid "I'm not in %s."
msgstr "Ich bin nicht in %s."
#: plugin.py:264
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Gives the user specified by <name> (or the user to whom "
"<hostmask>\n"
" currently maps) the specified capability <capability>\n"
" "
msgstr ""
"<Name|Hostmaske> <Fähigkeit>\n"
"\n"
"Gibt dem angebenen Benutzer <Name> (oder dem auf den die <Hostmaske> "
"zutrifft) die angegebene Fähigkeit."
#: plugin.py:284
msgid ""
"The \"owner\" capability can't be added in the bot. Use the supybot-adduser "
"program (or edit the users.conf file yourself) to add an owner capability."
msgstr ""
"Die \"owner\" Fähigkeit kann nicht über den Bot hinzugefügt werden. Benutze "
"das supybot-adduser Programm (oder verändere users.conf per Hand) um die "
"Besitzer Fähigkeit hinzuzufügen."
#: plugin.py:295
msgid "You can't add capabilities you don't have."
msgstr "Du kannst keine Fähigkeiten hinzufügen, die du nicht hast."
#: plugin.py:300
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Takes from the user specified by <name> (or the user to whom\n"
" <hostmask> currently maps) the specified capability "
"<capability>\n"
" "
msgstr ""
"<Name|Hostmaske> <Fähigkeit>\n"
"\n"
"Nimmt dem Benutzer der durch <Name> (oder dem Benutzer auf den momentan "
"<Hostmaske> zeigt) angeben wird die angegeben Fähigkeit <Fähigkeit>."
#: plugin.py:312
msgid "That user doesn't have that capability."
msgstr "Der Benutzer hat diese Fähigkeit nicht."
#: plugin.py:314
msgid "You can't remove capabilities you don't have."
msgstr "Du kannst keine Fähigkeiten entfernen, die du nicht hast."
#: plugin.py:322
msgid ""
"<hostmask|nick> [<expires>]\n"
"\n"
" This will set a persistent ignore on <hostmask> or the hostmask\n"
" currently associated with <nick>. <expires> is an optional "
"argument\n"
" specifying when (in \"seconds from now\") the ignore will "
"expire; if\n"
" it isn't given, the ignore will never automatically expire.\n"
" "
msgstr ""
"<Hostmaske|Nick> [<Ablaufzeitpunkt>]\n"
"\n"
"Es wird eine beständige Ignorierung auf <Hostmaske> oder auf die Hostmaske "
"die momentan mit <Nick> verbunden wird gesetzt. <Ablaufzeitpunkt> ist "
"optional, das legt fest wann die Ignorierung abläuft;falls dies nicht "
"angegeben wird, wird die Ignorierung niemals ablaufen."
#: plugin.py:335
msgid ""
"<hostmask|nick>\n"
"\n"
" This will remove the persistent ignore on <hostmask> or the\n"
" hostmask currently associated with <nick>.\n"
" "
msgstr ""
"<Hostmaske|Nick>\n"
"\n"
"Wird die beständige Ignorierung, von <Hostmaske> oder der Hostmaske die "
"momentan mit dem <Nick> verbunden wird, aufheben."
#: plugin.py:344
msgid "%s wasn't in the ignores database."
msgstr "%s war nicht in der Datenbank für Ignorierungen."
#: plugin.py:349
msgid ""
"takes no arguments\n"
"\n"
" Lists the hostmasks that the bot is ignoring.\n"
" "
msgstr ""
"hat keine Argumente\n"
"\n"
"Listet die Hostmasken auf, die der Bot ignoriert."
#: plugin.py:357
msgid "I'm not currently globally ignoring anyone."
msgstr "Momentan ignoriere ich niemanden global."
#: plugin.py:361
msgid ""
"takes no arguments\n"
"\n"
" Clears the current send queue for this network.\n"
" "
msgstr ""
"hat keine Argumente\n"
"\n"
"Leert die momentane Sendenwarteschlange für dieses Netzwerk."
#~ msgid "That nick is currently banned."
#~ msgstr "Dieser Nick ist momentan gebannt."

View File

@ -1,261 +0,0 @@
# Admin plugin in Limnoria.
# Copyright (C) 2011 Limnoria
# Mikaela Suomalainen <mikaela.suomalainen@outlook.com>, 2011.
#
msgid ""
msgstr ""
"Project-Id-Version: Finnish translation of Admin plugin in Supybot\n"
"POT-Creation-Date: 2014-08-17 13:46+CEST\n"
"PO-Revision-Date: 2014-03-22 12:46+0200\n"
"Last-Translator: Mikaela Suomalainen <mikaela.suomalainen@outlook.com>\n"
"Language-Team: \n"
"Language: fi_FI\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 1.5.4\n"
#: plugin.py:54
msgid "Nick/channel temporarily unavailable."
msgstr "Nimimerkki/kanava on väliaikaisesti saavuttamattomissa."
#: plugin.py:72
msgid "Cannot join %s, it's full."
msgstr "Ei voida liittyä kanavalle %s, se on täynnä."
#: plugin.py:80
msgid "Cannot join %s, I was not invited."
msgstr "Ei voi liittyä kanavalle %s, minua ei ole kutsuttu."
#: plugin.py:88
msgid "Cannot join %s, I am banned."
msgstr "Ei voi liittyä kanavalle %s, se on antanut minulle porttikiellon."
#: plugin.py:96
msgid "Cannot join %s, my keyword was wrong."
msgstr "En voi liittyä kanavalle %s, minun avainsana oli väärä."
#: plugin.py:104 plugin.py:113
msgid "Cannot join %s, I'm not identified with NickServ."
msgstr "En voi liittyä kanavalle %s, koska en ole tunnistautunut NickServille."
#: plugin.py:143
msgid ""
"<channel> [<key>]\n"
"\n"
" Tell the bot to join the given channel. If <key> is given, it is "
"used\n"
" when attempting to join the channel.\n"
" "
msgstr ""
"<kanava> [<avain>]\n"
"\n"
" Käskee botin liittyä annetulle kanavalle. Jos <avain> on annettu, "
"sitä käytetään\n"
" yrittäessä liittyä kanavalle.\n"
" "
#: plugin.py:156
msgid "I'm already too close to maximum number of channels for this network."
msgstr "Minä olen jo liian lähellä kanavien maksimimäärää tässä verkossa."
#: plugin.py:165
msgid ""
"takes no arguments\n"
"\n"
" Returns the channels the bot is on. Must be given in private, in "
"order\n"
" to protect the secrecy of secret channels.\n"
" "
msgstr ""
"Ei ota parametrejä\n"
"\n"
" Palauttaa listan kanavista, joilla botti on. Täytyy antaa "
"yksityisviestillä salaistenkanavien\n"
" salaisuuden suojelemiseksi.\n"
" "
#: plugin.py:175
msgid "I'm not currently in any channels."
msgstr "En juuri nyt ole millään kanavalla."
#: plugin.py:181
msgid "My connection is restricted, I can't change nicks."
msgstr "Minun yhteyteni on rajoitettu. En voi vaihtaa nimimerkkiä."
#: plugin.py:188
msgid "Someone else is already using that nick."
msgstr "Joku muu käyttää jo tuota nimimerkkiä."
#: plugin.py:195
#, fuzzy
msgid "I can't change nick, I'm currently banned in %s."
msgstr "Minä en voi vaihtaa nimimerkkiä, koska palvelin sanoi %q"
#: plugin.py:203
msgid "I can't change nicks, the server said %q."
msgstr "Minä en voi vaihtaa nimimerkkiä, koska palvelin sanoi %q"
#: plugin.py:217
msgid ""
"[<nick>] [<network>]\n"
"\n"
" Changes the bot's nick to <nick>. If no nick is given, returns the\n"
" bot's current nick.\n"
" "
msgstr ""
"[<nimimerkki>] [<verkko>]\n"
"\n"
" Vaihtaa botin nimimerkin <nimimerkiksi>. Mikäli nimimerkkiä ei anneta, "
"botin nykyinen\n"
" nimimerkki palautetaan.\n"
" "
#: plugin.py:234
msgid ""
"[<channel>] [<reason>]\n"
"\n"
" Tells the bot to part the list of channels you give it. <channel> "
"is\n"
" only necessary if you want the bot to part a channel other than the\n"
" current channel. If <reason> is specified, use it as the part\n"
" message.\n"
" "
msgstr ""
"[<kanava>] [<syy>]\n"
"\n"
" Käskee botin poistua kanavilta, jotka annat sille. <Kanava> on\n"
" vaadittu jos haluat botin poistuvat muulta, kuin \n"
" nykyiseltä kanavalta. Jos <syy> on määritetty, sitä käytetään\n"
" poistumisviestissä.\n"
" "
#: plugin.py:252
msgid "I'm not in %s."
msgstr "Minä en ole kanavalla %s."
#: plugin.py:264
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Gives the user specified by <name> (or the user to whom "
"<hostmask>\n"
" currently maps) the specified capability <capability>\n"
" "
msgstr ""
"<nimi|hostmask> <valtuus>\n"
"\n"
" Antaa <nimen> määrittämälle käyttäjälle (tai käyttäjälle jonka "
"<hostmask>\n"
" ilmoittaa) määritetyn valtuuden <valtuus>\n"
" "
#: plugin.py:284
msgid ""
"The \"owner\" capability can't be added in the bot. Use the supybot-adduser "
"program (or edit the users.conf file yourself) to add an owner capability."
msgstr ""
"\"Owner\" valtuutta ei voi lisätä botissa. Käytä supybot-adduser ohjelmaa "
"(tai muokkaa users.conf tiedostoa itse) lisätäksesi owner valtuuden."
#: plugin.py:295
msgid "You can't add capabilities you don't have."
msgstr "Et voi lisätä valtuuksia, joita sinulla ei ole."
#: plugin.py:300
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Takes from the user specified by <name> (or the user to whom\n"
" <hostmask> currently maps) the specified capability "
"<capability>\n"
" "
msgstr ""
"<nimi|hostmask> <valtuus>\n"
"\n"
" Ottaa <nimen> määrittämältä käyttäjältä (tai käyttäjältä "
"johon\n"
" <hostmask> sopii) määritetyn valtuuden <valtuus>\n"
" "
#: plugin.py:312
msgid "That user doesn't have that capability."
msgstr "Tuolla käyttäjällä ei tuota valtuutta."
#: plugin.py:314
msgid "You can't remove capabilities you don't have."
msgstr "Sinä et voi poistaa valtuuksia, joita sinulla ei ole."
#: plugin.py:322
msgid ""
"<hostmask|nick> [<expires>]\n"
"\n"
" This will set a persistent ignore on <hostmask> or the hostmask\n"
" currently associated with <nick>. <expires> is an optional "
"argument\n"
" specifying when (in \"seconds from now\") the ignore will "
"expire; if\n"
" it isn't given, the ignore will never automatically expire.\n"
" "
msgstr ""
"<hostmask|nimimerkki> [<vanhentuu>]\n"
"\n"
" Tämä asettaa pysyvän huomiotta jättämisen <hostmaskiin> tai "
"hostmaskiin,\n"
" joka on tällä hetkellä yhdistetty <nimimerkkiin>. <Vanhentuminen> on "
"vaihtoehtoinen paremetri,\n"
" joka määrittää (\"sekuntit\") joiden jälkeen huomiotta jättäminen "
"poistetaan; jos\n"
" sitä ei ole annettu, huomiotta jättäminen ei vanhene ikinä "
"automaattisesti.\n"
" "
#: plugin.py:335
msgid ""
"<hostmask|nick>\n"
"\n"
" This will remove the persistent ignore on <hostmask> or the\n"
" hostmask currently associated with <nick>.\n"
" "
msgstr ""
"<hostmask|nimimerkki>\n"
"\n"
" Tämä poistaa pysyvän huomiotta jättämisen <hostmaskista> tai\n"
" hostmaskista joka on tällä hetkellä yhdistetty <nimimerkkiin>.\n"
" "
#: plugin.py:344
msgid "%s wasn't in the ignores database."
msgstr "%s ei ollut huomiotta jätettävien tietokannassa."
#: plugin.py:349
msgid ""
"takes no arguments\n"
"\n"
" Lists the hostmasks that the bot is ignoring.\n"
" "
msgstr ""
"Ei ota parametrejä\n"
"\n"
" Luetteloi hostmaskit jotka ovat botin huomiotta jättämis listalla.\n"
" "
#: plugin.py:357
msgid "I'm not currently globally ignoring anyone."
msgstr "En tällä hetkellä jätä ketään huomioitta globaalisti."
#: plugin.py:361
msgid ""
"takes no arguments\n"
"\n"
" Clears the current send queue for this network.\n"
" "
msgstr ""
"ei ota parametrejä\n"
"\n"
" Tyhjentää nykyisen lähetysjonon tälle verkolle.\n"
" "
#~ msgid "That nick is currently banned."
#~ msgstr "Tuolla nimimerkillä on tällähetkellä porttikielto."

View File

@ -1,239 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Limnoria\n"
"POT-Creation-Date: 2014-08-17 13:46+CEST\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: ProgVal <progval@gmail.com>\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-SourceCharset: Ascii\n"
"X-Generator: Poedit 1.5.4\n"
#: plugin.py:54
msgid "Nick/channel temporarily unavailable."
msgstr "Nick/canal temporairement indisponible"
#: plugin.py:72
msgid "Cannot join %s, it's full."
msgstr "Ne peut joindre %s, il est plein."
#: plugin.py:80
msgid "Cannot join %s, I was not invited."
msgstr "Ne peut joindre %s, pas invité."
#: plugin.py:88
msgid "Cannot join %s, I am banned."
msgstr "Ne peut joindre %s, j'y suis banni."
#: plugin.py:96
msgid "Cannot join %s, my keyword was wrong."
msgstr "Ne peut joindre %s, mon mot de passe est mauvais."
#: plugin.py:104 plugin.py:113
msgid "Cannot join %s, I'm not identified with NickServ."
msgstr "Ne peut joindre %s, je ne suis pas identifié auprès de NickServ."
#: plugin.py:143
msgid ""
"<channel> [<key>]\n"
"\n"
" Tell the bot to join the given channel. If <key> is given, it is "
"used\n"
" when attempting to join the channel.\n"
" "
msgstr ""
"<canal> [<clef>]\n"
"\n"
"Dit au bot de rejoindre le canal donné. Si la <clef> est donnée, elle est "
"utilisée pour rejoindre le canal."
#: plugin.py:156
msgid "I'm already too close to maximum number of channels for this network."
msgstr "Je suis déjà sur un nombre de canaux trop grand pour ce réseau."
#: plugin.py:165
msgid ""
"takes no arguments\n"
"\n"
" Returns the channels the bot is on. Must be given in private, in "
"order\n"
" to protect the secrecy of secret channels.\n"
" "
msgstr ""
"Ne prend pas d'argument \n"
"\n"
"Retourne les canaux sur lesquels le bot est. Doit être en privé, dans le but "
"d'éviter que les canaux secrets ne soient divulgués."
#: plugin.py:175
msgid "I'm not currently in any channels."
msgstr "Je ne suis actuellement sur aucun canal."
#: plugin.py:181
msgid "My connection is restricted, I can't change nicks."
msgstr "Ma connexion est restreinte, je ne peux changer de nick."
#: plugin.py:188
msgid "Someone else is already using that nick."
msgstr "Quelqu'un d'autre utilise déjà ce nick."
#: plugin.py:195
#, fuzzy
msgid "I can't change nick, I'm currently banned in %s."
msgstr "Je ne peux changer de nick, le serveur a dit %q."
#: plugin.py:203
msgid "I can't change nicks, the server said %q."
msgstr "Je ne peux changer de nick, le serveur a dit %q."
#: plugin.py:217
msgid ""
"[<nick>] [<network>]\n"
"\n"
" Changes the bot's nick to <nick>. If no nick is given, returns the\n"
" bot's current nick.\n"
" "
msgstr ""
"[<nick>] [<réseau>]\n"
"\n"
"Change le nick du bot à <nick>. Si aucun nick n'est donné, retourne le nick "
"actuel du bot."
#: plugin.py:234
msgid ""
"[<channel>] [<reason>]\n"
"\n"
" Tells the bot to part the list of channels you give it. <channel> "
"is\n"
" only necessary if you want the bot to part a channel other than the\n"
" current channel. If <reason> is specified, use it as the part\n"
" message.\n"
" "
msgstr ""
"[<canal>] [<raison>]\n"
"\n"
"Dit au bot de partir de la liste de canaux que vous avez donnée. <canal> "
"n'est nécessaire que si vous voulez que le bot parte d'un autre canal que "
"l'actuel. Si la <raison> est spécifiée, elle est utilisée comme message de "
"départ."
#: plugin.py:252
msgid "I'm not in %s."
msgstr "Je ne suis pas sur %s."
#: plugin.py:264
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Gives the user specified by <name> (or the user to whom "
"<hostmask>\n"
" currently maps) the specified capability <capability>\n"
" "
msgstr ""
"<nom|masque d'hôte> <capacité>\n"
"\n"
"Donne la <capacité> à l'utilisateur spécifié par <nom> (ou l'utilisateur à "
"qui correspond <masque d'hôte>)."
#: plugin.py:284
msgid ""
"The \"owner\" capability can't be added in the bot. Use the supybot-adduser "
"program (or edit the users.conf file yourself) to add an owner capability."
msgstr ""
"La capacité \"owner\" ne peut être ajoutée via le bot. Utilisez le programme "
"supybot-adduser (ou éditez le fichier users.conf vous-même) pour ajouter la "
"capacité owner."
#: plugin.py:295
msgid "You can't add capabilities you don't have."
msgstr "Vous ne pouvez ajouter des capacités que vous n'avez pas."
#: plugin.py:300
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Takes from the user specified by <name> (or the user to whom\n"
" <hostmask> currently maps) the specified capability "
"<capability>\n"
" "
msgstr ""
"<nom|masque d'hôte> <capacité>\n"
"\n"
"Retire la <capacité> à l'utilisateur spécifié par le <nom> (ou celui à qui "
"correspond le <masque d'hôte>)."
#: plugin.py:312
msgid "That user doesn't have that capability."
msgstr "Cet utilisateur n'a pas cette capacité."
#: plugin.py:314
msgid "You can't remove capabilities you don't have."
msgstr "Vous ne pouvez retirer des capacités que vous n'avez pas."
#: plugin.py:322
msgid ""
"<hostmask|nick> [<expires>]\n"
"\n"
" This will set a persistent ignore on <hostmask> or the hostmask\n"
" currently associated with <nick>. <expires> is an optional "
"argument\n"
" specifying when (in \"seconds from now\") the ignore will "
"expire; if\n"
" it isn't given, the ignore will never automatically expire.\n"
" "
msgstr ""
"<masque d'hôte|nick> [<expiration>]\n"
"\n"
"Ajoute un masque d'ignorance persistant sur le <masque d'hôte>, ou sur le "
"masque d'hôte de <nick>. <expiration> est un argument optionnel spécifiant "
"quand (en \"secondes à partir de maintenant\") l'ignorance expirera ; si "
"elle n'est pas donnée, l'ignorance n'expirera jamais."
#: plugin.py:335
msgid ""
"<hostmask|nick>\n"
"\n"
" This will remove the persistent ignore on <hostmask> or the\n"
" hostmask currently associated with <nick>.\n"
" "
msgstr ""
"<masque d'hôte|nick>\n"
"\n"
"Ceci retirera le masque d'ignorance persistant sur le <masque d'hôte>, ou "
"sur le masque d'hôte associé au <nick>."
#: plugin.py:344
msgid "%s wasn't in the ignores database."
msgstr "%s n'étais pas dans ma base de données d'ignorance."
#: plugin.py:349
msgid ""
"takes no arguments\n"
"\n"
" Lists the hostmasks that the bot is ignoring.\n"
" "
msgstr ""
"Ne prend pas d'argument\n"
"\n"
"Liste les masques d'hôte que le bot ignore."
#: plugin.py:357
msgid "I'm not currently globally ignoring anyone."
msgstr "Je n'ignore actuellement personne globalement."
#: plugin.py:361
msgid ""
"takes no arguments\n"
"\n"
" Clears the current send queue for this network.\n"
" "
msgstr ""
"Ne prend pas d'argument\n"
"\n"
"Vide la queue en attente pour ce réseau."
#~ msgid "That nick is currently banned."
#~ msgstr "Ce nick est banni."

View File

@ -1,257 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Limnoria\n"
"POT-Creation-Date: 2014-08-17 13:46+CEST\n"
"PO-Revision-Date: 2012-03-15 20:25+0100\n"
"Last-Translator: skizzhg <skizzhg@gmx.com>\n"
"Language-Team: Italian <skizzhg@gmx.com>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: plugin.py:54
msgid "Nick/channel temporarily unavailable."
msgstr "Nick/canale temporaneamente non disponibile."
#: plugin.py:72
msgid "Cannot join %s, it's full."
msgstr "Non posso entrare in %s, è pieno."
#: plugin.py:80
msgid "Cannot join %s, I was not invited."
msgstr "Non posso entrare in %s, non sono stato invitato."
#: plugin.py:88
msgid "Cannot join %s, I am banned."
msgstr "Non posso entrare in %s, sono stato bannato."
#: plugin.py:96
msgid "Cannot join %s, my keyword was wrong."
msgstr "Non posso entrare in %s, la password era sbagliata."
#: plugin.py:104 plugin.py:113
msgid "Cannot join %s, I'm not identified with NickServ."
msgstr "Non posso entrare in %s, non sono identificato con NickServ."
#: plugin.py:143
msgid ""
"<channel> [<key>]\n"
"\n"
" Tell the bot to join the given channel. If <key> is given, it is "
"used\n"
" when attempting to join the channel.\n"
" "
msgstr ""
"<canale> [<password>]\n"
"\n"
" Dice al bot di entrare nel canale specificato. Se <password> è "
"fornita,\n"
" viene usata quando si tenta di entrare nel canale.\n"
" "
#: plugin.py:156
msgid "I'm already too close to maximum number of channels for this network."
msgstr "Sono già troppo vicino al numero massimo di canali per questa rete."
#: plugin.py:165
msgid ""
"takes no arguments\n"
"\n"
" Returns the channels the bot is on. Must be given in private, in "
"order\n"
" to protect the secrecy of secret channels.\n"
" "
msgstr ""
"non necessita argomenti\n"
"\n"
" Restituisce i canali dove è presente il bot. Deve essere richiesto "
"in\n"
" privato per preservare la segretezza dei canali privati.\n"
" "
#: plugin.py:175
msgid "I'm not currently in any channels."
msgstr "Al momento non sono in nessun canale."
#: plugin.py:181
msgid "My connection is restricted, I can't change nicks."
msgstr "La mia connessione è limitata, non posso cambiare nick."
#: plugin.py:188
msgid "Someone else is already using that nick."
msgstr "Qualcun altro sta utilizzando questo nick."
#: plugin.py:195
#, fuzzy
msgid "I can't change nick, I'm currently banned in %s."
msgstr "Non posso cambiare nick, il server ha detto %q."
#: plugin.py:203
msgid "I can't change nicks, the server said %q."
msgstr "Non posso cambiare nick, il server ha detto %q."
#: plugin.py:217
#, fuzzy
msgid ""
"[<nick>] [<network>]\n"
"\n"
" Changes the bot's nick to <nick>. If no nick is given, returns the\n"
" bot's current nick.\n"
" "
msgstr ""
"[<nick>]\n"
"\n"
" Cambia il nick del bot in <nick>. Se non ne viene fornito uno, "
"restituisce\n"
" quello attuale.\n"
" "
#: plugin.py:234
msgid ""
"[<channel>] [<reason>]\n"
"\n"
" Tells the bot to part the list of channels you give it. <channel> "
"is\n"
" only necessary if you want the bot to part a channel other than the\n"
" current channel. If <reason> is specified, use it as the part\n"
" message.\n"
" "
msgstr ""
"[<canale>] [<motivo>]\n"
"\n"
" Fornisce al bot l'elenco dei canali da cui uscire. <canale> è "
"necessario\n"
" solo se si vuole far uscire il bot da un canale diverso da quello "
"attuale.\n"
" Se <motivo> viene specificato, verrà usato come messaggio di "
"uscita.\n"
" "
#: plugin.py:252
msgid "I'm not in %s."
msgstr "Non sono in %s."
#: plugin.py:264
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Gives the user specified by <name> (or the user to whom "
"<hostmask>\n"
" currently maps) the specified capability <capability>\n"
" "
msgstr ""
"<nome|hostmask> <capacità>\n"
"\n"
" Dà all'utente specificato da <nome> (o quello a cui corrisponde\n"
" <hostmask> attualmente) la <capacità> specificata.\n"
" "
#: plugin.py:284
msgid ""
"The \"owner\" capability can't be added in the bot. Use the supybot-adduser "
"program (or edit the users.conf file yourself) to add an owner capability."
msgstr ""
"La capacità \"owner\" non può essere aggiunta al bot. Utilizzare il "
"programma supybot-adduser (o modificare il file users.conf) per aggiungerla."
#: plugin.py:295
msgid "You can't add capabilities you don't have."
msgstr "Non puoi aggiungere capacità che non hai."
#: plugin.py:300
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Takes from the user specified by <name> (or the user to whom\n"
" <hostmask> currently maps) the specified capability "
"<capability>\n"
" "
msgstr ""
"<nome|hostmask> <capacità>\n"
"\n"
" Rimuove l'utente specificato da <nome> (o quello a cui "
"corrisponde\n"
" <hostmask> attualmente) la <capacità> specificata\n"
" "
#: plugin.py:312
msgid "That user doesn't have that capability."
msgstr "Questo utente non ha tale capacità."
#: plugin.py:314
msgid "You can't remove capabilities you don't have."
msgstr "Non puoi rimuovere capacità che non hai."
#: plugin.py:322
msgid ""
"<hostmask|nick> [<expires>]\n"
"\n"
" This will set a persistent ignore on <hostmask> or the hostmask\n"
" currently associated with <nick>. <expires> is an optional "
"argument\n"
" specifying when (in \"seconds from now\") the ignore will "
"expire; if\n"
" it isn't given, the ignore will never automatically expire.\n"
" "
msgstr ""
"<hostmask|nick> [<scadenza>]\n"
"\n"
" Imposta un ignore permanente su <hostmask> o l'hostmask "
"attualmente\n"
" associata a <nick>. <scadenza> è un argomento opzionale per "
"specificare\n"
" quando (in \"secondi a partire da subito\") scadrà l'ignore; se "
"non fornito,\n"
" questo non scadrà mai.\n"
" "
#: plugin.py:335
msgid ""
"<hostmask|nick>\n"
"\n"
" This will remove the persistent ignore on <hostmask> or the\n"
" hostmask currently associated with <nick>.\n"
" "
msgstr ""
"<hostmask|nick>\n"
"\n"
" Rimuove l'ignore persistente su <hostmask> o l'attuale hostmask "
"associata a <nick>.\n"
" "
#: plugin.py:344
msgid "%s wasn't in the ignores database."
msgstr "%s non è nel mio database degli ignorati."
#: plugin.py:349
msgid ""
"takes no arguments\n"
"\n"
" Lists the hostmasks that the bot is ignoring.\n"
" "
msgstr ""
"Non necessita argomenti\n"
"\n"
" Elenca le hostmask che il bot sta ignorando.\n"
" "
#: plugin.py:357
msgid "I'm not currently globally ignoring anyone."
msgstr "Al momento non sto ignorando nessuno."
#: plugin.py:361
msgid ""
"takes no arguments\n"
"\n"
" Clears the current send queue for this network.\n"
" "
msgstr ""
"non necessita argomenti\n"
"\n"
" Pulisce l'attuale coda dei messaggi da inviare (interrompe il flood) "
"per questa rete.\n"
" "
#~ msgid "That nick is currently banned."
#~ msgstr "Il nick è attualmente bannato."

View File

@ -1,196 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2014-08-17 13:46+CEST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: plugin.py:54
#, docstring
msgid "Nick/channel temporarily unavailable."
msgstr ""
#: plugin.py:72
msgid "Cannot join %s, it's full."
msgstr ""
#: plugin.py:80
msgid "Cannot join %s, I was not invited."
msgstr ""
#: plugin.py:88
msgid "Cannot join %s, I am banned."
msgstr ""
#: plugin.py:96
msgid "Cannot join %s, my keyword was wrong."
msgstr ""
#: plugin.py:104 plugin.py:113
msgid "Cannot join %s, I'm not identified with NickServ."
msgstr ""
#: plugin.py:143
#, docstring
msgid ""
"<channel> [<key>]\n"
"\n"
" Tell the bot to join the given channel. If <key> is given, it is used\n"
" when attempting to join the channel.\n"
" "
msgstr ""
#: plugin.py:156
msgid "I'm already too close to maximum number of channels for this network."
msgstr ""
#: plugin.py:165
#, docstring
msgid ""
"takes no arguments\n"
"\n"
" Returns the channels the bot is on. Must be given in private, in order\n"
" to protect the secrecy of secret channels.\n"
" "
msgstr ""
#: plugin.py:175
msgid "I'm not currently in any channels."
msgstr ""
#: plugin.py:181
msgid "My connection is restricted, I can't change nicks."
msgstr ""
#: plugin.py:188
msgid "Someone else is already using that nick."
msgstr ""
#: plugin.py:195
msgid "I can't change nick, I'm currently banned in %s."
msgstr ""
#: plugin.py:203
msgid "I can't change nicks, the server said %q."
msgstr ""
#: plugin.py:217
#, docstring
msgid ""
"[<nick>] [<network>]\n"
"\n"
" Changes the bot's nick to <nick>. If no nick is given, returns the\n"
" bot's current nick.\n"
" "
msgstr ""
#: plugin.py:234
#, docstring
msgid ""
"[<channel>] [<reason>]\n"
"\n"
" Tells the bot to part the list of channels you give it. <channel> is\n"
" only necessary if you want the bot to part a channel other than the\n"
" current channel. If <reason> is specified, use it as the part\n"
" message.\n"
" "
msgstr ""
#: plugin.py:252
msgid "I'm not in %s."
msgstr ""
#: plugin.py:264
#, docstring
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Gives the user specified by <name> (or the user to whom <hostmask>\n"
" currently maps) the specified capability <capability>\n"
" "
msgstr ""
#: plugin.py:284
msgid "The \"owner\" capability can't be added in the bot. Use the supybot-adduser program (or edit the users.conf file yourself) to add an owner capability."
msgstr ""
#: plugin.py:295
msgid "You can't add capabilities you don't have."
msgstr ""
#: plugin.py:300
#, docstring
msgid ""
"<name|hostmask> <capability>\n"
"\n"
" Takes from the user specified by <name> (or the user to whom\n"
" <hostmask> currently maps) the specified capability <capability>\n"
" "
msgstr ""
#: plugin.py:312
msgid "That user doesn't have that capability."
msgstr ""
#: plugin.py:314
msgid "You can't remove capabilities you don't have."
msgstr ""
#: plugin.py:322
#, docstring
msgid ""
"<hostmask|nick> [<expires>]\n"
"\n"
" This will set a persistent ignore on <hostmask> or the hostmask\n"
" currently associated with <nick>. <expires> is an optional argument\n"
" specifying when (in \"seconds from now\") the ignore will expire; if\n"
" it isn't given, the ignore will never automatically expire.\n"
" "
msgstr ""
#: plugin.py:335
#, docstring
msgid ""
"<hostmask|nick>\n"
"\n"
" This will remove the persistent ignore on <hostmask> or the\n"
" hostmask currently associated with <nick>.\n"
" "
msgstr ""
#: plugin.py:344
msgid "%s wasn't in the ignores database."
msgstr ""
#: plugin.py:349
#, docstring
msgid ""
"takes no arguments\n"
"\n"
" Lists the hostmasks that the bot is ignoring.\n"
" "
msgstr ""
#: plugin.py:357
msgid "I'm not currently globally ignoring anyone."
msgstr ""
#: plugin.py:361
#, docstring
msgid ""
"takes no arguments\n"
"\n"
" Clears the current send queue for this network.\n"
" "
msgstr ""

View File

@ -1,373 +0,0 @@
###
# Copyright (c) 2002-2005, Jeremiah Fincher
# Copyright (c) 2010, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import sys
import time
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.utils as utils
from supybot.commands import *
import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils
import supybot.schedule as schedule
import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Admin')
class Admin(callbacks.Plugin):
def __init__(self, irc):
self.__parent = super(Admin, self)
self.__parent.__init__(irc)
self.joins = {}
self.pendingNickChanges = {}
@internationalizeDocstring
def do437(self, irc, msg):
"""Nick/channel temporarily unavailable."""
target = msg.args[0]
if irc.isChannel(target): # We don't care about nicks.
t = time.time() + 30
# Let's schedule a rejoin.
networkGroup = conf.supybot.networks.get(irc.network)
def rejoin():
irc.queueMsg(networkGroup.channels.join(target))
# We don't need to schedule something because we'll get another
# 437 when we try to join later.
schedule.addEvent(rejoin, t)
self.log.info('Scheduling a rejoin to %s at %s; '
'Channel temporarily unavailable.', target, t)
def do471(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error(_('Cannot join %s, it\'s full.') % channel)
except KeyError:
self.log.debug('Got 471 without Admin.join being called.')
def do473(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error(_('Cannot join %s, I was not invited.') % channel)
except KeyError:
self.log.debug('Got 473 without Admin.join being called.')
def do474(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error(_('Cannot join %s, I am banned.') % channel)
except KeyError:
self.log.debug('Got 474 without Admin.join being called.')
def do475(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error(_('Cannot join %s, my keyword was wrong.') % channel)
except KeyError:
self.log.debug('Got 475 without Admin.join being called.')
def do477(self, irc, msg):
try:
channel = msg.args[1]
(irc,msg) = self.joins.pop(channel)
irc.error(_('Cannot join %s, I\'m not identified with '
'NickServ.') % channel)
except KeyError:
self.log.debug('Got 477 without Admin.join being called.')
def do515(self, irc, msg):
try:
channel = msg.args[1]
(irc, msg) = self.joins.pop(channel)
irc.error(_('Cannot join %s, I\'m not identified with '
'NickServ.') % channel)
except KeyError:
self.log.debug('Got 515 without Admin.join being called.')
def doJoin(self, irc, msg):
if msg.prefix == irc.prefix:
try:
del self.joins[msg.args[0]]
except KeyError:
s = 'Joined a channel without Admin.join being called.'
self.log.debug(s)
def doInvite(self, irc, msg):
channel = msg.args[1]
if channel not in irc.state.channels:
if conf.supybot.alwaysJoinOnInvite.get(channel)() or \
ircdb.checkCapability(msg.prefix, 'admin'):
self.log.info('Invited to %s by %s.', channel, msg.prefix)
networkGroup = conf.supybot.networks.get(irc.network)
irc.queueMsg(networkGroup.channels.join(channel))
conf.supybot.networks.get(irc.network).channels().add(channel)
else:
self.log.warning('Invited to %s by %s, but '
'supybot.alwaysJoinOnInvite was False and '
'the user lacked the "admin" capability.',
channel, msg.prefix)
@internationalizeDocstring
def join(self, irc, msg, args, channel, key):
"""<channel> [<key>]
Tell the bot to join the given channel. If <key> is given, it is used
when attempting to join the channel.
"""
if not irc.isChannel(channel):
irc.errorInvalid('channel', channel, Raise=True)
networkGroup = conf.supybot.networks.get(irc.network)
networkGroup.channels().add(channel)
if key:
networkGroup.channels.key.get(channel).setValue(key)
maxchannels = irc.state.supported.get('maxchannels', sys.maxsize)
if len(irc.state.channels) + 1 > maxchannels:
irc.error(_('I\'m already too close to maximum number of '
'channels for this network.'), Raise=True)
irc.queueMsg(networkGroup.channels.join(channel))
irc.noReply()
self.joins[channel] = (irc, msg)
join = wrap(join, ['validChannel', additional('something')])
@internationalizeDocstring
def channels(self, irc, msg, args):
"""takes no arguments
Returns the channels the bot is on. Must be given in private, in order
to protect the secrecy of secret channels.
"""
L = irc.state.channels.keys()
if L:
utils.sortBy(ircutils.toLower, L)
irc.reply(format('%L', L))
else:
irc.reply(_('I\'m not currently in any channels.'))
channels = wrap(channels, ['private'])
def do484(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error(_('My connection is restricted, I can\'t change nicks.'))
else:
self.log.debug('Got 484 without Admin.nick being called.')
def do433(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error(_('Someone else is already using that nick.'))
else:
self.log.debug('Got 433 without Admin.nick being called.')
def do435(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error(_('I can\'t change nick, I\'m currently banned in %s.') %
msg.args[2])
else:
self.log.debug('Got 435 without Admin.nick being called.')
def do438(self, irc, msg):
irc = self.pendingNickChanges.get(irc, None)
if irc is not None:
irc.error(format(_('I can\'t change nicks, the server said %q.'),
msg.args[2]), private=True)
else:
self.log.debug('Got 438 without Admin.nick being called.')
def doNick(self, irc, msg):
if msg.nick == irc.nick or msg.args[0] == irc.nick:
try:
del self.pendingNickChanges[irc]
except KeyError:
self.log.debug('Got NICK without Admin.nick being called.')
@internationalizeDocstring
def nick(self, irc, msg, args, nick, network):
"""[<nick>] [<network>]
Changes the bot's nick to <nick>. If no nick is given, returns the
bot's current nick.
"""
network = network or irc.network
if nick:
group = getattr(conf.supybot.networks, network)
group.nick.setValue(nick)
irc.queueMsg(ircmsgs.nick(nick))
self.pendingNickChanges[irc.getRealIrc()] = irc
else:
irc.reply(irc.nick)
nick = wrap(nick, [additional('nick'), additional('something')])
@internationalizeDocstring
def part(self, irc, msg, args, channel, reason):
"""[<channel>] [<reason>]
Tells the bot to part the list of channels you give it. <channel> is
only necessary if you want the bot to part a channel other than the
current channel. If <reason> is specified, use it as the part
message.
"""
if channel is None:
if irc.isChannel(msg.args[0]):
channel = msg.args[0]
else:
irc.error(Raise=True)
try:
network = conf.supybot.networks.get(irc.network)
network.channels().remove(channel)
except KeyError:
pass
if channel not in irc.state.channels:
irc.error(_('I\'m not in %s.') % channel, Raise=True)
irc.queueMsg(ircmsgs.part(channel, reason or msg.nick))
if msg.nick in irc.state.channels[channel].users:
irc.noReply()
else:
irc.replySuccess()
part = wrap(part, [optional('validChannel'), additional('text')])
class capability(callbacks.Commands):
@internationalizeDocstring
def add(self, irc, msg, args, user, capability):
"""<name|hostmask> <capability>
Gives the user specified by <name> (or the user to whom <hostmask>
currently maps) the specified capability <capability>
"""
# Ok, the concepts that are important with capabilities:
#
### 1) No user should be able to elevate their privilege to owner.
### 2) Admin users are *not* superior to #channel.ops, and don't
### have God-like powers over channels.
### 3) We assume that Admin users are two things: non-malicious and
### and greedy for power. So they'll try to elevate their
### privilege to owner, but they won't try to crash the bot for
### no reason.
# Thus, the owner capability can't be given in the bot. Admin
# users can only give out capabilities they have themselves (which
# will depend on supybot.capabilities and its child default) but
# generally means they can't mess with channel capabilities.
if ircutils.strEqual(capability, 'owner'):
irc.error(_('The "owner" capability can\'t be added in the '
'bot. Use the supybot-adduser program (or edit the '
'users.conf file yourself) to add an owner '
'capability.'))
return
if ircdb.isAntiCapability(capability) or \
ircdb.checkCapability(msg.prefix, capability):
user.addCapability(capability)
ircdb.users.setUser(user)
irc.replySuccess()
else:
irc.error(_('You can\'t add capabilities you don\'t have.'))
add = wrap(add, ['otherUser', 'lowered'])
@internationalizeDocstring
def remove(self, irc, msg, args, user, capability):
"""<name|hostmask> <capability>
Takes from the user specified by <name> (or the user to whom
<hostmask> currently maps) the specified capability <capability>
"""
if ircdb.checkCapability(msg.prefix, capability) or \
ircdb.isAntiCapability(capability):
try:
user.removeCapability(capability)
ircdb.users.setUser(user)
irc.replySuccess()
except KeyError:
irc.error(_('That user doesn\'t have that capability.'))
else:
s = _('You can\'t remove capabilities you don\'t have.')
irc.error(s)
remove = wrap(remove, ['otherUser','lowered'])
class ignore(callbacks.Commands):
@internationalizeDocstring
def add(self, irc, msg, args, hostmask, expires):
"""<hostmask|nick> [<expires>]
This will set a persistent ignore on <hostmask> or the hostmask
currently associated with <nick>. <expires> is an optional argument
specifying when (in "seconds from now") the ignore will expire; if
it isn't given, the ignore will never automatically expire.
"""
ircdb.ignores.add(hostmask, expires)
irc.replySuccess()
add = wrap(add, ['hostmask', additional('expiry', 0)])
@internationalizeDocstring
def remove(self, irc, msg, args, hostmask):
"""<hostmask|nick>
This will remove the persistent ignore on <hostmask> or the
hostmask currently associated with <nick>.
"""
try:
ircdb.ignores.remove(hostmask)
irc.replySuccess()
except KeyError:
irc.error(_('%s wasn\'t in the ignores database.') % hostmask)
remove = wrap(remove, ['hostmask'])
@internationalizeDocstring
def list(self, irc, msg, args):
"""takes no arguments
Lists the hostmasks that the bot is ignoring.
"""
# XXX Add the expirations.
if ircdb.ignores.hostmasks:
irc.reply(format('%L', (list(map(repr,ircdb.ignores.hostmasks)))))
else:
irc.reply(_('I\'m not currently globally ignoring anyone.'))
list = wrap(list)
def clearq(self, irc, msg, args):
"""takes no arguments
Clears the current send queue for this network.
"""
irc.queue.reset()
irc.replySuccess()
clearq = wrap(clearq)
Class = Admin
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,131 +0,0 @@
###
# Copyright (c) 2002-2005, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *
class AdminTestCase(PluginTestCase):
plugins = ('Admin',)
def testChannels(self):
def getAfterJoinMessages():
m = self.irc.takeMsg()
self.assertEqual(m.command, 'MODE')
m = self.irc.takeMsg()
self.assertEqual(m.command, 'MODE')
m = self.irc.takeMsg()
self.assertEqual(m.command, 'WHO')
self.assertRegexp('channels', 'not.*in any')
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))
getAfterJoinMessages()
self.assertRegexp('channels', '#foo')
self.irc.feedMsg(ircmsgs.join('#bar', prefix=self.prefix))
getAfterJoinMessages()
self.assertRegexp('channels', '#bar and #foo')
self.irc.feedMsg(ircmsgs.join('#Baz', prefix=self.prefix))
getAfterJoinMessages()
self.assertRegexp('channels', '#bar, #Baz, and #foo')
def testIgnoreAddRemove(self):
self.assertNotError('admin ignore add foo!bar@baz')
self.assertError('admin ignore add alsdkfjlasd')
self.assertNotError('admin ignore remove foo!bar@baz')
self.assertError('admin ignore remove foo!bar@baz')
def testIgnoreList(self):
self.assertNotError('admin ignore list')
self.assertNotError('admin ignore add foo!bar@baz')
self.assertNotError('admin ignore list')
self.assertNotError('admin ignore add foo!bar@baz')
self.assertRegexp('admin ignore list', 'foo')
def testCapabilityAdd(self):
self.assertError('capability add foo bar')
u = ircdb.users.newUser()
u.name = 'foo'
ircdb.users.setUser(u)
self.assertNotError('capability add foo bar')
self.assertError('addcapability foo baz')
self.assert_('bar' in u.capabilities)
ircdb.users.delUser(u.id)
def testCapabilityRemove(self):
self.assertError('capability remove foo bar')
u = ircdb.users.newUser()
u.name = 'foo'
ircdb.users.setUser(u)
self.assertNotError('capability add foo bar')
self.assert_('bar' in u.capabilities)
self.assertError('removecapability foo bar')
self.assertNotError('capability remove foo bar')
self.assert_(not 'bar' in u.capabilities)
ircdb.users.delUser(u.id)
def testJoin(self):
m = self.getMsg('join #foo')
self.assertEqual(m.command, 'JOIN')
self.assertEqual(m.args[0], '#foo')
m = self.getMsg('join #foo key')
self.assertEqual(m.command, 'JOIN')
self.assertEqual(m.args[0], '#foo')
self.assertEqual(m.args[1], 'key')
def testPart(self):
def getAfterJoinMessages():
m = self.irc.takeMsg()
self.assertEqual(m.command, 'MODE')
m = self.irc.takeMsg()
self.assertEqual(m.command, 'MODE')
m = self.irc.takeMsg()
self.assertEqual(m.command, 'WHO')
self.assertError('part #foo')
self.assertRegexp('part #foo', 'not in')
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))
getAfterJoinMessages()
m = self.getMsg('part #foo')
self.assertEqual(m.command, 'PART')
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))
getAfterJoinMessages()
m = self.getMsg('part #foo reason')
self.assertEqual(m.command, 'PART')
self.assertEqual(m.args[0], '#foo')
self.assertEqual(m.args[1], 'reason')
def testNick(self):
original = conf.supybot.nick()
try:
m = self.getMsg('nick foobar')
self.assertEqual(m.command, 'NICK')
self.assertEqual(m.args[0], 'foobar')
finally:
conf.supybot.networks.test.nick.setValue('')
def testAddCapabilityOwner(self):
self.assertError('admin capability add %s owner' % self.nick)
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,74 +0,0 @@
This plugin allows the user to create various aliases (known as "Akas",
since Alias is the name of another plugin Aka is based on) to other
commands or combinations of other commands (via nested commands).
It is a good idea to always quote the commands that are being aliased so
that any nested commands are not immediately run.
Basic usage
-----------
### Alias
Add an aka, Alias, which eases the transitioning to Aka from Alias.
First we will load Alias and Aka.
```
<jamessan> @load Alias
<bot> jamessan: The operation succeeded.
<jamessan> @load Aka
<bot> jamessan: The operation succeeded.
```
Then we import the Alias database to Aka in case it exists and unload
Alias.
```
<jamessan> @importaliasdatabase
<bot> jamessan: The operation succeeded.
<jamessan> @unload Alias
<bot> jamessan: The operation succeeded.
```
And now we will finally add the Aka `alias` itself.
```
<jamessan> @aka add "alias" "aka $1 $*"
<bot> jamessan: The operation succeeded.
```
Now you can use Aka as you used Alias before.
### Trout
Add an aka, trout, which expects a word as an argument
```
<jamessan> @aka add trout "reply action slaps $1 with a large trout"
<bot> jamessan: The operation succeeded.
<jamessan> @trout me
* bot slaps me with a large trout
```
This `trout` aka requires the plugin `Reply` to be loaded since it
provides the `action` command.
### LastFM
Add an aka, `lastfm`, which expects a last.fm username and replies with
their most recently played item.
```
@aka add lastfm "rss [format concat http://ws.audioscrobbler.com/1.0/user/ [format concat [web urlquote $1] /recenttracks.rss]]"
```
This `lastfm` aka requires the following plugins to be loaded: `RSS`,
`Format` and `Web`.
`RSS` provides `rss`, `Format provides `concat` and `Web` provides
`urlquote`.
Note that if the nested commands being aliased hadn't been quoted, then
those commands would have been run immediately, and `@lastfm` would always
reply with the same information, the result of those commands.

View File

@ -1,69 +0,0 @@
###
# Copyright (c) 2013, Valentin Lorentz
# 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 a description of the plugin (to be presented to the user inside the wizard)
here. This should describe *what* the plugin does.
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = ""
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.authors.unknown
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = '' # 'http://supybot.com/Members/yourname/Aka/download'
from . import config
from . import plugin
from imp import reload
# In case we're being reloaded.
reload(config)
reload(plugin)
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -1,60 +0,0 @@
###
# Copyright (c) 2013, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Aka')
except:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x:x
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself 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('Aka', True)
Aka = conf.registerPlugin('Aka')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(Aka, 'someConfigVariableName',
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
conf.registerGlobalValue(Aka, 'maximumWordsInName',
registry.Integer(5, _("""The maximum number of words allowed in a
command name. Setting this to an high value may slow down your bot
on long commands.""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -1 +0,0 @@
# Stub so local is a module, used for third-party modules

View File

@ -1,209 +0,0 @@
# Aka plugin for Limnoria
# Copyright (C) 2014 Limnoria
# Mikaela Suomalainen <mikaela.suomalainen@outlook.com>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: Aka plugin for Limnoria\n"
"POT-Creation-Date: 2014-08-01 19:48+EEST\n"
"PO-Revision-Date: 2014-08-01 20:05+0200\n"
"Last-Translator: Mikaela Suomalainen <mikaela.suomalainen@outlook.com>\n"
"Language-Team: \n"
"Language: Finnish\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 1.5.4\n"
#: config.py:55
msgid ""
"The maximum number of words allowed in a\n"
" command name. Setting this to an high value may slow down your bot\n"
" on long commands."
msgstr ""
"Komennon nimessä sallittujen merkkien enimmäismäärä.\n"
" Korkean arvon asettaminen tähän voi hidastaa bottiasi pitkien komentojen "
"kanssa."
#: plugin.py:140 plugin.py:264 plugin.py:505
msgid "This Aka already exists."
msgstr "Tämä Aka on jo olemassa."
#: plugin.py:169 plugin.py:181 plugin.py:195 plugin.py:291 plugin.py:308
#: plugin.py:325
msgid "This Aka does not exist"
msgstr "Tätä Akaa ei ole olemassakaan"
#: plugin.py:293
msgid "This Aka is already locked."
msgstr "Tämä Aka on jo lukittu."
#: plugin.py:310
msgid "This Aka is already unlocked."
msgstr "Tämä Aka on jo avattu."
#: plugin.py:372
msgid ""
"Add the help for \"@plugin help Aka\" here\n"
" This should describe *how* to use this plugin."
msgstr ""
"Lisää ohje komentoa \"@plugin help Aka\" varten tähän.\n"
" Tämän pitäisi kuvata *kuinka* tätä lisä-osaa käytetään."
#: plugin.py:479
msgid "You've attempted more nesting than is currently allowed on this bot."
msgstr ""
"Olet yrittänyt sisällyttää enemmän komentoja, kuin tässä botti sallii juuri "
"nyt."
#: plugin.py:483
msgid " at least"
msgstr "ainakin"
#: plugin.py:492
msgid "Locked by %s at %s"
msgstr "Lukinnut %s aikaan %s"
#: plugin.py:495
msgid ""
"<an alias,%s %n>\n"
"\n"
"Alias for %q.%s"
msgstr ""
"<alias,%s %n>\n"
"\n"
"Alias komennolle %q.%s"
#: plugin.py:496
msgid "argument"
msgstr "parametri"
#: plugin.py:502
msgid "You can't overwrite commands in this plugin."
msgstr "Et voi ylikirjoittaa tämän lisä-osan komentoja."
#: plugin.py:507
msgid "This Aka has too many spaces in its name."
msgstr "Tämän Akan nimessä on liian monta välilyöntiä."
#: plugin.py:512
msgid "Can't mix $* and optional args (@1, etc.)"
msgstr ""
"$*:ä ja vapaaehtoisia parametrejä (@1, jne.) ei voida sekoittaa keskenään"
#: plugin.py:514
msgid "There can be only one $* in an alias."
msgstr "Aliaksessa voi olla vain yksi $*."
#: plugin.py:521
msgid "This Aka is locked."
msgstr "Tämä Aka on lukittu."
#: plugin.py:525
#, fuzzy
msgid ""
"[--channel <#channel>] <name> <command>\n"
"\n"
" Defines an alias <name> that executes <command>. The <command>\n"
" should be in the standard \"command argument [nestedcommand "
"argument]\"\n"
" arguments to the alias; they'll be filled with the first, second, "
"etc.\n"
" arguments. $1, $2, etc. can be used for required arguments. @1, "
"@2,\n"
" etc. can be used for optional arguments. $* simply means \"all\n"
" arguments that have not replaced $1, $2, etc.\", ie. it will also\n"
" include optional arguments.\n"
" "
msgstr ""
"[--channel <#kanava>] <nimi> <komento>\n"
"\n"
"Määrittää aliaksen <nimi>, joka suorittaa <komennon>. <Komennon>\n"
" pitäisi olla tavallisessa muodossa \"komento parametri [sisällytettykomento "
"parametri]\"\n"
" parametreinä aliakselle; ne täytetään ensimmäisenä, toisena, jne.\n"
" parametreinä. $1, $2, jne. voidaan käyttää vaadittuina parametreinä. @1, "
"@2,\n"
" jne. voidaan käyttää vapaaehtoisina parametreinä. $* tarkoittaa "
"yksinkertaisesti \"kaikki\n"
" jotka eivät ole korvanneet $1, $2, jne.\", esim. se sisältää vapaa-ehtoiset "
"parametrit.\n"
" "
#: plugin.py:539 plugin.py:565 plugin.py:597 plugin.py:620 plugin.py:643
msgid "%r is not a valid channel."
msgstr "%r ei ole kelvollinen kanava."
#: plugin.py:557
msgid ""
"[--channel <#channel>] <name>\n"
"\n"
" Removes the given alias, if unlocked.\n"
" "
msgstr ""
"[--channel <#kanava>] <nimi>\n"
"\n"
" Poistaa annetun aliaksen, ellei se ole lukittu.\n"
" "
#: plugin.py:579
msgid ""
"Check if the user has any of the required capabilities to manage\n"
" the regexp database."
msgstr ""
"Tarkistaa onko käyttäjällä vaadittu valtuus säännöllisten lausekkeiden\n"
" tietokannan hallintaan."
#: plugin.py:589
msgid ""
"[--channel <#channel>] <alias>\n"
"\n"
" Locks an alias so that no one else can change it.\n"
" "
msgstr ""
"[--channel <#kanava>] <alias>\n"
"\n"
" Lukitsee aliaksen estäen muita muokkaamasta sitä.\n"
" "
#: plugin.py:612
msgid ""
"[--channel <#channel>] <alias>\n"
"\n"
" Unlocks an alias so that people can define new aliases over it.\n"
" "
msgstr ""
"[--channel <#kanava>] <alias>\n"
"\n"
" Avaa aliaksen, jotta kaikki voivat määrittää uusia aliaksia sen päälle.\n"
" "
#: plugin.py:635
msgid ""
"<command>\n"
"\n"
" This command shows the content of an Aka.\n"
" "
msgstr ""
"<komento>\n"
"\n"
" Tämä komento näyttää Akan sisällön."
#: plugin.py:652
msgid ""
"takes no arguments\n"
"\n"
" Imports the Alias database into Aka's, and clean the former."
msgstr ""
"ei ota parametrejä\n"
"\n"
" Tuo Aliaksen tietokannan Akaan ja tyhjentää aiemman."
#: plugin.py:657
msgid "Alias plugin is not loaded."
msgstr "Alias lisä-osa ei ole ladattu."
#: plugin.py:667
msgid "Error occured when importing the %n: %L"
msgstr "Virhe komennon %n tuomisessa: %L"

View File

@ -1,169 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2014-08-01 19:48+EEST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: config.py:55
msgid ""
"The maximum number of words allowed in a\n"
" command name. Setting this to an high value may slow down your bot\n"
" on long commands."
msgstr ""
#: plugin.py:140 plugin.py:264 plugin.py:505
msgid "This Aka already exists."
msgstr ""
#: plugin.py:169 plugin.py:181 plugin.py:195 plugin.py:291 plugin.py:308
#: plugin.py:325
msgid "This Aka does not exist"
msgstr ""
#: plugin.py:293
msgid "This Aka is already locked."
msgstr ""
#: plugin.py:310
msgid "This Aka is already unlocked."
msgstr ""
#: plugin.py:372
#, docstring
msgid ""
"Add the help for \"@plugin help Aka\" here\n"
" This should describe *how* to use this plugin."
msgstr ""
#: plugin.py:479
msgid "You've attempted more nesting than is currently allowed on this bot."
msgstr ""
#: plugin.py:483
msgid " at least"
msgstr ""
#: plugin.py:492
msgid "Locked by %s at %s"
msgstr ""
#: plugin.py:495
msgid ""
"<an alias,%s %n>\n"
"\n"
"Alias for %q.%s"
msgstr ""
#: plugin.py:496
msgid "argument"
msgstr ""
#: plugin.py:502
msgid "You can't overwrite commands in this plugin."
msgstr ""
#: plugin.py:507
msgid "This Aka has too many spaces in its name."
msgstr ""
#: plugin.py:512
msgid "Can't mix $* and optional args (@1, etc.)"
msgstr ""
#: plugin.py:514
msgid "There can be only one $* in an alias."
msgstr ""
#: plugin.py:521
msgid "This Aka is locked."
msgstr ""
#: plugin.py:525
#, docstring
msgid ""
"[--channel <#channel>] <name> <command>\n"
"\n"
" Defines an alias <name> that executes <command>. The <command>\n"
" should be in the standard \"command argument [nestedcommand argument]\"\n"
" arguments to the alias; they'll be filled with the first, second, etc.\n"
" arguments. $1, $2, etc. can be used for required arguments. @1, @2,\n"
" etc. can be used for optional arguments. $* simply means \"all\n"
" arguments that have not replaced $1, $2, etc.\", ie. it will also\n"
" include optional arguments.\n"
" "
msgstr ""
#: plugin.py:539 plugin.py:565 plugin.py:597 plugin.py:620 plugin.py:643
msgid "%r is not a valid channel."
msgstr ""
#: plugin.py:557
#, docstring
msgid ""
"[--channel <#channel>] <name>\n"
"\n"
" Removes the given alias, if unlocked.\n"
" "
msgstr ""
#: plugin.py:579
#, docstring
msgid ""
"Check if the user has any of the required capabilities to manage\n"
" the regexp database."
msgstr ""
#: plugin.py:589
#, docstring
msgid ""
"[--channel <#channel>] <alias>\n"
"\n"
" Locks an alias so that no one else can change it.\n"
" "
msgstr ""
#: plugin.py:612
#, docstring
msgid ""
"[--channel <#channel>] <alias>\n"
"\n"
" Unlocks an alias so that people can define new aliases over it.\n"
" "
msgstr ""
#: plugin.py:635
#, docstring
msgid ""
"<command>\n"
"\n"
" This command shows the content of an Aka.\n"
" "
msgstr ""
#: plugin.py:652
#, docstring
msgid ""
"takes no arguments\n"
"\n"
" Imports the Alias database into Aka's, and clean the former."
msgstr ""
#: plugin.py:657
msgid "Alias plugin is not loaded."
msgstr ""
#: plugin.py:667
msgid "Error occured when importing the %n: %L"
msgstr ""

View File

@ -1,720 +0,0 @@
###
# Copyright (c) 2013, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import re
import os
import sys
import datetime
import operator
import supybot.conf as conf
import supybot.utils as utils
import supybot.ircdb as ircdb
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Aka')
try:
import sqlite3
except ImportError:
sqlite3 = None
try:
import sqlalchemy
import sqlalchemy.ext
import sqlalchemy.ext.declarative
except ImportError:
sqlalchemy = None
if not (sqlite3 or sqlalchemy):
raise callbacks.Error('You have to install python-sqlite3 or '
'python-sqlalchemy in order to load this plugin.')
available_db = {}
class Alias(object):
__slots__ = ('name', 'alias', 'locked', 'locked_by', 'locked_at')
def __init__(self, name, alias):
self.name = name
self.alias = alias
self.locked = False
self.locked_by = None
self.locked_at = None
def __repr__(self):
return "<Alias('%r', '%r')>" % (self.name, self.alias)
if sqlite3:
class SQLiteAkaDB(object):
__slots__ = ('engines', 'filename', 'dbs',)
def __init__(self, filename):
self.engines = ircutils.IrcDict()
self.filename = filename.replace('sqlite3', 'sqlalchemy')
def close(self):
self.dbs.clear()
def get_db(self, channel):
if channel in self.engines:
engine = self.engines[channel]
else:
filename = plugins.makeChannelFilename(self.filename, channel)
exists = os.path.exists(filename)
engine = sqlite3.connect(filename, check_same_thread=False)
if not exists:
cursor = engine.cursor()
cursor.execute("""CREATE TABLE aliases (
id INTEGER NOT NULL,
name VARCHAR NOT NULL,
alias VARCHAR NOT NULL,
locked BOOLEAN NOT NULL,
locked_by VARCHAR,
locked_at DATETIME,
PRIMARY KEY (id),
UNIQUE (name))""")
engine.commit()
self.engines[channel] = engine
assert engine.execute("select 1").fetchone() == (1,)
return engine
def has_aka(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
return self.get_db(channel).cursor() \
.execute("""SELECT COUNT() as count
FROM aliases WHERE name = ?;""", (name,)) \
.fetchone()[0]
def get_aka_list(self, channel):
cursor = self.get_db(channel).cursor()
cursor.execute("""SELECT name FROM aliases;""")
list_ = cursor.fetchall()
return list_
def get_alias(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
cursor = self.get_db(channel).cursor()
cursor.execute("""SELECT alias FROM aliases
WHERE name = ?;""", (name,))
r = cursor.fetchone()
if r:
return r[0]
else:
return None
def add_aka(self, channel, name, alias):
name = callbacks.canonicalName(name, preserve_spaces=True)
if self.has_aka(channel, name):
raise AkaError(_('This Aka already exists.'))
if sys.version_info[0] < 3:
if isinstance(name, str):
name = name.decode('utf8')
if isinstance(alias, str):
alias = alias.decode('utf8')
db = self.get_db(channel)
cursor = db.cursor()
cursor.execute("""INSERT INTO aliases VALUES (
NULL, ?, ?, 0, NULL, NULL);""", (name, alias))
db.commit()
def remove_aka(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
db.cursor().execute('DELETE FROM aliases WHERE name = ?', (name,))
db.commit()
def lock_aka(self, channel, name, by):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
cursor = db.cursor().execute("""UPDATE aliases
SET locked=1, locked_at=?, locked_by=? WHERE name = ?""",
(datetime.datetime.now(), by, name))
if cursor.rowcount == 0:
raise AkaError(_('This Aka does not exist'))
db.commit()
def unlock_aka(self, channel, name, by):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
cursor = db.cursor()
cursor.execute("""UPDATE aliases SET locked=0, locked_at=?
WHERE name = ?""", (datetime.datetime.now(), name))
if cursor.rowcount == 0:
raise AkaError(_('This Aka does not exist'))
db.commit()
def get_aka_lock(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
cursor = self.get_db(channel).cursor()
cursor.execute("""SELECT locked, locked_by, locked_at
FROM aliases WHERE name = ?;""", (name,))
r = cursor.fetchone()
if r:
return (bool(r[0]), r[1], r[2])
else:
raise AkaError(_('This Aka does not exist'))
available_db.update({'sqlite3': SQLiteAkaDB})
elif sqlalchemy:
Base = sqlalchemy.ext.declarative.declarative_base()
class SQLAlchemyAlias(Alias, Base):
__slots__ = ()
__tablename__ = 'aliases'
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
name = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
alias = sqlalchemy.Column(sqlalchemy.String, nullable=False)
locked = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)
locked_by = sqlalchemy.Column(sqlalchemy.String, nullable=True)
locked_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
# TODO: Add table for usage statistics
class SqlAlchemyAkaDB(object):
__slots__ = ('engines', 'filename', 'sqlalchemy', 'dbs')
def __init__(self, filename):
self.engines = ircutils.IrcDict()
self.filename = filename
self.sqlalchemy = sqlalchemy
def close(self):
self.dbs.clear()
def get_db(self, channel):
if channel in self.engines:
engine = self.engines[channel]
else:
filename = plugins.makeChannelFilename(self.filename, channel)
exists = os.path.exists(filename)
engine = sqlalchemy.create_engine('sqlite:///' + filename)
if not exists:
Base.metadata.create_all(engine)
self.engines[channel] = engine
assert engine.execute("select 1").scalar() == 1
Session = sqlalchemy.orm.sessionmaker()
Session.configure(bind=engine)
return Session()
def has_aka(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
count = self.get_db(channel).query(SQLAlchemyAlias) \
.filter(SQLAlchemyAlias.name == name) \
.count()
return bool(count)
def get_aka_list(self, channel):
list_ = list(self.get_db(channel).query(SQLAlchemyAlias.name))
return list_
def get_alias(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
try:
return self.get_db(channel).query(SQLAlchemyAlias.alias) \
.filter(SQLAlchemyAlias.name == name).one()[0]
except sqlalchemy.orm.exc.NoResultFound:
return None
def add_aka(self, channel, name, alias):
name = callbacks.canonicalName(name, preserve_spaces=True)
if self.has_aka(channel, name):
raise AkaError(_('This Aka already exists.'))
if sys.version_info[0] < 3:
if isinstance(name, str):
name = name.decode('utf8')
if isinstance(alias, str):
alias = alias.decode('utf8')
db = self.get_db(channel)
db.add(SQLAlchemyAlias(name, alias))
db.commit()
def remove_aka(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
db.query(SQLAlchemyAlias).filter(SQLAlchemyAlias.name == name).delete()
db.commit()
def lock_aka(self, channel, name, by):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
try:
aka = db.query(SQLAlchemyAlias) \
.filter(SQLAlchemyAlias.name == name).one()
except sqlalchemy.orm.exc.NoResultFound:
raise AkaError(_('This Aka does not exist'))
if aka.locked:
raise AkaError(_('This Aka is already locked.'))
aka.locked = True
aka.locked_by = by
aka.locked_at = datetime.datetime.now()
db.commit()
def unlock_aka(self, channel, name, by):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
db = self.get_db(channel)
try:
aka = db.query(SQLAlchemyAlias) \
.filter(SQLAlchemyAlias.name == name).one()
except sqlalchemy.orm.exc.NoResultFound:
raise AkaError(_('This Aka does not exist'))
if not aka.locked:
raise AkaError(_('This Aka is already unlocked.'))
aka.locked = False
aka.locked_by = by
aka.locked_at = datetime.datetime.now()
db.commit()
def get_aka_lock(self, channel, name):
name = callbacks.canonicalName(name, preserve_spaces=True)
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
try:
return self.get_db(channel) \
.query(SQLAlchemyAlias.locked, SQLAlchemyAlias.locked_by, SQLAlchemyAlias.locked_at)\
.filter(SQLAlchemyAlias.name == name).one()
except sqlalchemy.orm.exc.NoResultFound:
raise AkaError(_('This Aka does not exist'))
available_db.update({'sqlalchemy': SqlAlchemyAkaDB})
def getArgs(args, required=1, optional=0, wildcard=0):
if len(args) < required:
raise callbacks.ArgumentError
if len(args) < required + optional:
ret = list(args) + ([''] * (required + optional - len(args)))
elif len(args) >= required + optional:
if not wildcard:
ret = list(args[:required + optional - 1])
ret.append(' '.join(args[required + optional - 1:]))
else:
ret = list(args)
return ret
class AkaError(Exception):
pass
class RecursiveAlias(AkaError):
pass
dollarRe = re.compile(r'\$(\d+)')
def findBiggestDollar(alias):
dollars = dollarRe.findall(alias)
dollars = list(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 = list(map(int, ats))
ats.sort()
if ats:
return ats[-1]
else:
return 0
AkaDB = plugins.DB('Aka', available_db)
class Aka(callbacks.Plugin):
"""Aka is improved version of the Alias plugin. It stores akas outside
of the bot.conf and doesn't have risk of corrupting the bot.conf file
which often happens when there are Unicode issues. Aka also
introduces multi-worded akas."""
def __init__(self, irc):
self.__parent = super(Aka, self)
self.__parent.__init__(irc)
self._db = AkaDB()
def isCommandMethod(self, name):
args = name.split(' ')
if '|' in args:
return False
if len(args) > 1 and \
callbacks.canonicalName(args[0]) != self.canonicalName():
for cb in dynamic.irc.callbacks: # including this plugin
if cb.isCommandMethod(' '.join(args[0:-1])):
return False
if sys.version_info[0] < 3 and isinstance(name, str):
name = name.decode('utf8')
channel = dynamic.channel or 'global'
return self._db.has_aka(channel, name) or \
self._db.has_aka('global', name) or \
self.__parent.isCommandMethod(name)
isCommand = isCommandMethod
def listCommands(self):
channel = dynamic.channel or 'global'
return list(set(list(map(callbacks.formatCommand,
self._db.get_aka_list(channel) +
self._db.get_aka_list('global'))) +
['add', 'remove', 'lock', 'unlock', 'importaliasdatabase']))
def getCommand(self, args, check_other_plugins=True):
canonicalName = callbacks.canonicalName
# All the code from here to the 'for' loop is copied from callbacks.py
assert args == list(map(canonicalName, args))
first = args[0]
for cb in self.cbs:
if first == cb.canonicalName():
return cb.getCommand(args[1:])
if first == self.canonicalName() and len(args) > 1:
ret = self.getCommand(args[1:], False)
if ret:
return [first] + ret
max_length = self.registryValue('maximumWordsInName')
for i in xrange(1, min(len(args)+1, max_length)):
if self.isCommandMethod(callbacks.formatCommand(args[0:i])):
return args[0:i]
return []
def getCommandMethod(self, command):
if len(command) == 1 or command[0] == self.canonicalName():
try:
return self.__parent.getCommandMethod(command)
except AttributeError:
pass
name = callbacks.formatCommand(command)
channel = dynamic.channel or 'global'
original = self._db.get_alias(channel, name)
if not original:
original = self._db.get_alias('global', name)
biggestDollar = findBiggestDollar(original)
biggestAt = findBiggestAt(original)
wildcard = '$*' in original
def f(irc, msg, args):
tokens = callbacks.tokenize(original)
if biggestDollar or biggestAt:
args = getArgs(args, required=biggestDollar, optional=biggestAt,
wildcard=wildcard)
max_len = conf.supybot.reply.maximumLength()
args = list([x[:max_len] for x in 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)
maxNesting = conf.supybot.commands.nested.maximum()
if maxNesting and irc.nested+1 > maxNesting:
irc.error(_('You\'ve attempted more nesting than is '
'currently allowed on this bot.'), Raise=True)
self.Proxy(irc, msg, tokens)
if biggestDollar and (wildcard or biggestAt):
flexargs = _(' at least')
else:
flexargs = ''
try:
lock = self._db.get_aka_lock(channel, name)
except AkaError:
lock = self._db.get_aka_lock('global', name)
(locked, locked_by, locked_at) = lock
if locked:
lock = ' ' + _('Locked by %s at %s') % (locked_by, locked_at)
else:
lock = ''
doc = format(_('<an alias,%s %n>\n\nAlias for %q.%s'),
flexargs, (biggestDollar, _('argument')), original, lock)
f = utils.python.changeFunctionName(f, name, doc)
return f
def _add_aka(self, channel, name, alias):
if self.__parent.isCommandMethod(name):
raise AkaError(_('You can\'t overwrite commands in '
'this plugin.'))
if self._db.has_aka(channel, name):
raise AkaError(_('This Aka already exists.'))
if len(name.split(' ')) > self.registryValue('maximumWordsInName'):
raise AkaError(_('This Aka has too many spaces in its name.'))
biggestDollar = findBiggestDollar(alias)
biggestAt = findBiggestAt(alias)
wildcard = '$*' in alias
if biggestAt and wildcard:
raise AkaError(_('Can\'t mix $* and optional args (@1, etc.)'))
if alias.count('$*') > 1:
raise AkaError(_('There can be only one $* in an alias.'))
self._db.add_aka(channel, name, alias)
def _remove_aka(self, channel, name, evenIfLocked=False):
if not evenIfLocked:
(locked, by, at) = self._db.get_aka_lock(channel, name)
if locked:
raise AkaError(_('This Aka is locked.'))
self._db.remove_aka(channel, name)
def add(self, irc, msg, args, optlist, name, alias):
"""[--channel <#channel>] <name> <command>
Defines an alias <name> that executes <command>. The <command>
should be in the standard "command argument [nestedcommand argument]"
arguments to the alias; they'll be filled with the first, second, etc.
arguments. $1, $2, etc. can be used for required arguments. @1, @2,
etc. can be used for optional arguments. $* simply means "all
arguments that have not replaced $1, $2, etc.", ie. it will also
include optional arguments.
"""
channel = 'global'
for (option, arg) in optlist:
if option == 'channel':
if not ircutils.isChannel(arg):
irc.error(_('%r is not a valid channel.') % arg,
Raise=True)
channel = arg
if ' ' not in alias:
# If it's a single word, they probably want $*.
alias += ' $*'
try:
self._add_aka(channel, name, alias)
self.log.info('Adding Aka %r for %r (from %s)',
name, alias, msg.prefix)
irc.replySuccess()
except AkaError as e:
irc.error(str(e))
add = wrap(add, [getopts({
'channel': 'somethingWithoutSpaces',
}), 'something', 'text'])
def set(self, irc, msg, args, optlist, name, alias):
"""[--channel <#channel>] <name> <command>
Overwrites an existing alias <name> to execute <command> instead. The
<command> should be in the standard "command argument [nestedcommand
argument]" arguments to the alias; they'll be filled with the first,
second, etc. arguments. $1, $2, etc. can be used for required
arguments. @1, @2, etc. can be used for optional arguments. $* simply
means "all arguments that have not replaced $1, $2, etc.", ie. it will
also include optional arguments.
"""
channel = 'global'
for (option, arg) in optlist:
if option == 'channel':
if not ircutils.isChannel(arg):
irc.error(_('%r is not a valid channel.') % arg,
Raise=True)
channel = arg
try:
self._remove_aka(channel, name)
except AkaError as e:
irc.error(str(e), Raise=True)
if ' ' not in alias:
# If it's a single word, they probably want $*.
alias += ' $*'
try:
self._add_aka(channel, name, alias)
self.log.info('Setting Aka %r to %r (from %s)',
name, alias, msg.prefix)
irc.replySuccess()
except AkaError as e:
irc.error(str(e))
set = wrap(set, [getopts({
'channel': 'somethingWithoutSpaces',
}), 'something', 'text'])
def remove(self, irc, msg, args, optlist, name):
"""[--channel <#channel>] <name>
Removes the given alias, if unlocked.
"""
channel = 'global'
for (option, arg) in optlist:
if option == 'channel':
if not ircutils.isChannel(arg):
irc.error(_('%r is not a valid channel.') % arg,
Raise=True)
channel = arg
try:
self._remove_aka(channel, name)
self.log.info('Removing Aka %r (from %s)', name, msg.prefix)
irc.replySuccess()
except AkaError as e:
irc.error(str(e))
remove = wrap(remove, [getopts({
'channel': 'somethingWithoutSpaces',
}), 'something'])
def _checkManageCapabilities(self, irc, msg, channel):
"""Check if the user has any of the required capabilities to manage
the regexp database."""
if channel != 'global':
capability = ircdb.makeChannelCapability(channel, 'op')
else:
capability = 'admin'
if not ircdb.checkCapability(msg.prefix, capability):
irc.errorNoCapability(capability, Raise=True)
def lock(self, irc, msg, args, optlist, user, name):
"""[--channel <#channel>] <alias>
Locks an alias so that no one else can change it.
"""
channel = 'global'
for (option, arg) in optlist:
if option == 'channel':
if not ircutils.isChannel(arg):
irc.error(_('%r is not a valid channel.') % arg,
Raise=True)
channel = arg
self._checkManageCapabilities(irc, msg, channel)
try:
self._db.lock_aka(channel, name, user.name)
except AkaError as e:
irc.error(str(e))
else:
irc.replySuccess()
lock = wrap(lock, [getopts({
'channel': 'somethingWithoutSpaces',
}), 'user', 'something'])
def unlock(self, irc, msg, args, optlist, user, name):
"""[--channel <#channel>] <alias>
Unlocks an alias so that people can define new aliases over it.
"""
channel = 'global'
for (option, arg) in optlist:
if option == 'channel':
if not ircutils.isChannel(arg):
irc.error(_('%r is not a valid channel.') % arg,
Raise=True)
channel = arg
self._checkManageCapabilities(irc, msg, channel)
try:
self._db.unlock_aka(channel, name, user.name)
except AkaError as e:
irc.error(str(e))
else:
irc.replySuccess()
unlock = wrap(unlock, [getopts({
'channel': 'somethingWithoutSpaces',
}), 'user', 'something'])
def show(self, irc, msg, args, optlist, name):
"""<command>
This command shows the content of an Aka.
"""
channel = 'global'
for (option, arg) in optlist:
if option == 'channel':
if not ircutils.isChannel(arg):
irc.error(_('%r is not a valid channel.') % arg,
Raise=True)
channel = arg
command = self._db.get_alias(channel, name)
if command:
irc.reply(command)
else:
irc.error(_('This Aka does not exist'))
show = wrap(show, [getopts({'channel': 'somethingWithoutSpaces'}),
'text'])
def importaliasdatabase(self, irc, msg, args):
"""takes no arguments
Imports the Alias database into Aka's, and clean the former."""
alias_plugin = irc.getCallback('Alias')
if alias_plugin is None:
irc.error(_('Alias plugin is not loaded.'), Raise=True)
errors = {}
for (name, (command, locked, func)) in alias_plugin.aliases.items():
try:
self._add_aka('global', name, command)
except AkaError as e:
errors[name] = e.args[0]
else:
alias_plugin.removeAlias(name, evenIfLocked=True)
if errors:
irc.error(format(_('Error occured when importing the %n: %L'),
(len(errors), 'following', 'command'),
['%s (%s)' % x for x in errors.items()]))
else:
irc.replySuccess()
importaliasdatabase = wrap(importaliasdatabase, ['owner'])
Class = Aka
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,230 +0,0 @@
# -*- coding: utf8 -*-
###
# Copyright (c) 2002-2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *
import supybot.conf as conf
import supybot.plugin as plugin
import supybot.registry as registry
import plugin as Aka
class FunctionsTest(SupyTestCase):
def testFindBiggestDollar(self):
self.assertEqual(Aka.findBiggestDollar(''), 0)
self.assertEqual(Aka.findBiggestDollar('foo'), 0)
self.assertEqual(Aka.findBiggestDollar('$0'), 0)
self.assertEqual(Aka.findBiggestDollar('$1'), 1)
self.assertEqual(Aka.findBiggestDollar('$2'), 2)
self.assertEqual(Aka.findBiggestDollar('$2 $10'), 10)
self.assertEqual(Aka.findBiggestDollar('$3'), 3)
self.assertEqual(Aka.findBiggestDollar('$3 $2 $1'), 3)
self.assertEqual(Aka.findBiggestDollar('foo bar $1'), 1)
self.assertEqual(Aka.findBiggestDollar('foo $2 $1'), 2)
self.assertEqual(Aka.findBiggestDollar('foo $0 $1'), 1)
self.assertEqual(Aka.findBiggestDollar('foo $1 $3'), 3)
self.assertEqual(Aka.findBiggestDollar('$10 bar $1'), 10)
class AkaChannelTestCase(ChannelPluginTestCase):
plugins = ('Aka', 'Conditional', 'Filter', 'Math', 'Utilities',
'Format', 'Reply')
def testDoesNotOverwriteCommands(self):
# We don't have dispatcher commands anymore
#self.assertError('aka add aka "echo foo bar baz"')
self.assertError('aka add add "echo foo bar baz"')
self.assertError('aka add remove "echo foo bar baz"')
self.assertError('aka add lock "echo foo bar baz"')
self.assertError('aka add unlock "echo foo bar baz"')
def testAkaHelp(self):
self.assertNotError('aka add slashdot foo')
self.assertRegexp('help slashdot', "Alias for .*foo")
self.assertNotError('aka add nonascii echo éé')
self.assertRegexp('help nonascii', "Alias for .*echo éé")
def testShow(self):
self.assertNotError('aka add foo bar')
self.assertResponse('show foo', 'bar $*')
self.assertNotError('aka add "foo bar" baz')
self.assertResponse('show "foo bar"', 'baz $*')
def testRemove(self):
self.assertNotError('aka add foo echo bar')
self.assertResponse('foo', 'bar')
self.assertNotError('aka remove foo')
self.assertError('foo')
def testDollars(self):
self.assertNotError('aka add rot26 "rot13 [rot13 $1]"')
self.assertResponse('rot26 foobar', 'foobar')
def testMoreDollars(self):
self.assertNotError('aka add rev "echo $3 $2 $1"')
self.assertResponse('rev foo bar baz', 'baz bar foo')
def testAllArgs(self):
self.assertNotError('aka add swap "echo $2 $1 $*"')
self.assertResponse('swap 1 2 3 4 5', '2 1 3 4 5')
self.assertError('aka add foo "echo $1 @1 $*"')
self.assertNotError('aka add moo echo $1 $*')
self.assertError('moo')
self.assertResponse('moo foo', 'foo')
self.assertResponse('moo foo bar', 'foo bar')
self.assertNotError('aka add spam "echo [echo $*]"')
self.assertResponse('spam egg', 'egg')
self.assertResponse('spam egg bacon', 'egg bacon')
def testChannel(self):
self.assertNotError('aka add channel echo $channel')
self.assertResponse('aka channel', self.channel)
def testAddRemoveAka(self):
cb = self.irc.getCallback('Aka')
cb._add_aka('global', 'foobar', 'echo sbbone')
cb._db.lock_aka('global', 'foobar', 'evil_admin')
self.assertResponse('foobar', 'sbbone')
self.assertRegexp('list Aka', 'foobar')
self.assertRaises(Aka.AkaError, cb._remove_aka, 'global', 'foobar')
cb._remove_aka('global', 'foobar', evenIfLocked=True)
self.assertNotRegexp('list Aka', 'foobar')
self.assertError('foobar')
def testOptionalArgs(self):
self.assertNotError('aka add myrepr "repr @1"')
self.assertResponse('myrepr foo', '"foo"')
self.assertResponse('myrepr ""', '""')
def testNoExtraSpaces(self):
self.assertNotError('aka add foo "action takes $1\'s money"')
self.assertResponse('foo bar', '\x01ACTION takes bar\'s money\x01')
def testNoExtraQuotes(self):
self.assertNotError('aka add myre "echo s/$1/$2/g"')
self.assertResponse('myre foo bar', 's/foo/bar/g')
def testSimpleAkaWithoutArgsImpliesDollarStar(self):
self.assertNotError('aka add exo echo')
self.assertResponse('exo foo bar baz', 'foo bar baz')
def testChannelPriority(self):
self.assertNotError('aka add spam "echo foo"')
self.assertNotError('aka add --channel %s spam "echo bar"' %
self.channel)
self.assertResponse('spam', 'bar')
self.assertNotError('aka add --channel %s egg "echo baz"' %
self.channel)
self.assertNotError('aka add egg "echo qux"')
self.assertResponse('egg', 'baz')
def testComplicatedNames(self):
self.assertNotError(u'aka add café "echo coffee"')
self.assertResponse(u'café', 'coffee')
self.assertNotError('aka add "foo bar" "echo spam"')
self.assertResponse('foo bar', 'spam')
self.assertNotError('aka add "foo" "echo egg"')
self.assertResponse('foo', 'egg')
# You could expect 'spam' here, but in fact, this is dangerous.
# Just imagine this session:
# <evil_user> aka add "echo foo" quit
# <bot> The operation succeeded.
# ...
# <owner> echo foo
# * bot has quit
self.assertResponse('foo bar', 'egg')
def testNoOverride(self):
self.assertNotError('aka add "echo foo" "echo bar"')
self.assertResponse('echo foo', 'foo')
self.assertNotError('aka add foo "echo baz"')
self.assertNotError('aka add "foo bar" "echo qux"')
self.assertResponse('foo bar', 'baz')
def testRecursivity(self):
self.assertNotError('aka add fact '
r'"cif [nceq $1 0] \"echo 1\" '
r'\"calc $1 * [fact [calc $1 - 1]]\""')
self.assertResponse('fact 4', '24')
self.assertRegexp('fact 50', 'more nesting')
def testDollarStarNesting(self):
self.assertNotError('aka add alias aka $*')
self.assertNotError('alias add a+ aka add $*')
class AkaTestCase(PluginTestCase):
plugins = ('Aka', 'Alias', 'User', 'Utilities')
def testMaximumLength(self):
self.assertNotError('aka add "foo bar baz qux quux" "echo test"')
self.assertError('aka add "foo bar baz qux quux corge" "echo test"')
def testAkaLockedHelp(self):
self.assertNotError('register evil_admin foo')
self.assertNotError('aka add slashdot foo')
self.assertRegexp('help aka slashdot', "Alias for .*foo")
self.assertNotRegexp('help aka slashdot', 'Locked by')
self.assertNotError('aka lock slashdot')
self.assertRegexp('help aka slashdot', 'Locked by evil_admin')
self.assertNotError('aka unlock slashdot')
self.assertNotRegexp('help aka slashdot', 'Locked by')
def testAliasImport(self):
self.assertNotError('alias add foo "echo bar"')
self.assertNotError(u'alias add baz "echo café"')
self.assertNotError('aka add qux "echo quux"')
self.assertResponse('alias foo', 'bar')
self.assertResponse('alias baz', 'café')
self.assertRegexp('aka foo', 'there is no command named')
self.assertResponse('aka qux', 'quux')
self.assertNotError('aka importaliasdatabase')
self.assertRegexp('alias foo', 'there is no command named')
self.assertResponse('aka foo', 'bar')
self.assertResponse('aka baz', 'café')
self.assertResponse('aka qux', 'quux')
self.assertNotError('alias add foo "echo test"')
self.assertNotError('alias add spam "echo egg"')
self.assertNotError('alias lock spam')
self.assertRegexp('aka importaliasdatabase',
r'the 1 following command: foo \(This Aka already exists.\)$')
self.assertResponse('aka foo', 'bar')
self.assertResponse('alias foo', 'test')
self.assertRegexp('alias spam', 'there is no command named')
self.assertResponse('aka spam', 'egg')
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,27 +0,0 @@
This plugin allows the user to create various aliases to other commands
or combinations of other commands (via nested commands). It is a good
idea to always quote the commands that are being aliased so that any
nested commands are not immediately run.
Basic usage
-----------
Add an alias, `trout`, which expects a word as an argument
```
<jamessan> @alias add trout "action slaps $1 with a large trout"
<bot> jamessan: The operation succeeded.
<jamessan> @trout me
* bot slaps me with a large trout
```
Add an alias, `lastfm`, which expects a last.fm user and replies with
their recently played items.
```
@alias add lastfm "rss [format concat http://ws.audioscrobbler.com/1.0/user/ [format concat [urlquote $1] /recenttracks.rss]]"
```
Note that if the nested commands being aliased hadn't been quoted, then
those commands would have been run immediately, and `@lastfm` would always
reply with the same information, the result of those commands.

View File

@ -1,62 +0,0 @@
###
# Copyright (c) 2005, 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. NOTE THAT IT'S RECOMMENDED TO USE Aka
PLUGIN INSTEAD!
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = ""
__author__ = supybot.authors.jemfinch
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
from plugin import findBiggestDollar, AliasError, escapeAlias, unescapeAlias # for the tests.
if world.testing:
import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,47 +0,0 @@
###
# Copyright (c) 2005, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Alias')
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself 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('Alias', True)
Alias = conf.registerPlugin('Alias')
conf.registerGroup(Alias, 'aliases')
conf.registerGroup(Alias, 'escapedaliases')
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -1,104 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Supybot\n"
"POT-Creation-Date: 2012-02-15 23:03+CET\n"
"PO-Revision-Date: 2012-04-27 15:36+0200\n"
"Last-Translator: Mikaela Suomalainen <mikaela.suomalainen@outlook.com>\n"
"Language-Team: German <fbesser@gmail.com>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Poedit-Language: German\n"
"X-Poedit-Country: GERMANY\n"
#: plugin.py:45
msgid ""
"Returns the channel the msg came over or the channel given in args.\n"
"\n"
" If the channel was given in args, args is modified (the channel is\n"
" removed).\n"
" "
msgstr ""
"Gibt den Kanal aus über den die Nachricht kam oder der Kanal der in den Argumenten gegeben wurde.\n"
"\n"
"Falls der Kanal in den Argumenten angegeben wurde, werden die Argumente bearbeitet (der Kanal wird entfernt."
#: plugin.py:164
msgid " at least"
msgstr "mindestens"
#: plugin.py:165
msgid ""
"<an alias,%s %n>\n"
"\n"
"Alias for %q."
msgstr ""
"<ein Alias, %s %n>\n"
"\n"
"Alias für %q."
#: plugin.py:166
msgid "argument"
msgstr "Argument"
#: plugin.py:220
msgid ""
"<alias>\n"
"\n"
" Locks an alias so that no one else can change it.\n"
" "
msgstr ""
"<Alias>\n"
"\n"
"Versperrt ein Alias, sodass er nicht verändert werden kann."
#: plugin.py:229
#: plugin.py:243
msgid "There is no such alias."
msgstr "Es gibt keinen Alias mit diesem Namen."
#: plugin.py:234
msgid ""
"<alias>\n"
"\n"
" Unlocks an alias so that people can define new aliases over it.\n"
" "
msgstr ""
"<Alias>\n"
"\n"
"Entsperrt den Alias, sodass andere Personen ihn verändern können."
#: plugin.py:254
msgid "That name isn't valid. Try %q instead."
msgstr "Dieser Name ist nicht zulässig. Probiere anstatt %q."
#: plugin.py:292
#, fuzzy
msgid ""
"<name> <command>\n"
"\n"
" Defines an alias <name> that executes <command>. The <command>\n"
" should be in the standard \"command argument [nestedcommand argument]\"\n"
" arguments to the alias; they'll be filled with the first, second, etc.\n"
" arguments. $1, $2, etc. can be used for required arguments. @1, @2,\n"
" etc. can be used for optional arguments. $* simply means \"all\n"
" remaining arguments,\" and cannot be combined with optional arguments.\n"
" "
msgstr ""
"<Name> <Alias>\n"
"\n"
"Definiert einen Alias <Name> der <Alias> ausführt. <Alias> sollte in der Standardform \"Befehl Argument [verschachtelter Befehl Argument\" angegeben werden."
#: plugin.py:315
msgid ""
"<name>\n"
"\n"
" Removes the given alias, if unlocked.\n"
" "
msgstr ""
"<Name>\n"
"\n"
"Entfernt den gegeben Alias, falls er nicht gesperrt ist."

View File

@ -1,138 +0,0 @@
# Alias plugin in Limnoria.
# Copyright (C) 2011, 2012 Limnoria
# Mikaela Suomalainen <mkaysi@outlook.com>, 2011, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: Supybot Alias plugin\n"
"POT-Creation-Date: 2014-03-22 12:41+EET\n"
"PO-Revision-Date: 2014-03-22 15:28+0200\n"
"Last-Translator: Mikaela Suomalainen <mikaela.suomalainen@outlook.com>\n"
"Language-Team: suomi <>\n"
"Language: fi_FI\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 1.5.4\n"
#: plugin.py:46
msgid ""
"Returns the channel the msg came over or the channel given in args.\n"
"\n"
" If the channel was given in args, args is modified (the channel is\n"
" removed).\n"
" "
msgstr ""
"Palauttaa kanavan, jolta viesti tuli tai kanavan, joka on annettu "
"parametreissä.\n"
"\n"
" Jos kanava annetaan parametreissä, parametriä muokataan (kanava\n"
" poistetaan).\n"
" "
#: plugin.py:108
#, fuzzy
msgid ""
"Encodes [a-z0-9.]+ into [a-z][a-z0-9].\n"
" Format: a<number of escaped chars>a(<index>d)+<word without dots>."
msgstr ""
"Salaa [a-z0-9.]+ sisään [a-z][a-z0-9].\n"
" Muoto: a<ohitettujen merkkien määrä>a(<index>d)+<sana ilman pisteitä>."
#: plugin.py:219
msgid "You've attempted more nesting than is currently allowed on this bot."
msgstr ""
"Yritit sisällyttää useampia komentoja, kuin tämä botti sallii juuri nyt."
#: plugin.py:224
msgid " at least"
msgstr "vähintään"
#: plugin.py:226 plugin.py:231
msgid ""
"<an alias,%s %n>\n"
"\n"
"Alias for %q."
msgstr ""
"<alias,%s %n>\n"
"\n"
"Alias %q:lle."
#: plugin.py:227 plugin.py:232
msgid "argument"
msgstr "parametri"
#: plugin.py:300
msgid ""
"<alias>\n"
"\n"
" Locks an alias so that no one else can change it.\n"
" "
msgstr ""
"<alias>\n"
"\n"
" Lukitsee aliaksen, niin ettei kukaan muu voi muuttaa sitä.\n"
" "
#: plugin.py:309 plugin.py:323
msgid "There is no such alias."
msgstr "Tuollaista aliasta ei ole."
#: plugin.py:314
msgid ""
"<alias>\n"
"\n"
" Unlocks an alias so that people can define new aliases over it.\n"
" "
msgstr ""
"<alias>\n"
"\n"
" Poistaa lukituksen aliaksesta, jotta ihmiset vouvat määrittää uusia "
"aliaksia sen päälle.\n"
" "
#: plugin.py:335
msgid "That name isn't valid. Try %q instead."
msgstr "Tuo nimi ei ole kelvollinen. Yritä sen sijaan %q:ta."
#: plugin.py:383
msgid ""
"<name> <command>\n"
"\n"
" Defines an alias <name> that executes <command>. The <command>\n"
" should be in the standard \"command argument [nestedcommand "
"argument]\"\n"
" arguments to the alias; they'll be filled with the first, second, "
"etc.\n"
" arguments. $1, $2, etc. can be used for required arguments. @1, "
"@2,\n"
" etc. can be used for optional arguments. $* simply means \"all\n"
" remaining arguments,\" and cannot be combined with optional "
"arguments.\n"
" "
msgstr ""
"<nimi> <alias>\n"
"\n"
" Määrittää aliaksen <nimi>, joka suorittaa <komennon>. <Aliaksen>\n"
" pitäisi olla tavallinen \"komento parametri [sisäkkäinen komento "
"parametrit]\"\n"
" parametrejä aliakselle; ne täytetään ensimmäinen, toinen, jne.\n"
" Parametrit. $1, $2, jne. ovat vaadittava parametrejä. @1, @2,\n"
" jne. ovat valinnaisia parametrejä. $* tarkoittaa yksinkertaisesti "
"\"kaikki\n"
" jäljellä olevat parametrit,\" ja johon ei voida yhdistää vaihtoehtoisia "
"parametrejä.\n"
" "
#: plugin.py:406
msgid ""
"<name>\n"
"\n"
" Removes the given alias, if unlocked.\n"
" "
msgstr ""
"<nimi>\n"
"\n"
" Poistaa annetun aliaksen jos se ei ole lukittu.\n"
" "

View File

@ -1,123 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Limnoria\n"
"POT-Creation-Date: 2014-01-22 13:38+CET\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: Limnoria <progval@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-SourceCharset: ASCII\n"
"X-Generator: Poedit 1.5.4\n"
"Language: fr\n"
#: plugin.py:46
msgid ""
"Returns the channel the msg came over or the channel given in args.\n"
"\n"
" If the channel was given in args, args is modified (the channel is\n"
" removed).\n"
" "
msgstr ""
"Retourne le canal duquel vient le message ou le canal donné en argument.\n"
"\n"
"Si le canal était donné en argument, args est modifié (le canal est "
"supprimé)."
#: plugin.py:108
msgid ""
"Encodes [a-z0-9.]+ into [a-z][a-z0-9].\n"
" Format: a<number of escaped chars>a(<index>d)+<word without dots>."
msgstr "."
#: plugin.py:219
msgid "You've attempted more nesting than is currently allowed on this bot."
msgstr ""
"Vous avez essayé dutiliser plus dimbrication que ce qui est actuellement "
"autorisé sur ce bot."
#: plugin.py:224
msgid " at least"
msgstr "au moins"
#: plugin.py:226 plugin.py:231
msgid ""
"<an alias,%s %n>\n"
"\n"
"Alias for %q."
msgstr ""
"<un alias,%s %n>\n"
"\n"
"Alias pour %q."
#: plugin.py:227 plugin.py:232
msgid "argument"
msgstr "argument"
#: plugin.py:300
msgid ""
"<alias>\n"
"\n"
" Locks an alias so that no one else can change it.\n"
" "
msgstr ""
"<alias>\n"
"\n"
"Vérouille un alias pour que personne d'autre ne puisse le changer."
#: plugin.py:309 plugin.py:323
msgid "There is no such alias."
msgstr "Cet alias n'existe pas."
#: plugin.py:314
msgid ""
"<alias>\n"
"\n"
" Unlocks an alias so that people can define new aliases over it.\n"
" "
msgstr ""
"<alias>\n"
"\n"
"Déverrouille un alias de façon à ce que des gens puissent le redéfinir."
#: plugin.py:335
msgid "That name isn't valid. Try %q instead."
msgstr "Ce nom n'est pas valide. Essayez plutôt %q."
#: plugin.py:383
msgid ""
"<name> <command>\n"
"\n"
" Defines an alias <name> that executes <command>. The <command>\n"
" should be in the standard \"command argument [nestedcommand "
"argument]\"\n"
" arguments to the alias; they'll be filled with the first, second, "
"etc.\n"
" arguments. $1, $2, etc. can be used for required arguments. @1, "
"@2,\n"
" etc. can be used for optional arguments. $* simply means \"all\n"
" remaining arguments,\" and cannot be combined with optional "
"arguments.\n"
" "
msgstr ""
"<nom> <alias>\n"
"\n"
"Défini un alias <nom> qui exécute <alias>. L'<alias> peut être dans le "
"standard \"commande argument [commandeimbriquee argument]\". Les arguments "
"donnés à l'alias doivent être donnés dans l'ordre. Vous pouvez utiliser $1, "
"$2, etc pour symboliser les arguments obligatoires qui seront donnés à "
"l'alias, et @1, @2, etc pour symboliser ceux optionnels. $* signifie "
"simplement *tous* les arguments restants, et ne peut être combiné avec des "
"arguments optionnels."
#: plugin.py:406
msgid ""
"<name>\n"
"\n"
" Removes the given alias, if unlocked.\n"
" "
msgstr ""
"<nom>\n"
"\n"
"Supprime l'alias donné, si il n'est pas verrouillé."

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