limnoria-2018.01.25/0000755000175000017500000000000013233426077013321 5ustar valval00000000000000limnoria-2018.01.25/PKG-INFO0000644000175000017500000000340513233426077014420 0ustar valval00000000000000Metadata-Version: 1.1 Name: limnoria Version: 2018.01.25 Summary: A modified version of Supybot (an IRC bot and framework) Home-page: https://github.com/ProgVal/Limnoria Author: Valentin Lorentz Author-email: progval+limnoria@progval.net License: UNKNOWN Download-URL: https://pypi.python.org/pypi/limnoria Description: A robust, full-featured Python IRC bot with a clean and flexible plugin API. Equipped with a complete ACL system for specifying user permissions with as much as per-command granularity. Batteries are included in the form of numerous plugins already written. Platform: linux Platform: linux2 Platform: win32 Platform: cygwin Platform: darwin Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Natural Language :: Finnish Classifier: Natural Language :: French Classifier: Natural Language :: Hungarian Classifier: Natural Language :: Italian Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Communications :: Chat :: Internet Relay Chat Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides: supybot limnoria-2018.01.25/setup.py0000644000175000017500000002264013233426066015035 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import sys import time import warnings import datetime import tempfile import subprocess from math import ceil warnings.filterwarnings('always', category=DeprecationWarning) debug = '--debug' in sys.argv path = os.path.dirname(__file__) if debug: print('DEBUG: Changing dir from %r to %r' % (os.getcwd(), path)) if path: os.chdir(path) VERSION_FILE = os.path.join('src', 'version.py') version = None try: proc = subprocess.Popen('git show HEAD --format=%ct', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) date = proc.stdout.readline() if sys.version_info[0] >= 3: date = date.decode() version = ".".join(str(i).zfill(2) for i in time.strptime(time.asctime(time.gmtime(int(date.strip()))))[:3]) except: if os.path.isfile(VERSION_FILE): from src.version import version else: from time import gmtime, strftime version = 'installed on ' + strftime("%Y-%m-%dT%H-%M-%S", gmtime()) try: os.unlink(VERSION_FILE) except OSError: # Does not exist pass if version: fd = open(os.path.join('src', 'version.py'), 'a') fd.write("version = '%s'\n" % version) fd.write('try: # For import from setup.py\n') fd.write(' import supybot.utils.python\n') fd.write(' supybot.utils.python._debug_software_version = version\n') fd.write('except ImportError:\n') fd.write(' pass\n') fd.close() if sys.version_info < (2, 6, 0): sys.stderr.write("Supybot requires Python 2.6 or newer.") sys.stderr.write(os.linesep) sys.exit(-1) import textwrap clean = False while '--clean' in sys.argv: clean = True sys.argv.remove('--clean') import glob import shutil import os plugins = [s for s in os.listdir('plugins') if os.path.exists(os.path.join('plugins', s, 'plugin.py'))] def normalizeWhitespace(s): return ' '.join(s.split()) try: from distutils.core import setup from distutils.sysconfig import get_python_lib except ImportError as e: s = normalizeWhitespace("""Supybot requires the distutils package to install. This package is normally included with Python, but for some unfathomable reason, many distributions to take it out of standard Python and put it in another package, usually caled 'python-dev' or python-devel' or something similar. This is one of the dumbest things a distribution can do, because it means that developers cannot rely on *STANDARD* Python modules to be present on systems of that distribution. Complain to your distribution, and loudly. If you how much of our time we've wasted telling people to install what should be included by default with Python you'd understand why we're unhappy about this. Anyway, to reiterate, install the development package for Python that your distribution supplies.""") sys.stderr.write(os.linesep*2) sys.stderr.write(textwrap.fill(s)) sys.stderr.write(os.linesep*2) sys.exit(-1) if clean: previousInstall = os.path.join(get_python_lib(), 'supybot') if os.path.exists(previousInstall): try: print('Removing current installation.') shutil.rmtree(previousInstall) except Exception as e: print('Couldn\'t remove former installation: %s' % e) sys.exit(-1) packages = ['supybot', 'supybot.locales', 'supybot.utils', 'supybot.drivers', 'supybot.plugins',] + \ ['supybot.plugins.'+s for s in plugins] + \ [ 'supybot.plugins.Dict.local', 'supybot.plugins.Math.local', ] package_dir = {'supybot': 'src', 'supybot.utils': 'src/utils', 'supybot.locales': 'locales', 'supybot.plugins': 'plugins', 'supybot.drivers': 'src/drivers', 'supybot.plugins.Dict.local': 'plugins/Dict/local', 'supybot.plugins.Math.local': 'plugins/Math/local', } package_data = {'supybot.locales': [s for s in os.listdir('locales/')]} for plugin in plugins: package_dir['supybot.plugins.' + plugin] = 'plugins/' + plugin locales_path = 'plugins/' + plugin + '/locales/' locales_name = 'supybot.plugins.'+plugin if os.path.exists(locales_path): package_data.update({locales_name: ['locales/'+s for s in os.listdir(locales_path)]}) setup( # Metadata name='limnoria', provides=['supybot'], version=version, author='Valentin Lorentz', url='https://github.com/ProgVal/Limnoria', author_email='progval+limnoria@progval.net', download_url='https://pypi.python.org/pypi/limnoria', description='A modified version of Supybot (an IRC bot and framework)', platforms=['linux', 'linux2', 'win32', 'cygwin', 'darwin'], long_description=normalizeWhitespace("""A robust, full-featured Python IRC bot with a clean and flexible plugin API. Equipped with a complete ACL system for specifying user permissions with as much as per-command granularity. Batteries are included in the form of numerous plugins already written."""), classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Natural Language :: Finnish', 'Natural Language :: French', 'Natural Language :: Hungarian', 'Natural Language :: Italian', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Communications :: Chat :: Internet Relay Chat', 'Topic :: Software Development :: Libraries :: Python Modules', ], # Installation data packages=packages, package_dir=package_dir, package_data=package_data, scripts=['scripts/supybot', 'scripts/supybot-test', 'scripts/supybot-botchk', 'scripts/supybot-wizard', 'scripts/supybot-adduser', 'scripts/supybot-plugin-doc', 'scripts/supybot-plugin-create', ], data_files=[('share/man/man1', ['man/supybot.1']), ('share/man/man1', ['man/supybot-test.1']), ('share/man/man1', ['man/supybot-botchk.1']), ('share/man/man1', ['man/supybot-wizard.1']), ('share/man/man1', ['man/supybot-adduser.1']), ('share/man/man1', ['man/supybot-plugin-doc.1']), ('share/man/man1', ['man/supybot-plugin-create.1']), ], ) if sys.version_info < (2, 7, 9): warnings.warn('Running Limnoria on Python older than 2.7.9 is not ' 'recommended because it does not support SSL ' 'certificate verification. For more informations, see: ' '', DeprecationWarning) elif sys.version_info < (3, 0): pass # fine, for the moment elif sys.version_info < (3, 4): warnings.warn('Running Limnoria on Python 3.2 or 3.3 is not ' 'recommended because these versions do not support SSL ' 'certificate verification. For more informations, see: ' '', DeprecationWarning) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/0000755000175000017500000000000013233426077014300 5ustar valval00000000000000limnoria-2018.01.25/test/test_yn.py0000644000175000017500000000746613233426066016352 0ustar valval00000000000000### # Copyright (c) 2014, Artur Krysiak # 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 unittest from supybot import questions from supybot.test import SupyTestCase if sys.version_info >= (2, 7, 0): skipif = unittest.skipIf else: skipif = lambda x, y: lambda z:None try: from unittest import mock # Python 3.3+ except ImportError: try: import mock # Everything else, an external 'mock' library except ImportError: mock = None # so complicated construction because I want to # gain the string 'y' instead of the character 'y' # the reason of usage this construction is to prove # that comparing strings by 'is' is wrong # better solution is usage of '==' operator ;) _yes_answer = ''.join(['', 'y']) @skipif(mock is None, 'python-mock is not installed.') class TestYn(SupyTestCase): def test_default_yes_selected(self): questions.expect = mock.Mock(return_value=_yes_answer) answer = questions.yn('up', default='y') self.assertTrue(answer) def test_default_no_selected(self): questions.expect = mock.Mock(return_value='n') answer = questions.yn('up', default='n') self.assertFalse(answer) def test_yes_selected_without_defaults(self): questions.expect = mock.Mock(return_value=_yes_answer) answer = questions.yn('up') self.assertTrue(answer) def test_no_selected_without_defaults(self): questions.expect = mock.Mock(return_value='n') answer = questions.yn('up') self.assertFalse(answer) def test_no_selected_with_default_yes(self): questions.expect = mock.Mock(return_value='n') answer = questions.yn('up', default='y') self.assertFalse(answer) def test_yes_selected_with_default_yes(self): questions.expect = mock.Mock(return_value=_yes_answer) answer = questions.yn('up', default='y') self.assertTrue(answer) def test_yes_selected_with_default_no(self): questions.expect = mock.Mock(return_value=_yes_answer) answer = questions.yn('up', default='n') self.assertTrue(answer) def test_no_selected_with_default_no(self): questions.expect = mock.Mock(return_value='n') answer = questions.yn('up', default='n') self.assertFalse(answer) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_utils.py0000644000175000017500000012510613233426066017054 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009,2011, James McCoy # 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 sys import time import pickle import supybot.utils as utils from supybot.utils.structures import * import supybot.utils.minisix as minisix if sys.version_info[0] >= 0: xrange = range class UtilsTest(SupyTestCase): def testReversed(self): L = list(range(10)) revL = list(reversed(L)) L.reverse() self.assertEqual(L, revL, 'reversed didn\'t return reversed list') for _ in reversed([]): self.fail('reversed caused iteration over empty sequence') class SeqTest(SupyTestCase): def testRenumerate(self): for i in xrange(5): L = list(enumerate(range(i))) LL = list(utils.seq.renumerate(range(i))) self.assertEqual(L, LL[::-1]) def testWindow(self): L = list(range(10)) def wwindow(*args): return list(utils.seq.window(*args)) self.assertEqual(wwindow([], 1), [], 'Empty sequence, empty window') self.assertEqual(wwindow([], 2), [], 'Empty sequence, empty window') self.assertEqual(wwindow([], 5), [], 'Empty sequence, empty window') self.assertEqual(wwindow([], 100), [], 'Empty sequence, empty window') self.assertEqual(wwindow(L, 1), [[x] for x in L], 'Window length 1') self.assertRaises(ValueError, wwindow, [], 0) self.assertRaises(ValueError, wwindow, [], -1) class GenTest(SupyTestCase): def testInsensitivePreservingDict(self): ipd = utils.InsensitivePreservingDict d = ipd(dict(Foo=10)) self.failUnless(d['foo'] == 10) self.assertEqual(d.keys(), ['Foo']) self.assertEqual(d.get('foo'), 10) self.assertEqual(d.get('Foo'), 10) def testFindBinaryInPath(self): if os.name == 'posix': self.assertEqual(None, utils.findBinaryInPath('asdfhjklasdfhjkl')) self.failUnless(utils.findBinaryInPath('sh').endswith('/bin/sh')) def testExnToString(self): try: raise KeyError(1) except Exception as e: self.assertEqual(utils.exnToString(e), 'KeyError: 1') try: raise EOFError() except Exception as e: self.assertEqual(utils.exnToString(e), 'EOFError') def testSaltHash(self): s = utils.saltHash('jemfinch') (salt, hash) = s.split('|') self.assertEqual(utils.saltHash('jemfinch', salt=salt), s) def testSafeEval(self): for s in ['1', '()', '(1,)', '[]', '{}', '{1:2}', '{1:(2,3)}', '1.0', '[1,2,3]', 'True', 'False', 'None', '(True,False,None)', '"foo"', '{"foo": "bar"}']: self.assertEqual(eval(s), utils.safeEval(s)) for s in ['lambda: 2', 'import foo', 'foo.bar']: self.assertRaises(ValueError, utils.safeEval, s) def testSafeEvalTurnsSyntaxErrorIntoValueError(self): self.assertRaises(ValueError, utils.safeEval, '/usr/local/') def testIterableMap(self): class alist(utils.IterableMap): def __init__(self): self.L = [] def __setitem__(self, key, value): self.L.append((key, value)) def items(self): for (k, v) in self.L: yield (k, v) AL = alist() self.failIf(AL) AL[1] = 2 AL[2] = 3 AL[3] = 4 self.failUnless(AL) self.assertEqual(list(AL.items()), [(1, 2), (2, 3), (3, 4)]) self.assertEqual(list(AL.items()), [(1, 2), (2, 3), (3, 4)]) self.assertEqual(list(AL.keys()), [1, 2, 3]) if minisix.PY2: self.assertEqual(list(AL.keys()), [1, 2, 3]) self.assertEqual(list(AL.values()), [2, 3, 4]) self.assertEqual(list(AL.values()), [2, 3, 4]) self.assertEqual(len(AL), 3) def testSortBy(self): L = ['abc', 'z', 'AD'] utils.sortBy(len, L) self.assertEqual(L, ['z', 'AD', 'abc']) utils.sortBy(str.lower, L) self.assertEqual(L, ['abc', 'AD', 'z']) L = ['supybot', 'Supybot'] utils.sortBy(str.lower, L) self.assertEqual(L, ['supybot', 'Supybot']) def testSorted(self): L = ['a', 'c', 'b'] self.assertEqual(sorted(L), ['a', 'b', 'c']) self.assertEqual(L, ['a', 'c', 'b']) self.assertEqual(sorted(L, reverse=True), ['c', 'b', 'a']) def testTimeElapsed(self): self.assertRaises(ValueError, utils.timeElapsed, 0, leadingZeroes=False, seconds=False) then = 0 now = 0 for now, expected in [(0, '0 seconds'), (1, '1 second'), (60, '1 minute and 0 seconds'), (61, '1 minute and 1 second'), (62, '1 minute and 2 seconds'), (122, '2 minutes and 2 seconds'), (3722, '1 hour, 2 minutes, and 2 seconds'), (7322, '2 hours, 2 minutes, and 2 seconds'), (90061,'1 day, 1 hour, 1 minute, and 1 second'), (180122, '2 days, 2 hours, 2 minutes, ' 'and 2 seconds')]: self.assertEqual(utils.timeElapsed(now - then), expected) def timeElapsedShort(self): self.assertEqual(utils.timeElapsed(123, short=True), '2m 3s') def testAbbrev(self): L = ['abc', 'bcd', 'bbe', 'foo', 'fool'] d = utils.abbrev(L) def getItem(s): return d[s] self.assertRaises(KeyError, getItem, 'f') self.assertRaises(KeyError, getItem, 'fo') self.assertRaises(KeyError, getItem, 'b') self.assertEqual(d['bb'], 'bbe') self.assertEqual(d['bc'], 'bcd') self.assertEqual(d['a'], 'abc') self.assertEqual(d['ab'], 'abc') self.assertEqual(d['fool'], 'fool') self.assertEqual(d['foo'], 'foo') def testAbbrevFailsWithDups(self): L = ['english', 'english'] self.assertRaises(ValueError, utils.abbrev, L) class StrTest(SupyTestCase): def testRsplit(self): rsplit = utils.str.rsplit self.assertEqual(rsplit('foo bar baz'), 'foo bar baz'.split()) self.assertEqual(rsplit('foo bar baz', maxsplit=1), ['foo bar', 'baz']) self.assertEqual(rsplit('foo bar baz', maxsplit=1), ['foo bar', 'baz']) self.assertEqual(rsplit('foobarbaz', 'bar'), ['foo', 'baz']) def testMatchCase(self): f = utils.str.matchCase self.assertEqual('bar', f('foo', 'bar')) self.assertEqual('Bar', f('Foo', 'bar')) self.assertEqual('BAr', f('FOo', 'bar')) self.assertEqual('BAR', f('FOO', 'bar')) self.assertEqual('bAR', f('fOO', 'bar')) self.assertEqual('baR', f('foO', 'bar')) self.assertEqual('BaR', f('FoO', 'bar')) def testPluralize(self): f = utils.str.pluralize self.assertEqual('bikes', f('bike')) self.assertEqual('BIKES', f('BIKE')) self.assertEqual('matches', f('match')) self.assertEqual('Patches', f('Patch')) self.assertEqual('fishes', f('fish')) self.assertEqual('tries', f('try')) self.assertEqual('days', f('day')) def testDepluralize(self): f = utils.str.depluralize self.assertEqual('bike', f('bikes')) self.assertEqual('Bike', f('Bikes')) self.assertEqual('BIKE', f('BIKES')) self.assertEqual('match', f('matches')) self.assertEqual('Match', f('Matches')) self.assertEqual('fish', f('fishes')) self.assertEqual('try', f('tries')) def testDistance(self): self.assertEqual(utils.str.distance('', ''), 0) self.assertEqual(utils.str.distance('a', 'b'), 1) self.assertEqual(utils.str.distance('a', 'a'), 0) self.assertEqual(utils.str.distance('foobar', 'jemfinch'), 8) self.assertEqual(utils.str.distance('a', 'ab'), 1) self.assertEqual(utils.str.distance('foo', ''), 3) self.assertEqual(utils.str.distance('', 'foo'), 3) self.assertEqual(utils.str.distance('appel', 'nappe'), 2) self.assertEqual(utils.str.distance('nappe', 'appel'), 2) def testSoundex(self): L = [('Euler', 'E460'), ('Ellery', 'E460'), ('Gauss', 'G200'), ('Ghosh', 'G200'), ('Hilbert', 'H416'), ('Heilbronn', 'H416'), ('Knuth', 'K530'), ('Kant', 'K530'), ('Lloyd', 'L300'), ('Ladd', 'L300'), ('Lukasiewicz', 'L222'), ('Lissajous', 'L222')] for (name, key) in L: soundex = utils.str.soundex(name) self.assertEqual(soundex, key, '%s was %s, not %s' % (name, soundex, key)) self.assertRaises(ValueError, utils.str.soundex, '3') self.assertRaises(ValueError, utils.str.soundex, "'") def testDQRepr(self): L = ['foo', 'foo\'bar', 'foo"bar', '"', '\\', '', '\x00'] for s in L: r = utils.str.dqrepr(s) self.assertEqual(s, eval(r), s) self.failUnless(r[0] == '"' and r[-1] == '"', s) def testPerlReToPythonRe(self): f = utils.str.perlReToPythonRe r = f('m/foo/') self.failUnless(r.search('foo')) r = f('/foo/') self.failUnless(r.search('foo')) r = f('m/\\//') self.failUnless(r.search('/')) r = f('m/cat/i') self.failUnless(r.search('CAT')) self.assertRaises(ValueError, f, 'm/?/') def testP2PReDifferentSeparator(self): r = utils.str.perlReToPythonRe('m!foo!') self.failUnless(r.search('foo')) r = utils.str.perlReToPythonRe('m{cat}') self.failUnless(r.search('cat')) def testPerlReToReplacer(self): PRTR = utils.str.perlReToReplacer f = PRTR('s/foo/bar/') self.assertEqual(f('foobarbaz'), 'barbarbaz') f = PRTR('s/fool/bar/') self.assertEqual(f('foobarbaz'), 'foobarbaz') f = PRTR('s/foo//') self.assertEqual(f('foobarbaz'), 'barbaz') f = PRTR('s/ba//') self.assertEqual(f('foobarbaz'), 'foorbaz') f = PRTR('s/ba//g') self.assertEqual(f('foobarbaz'), 'foorz') f = PRTR('s/ba\\///g') self.assertEqual(f('fooba/rba/z'), 'foorz') f = PRTR('s/ba\\\\//g') self.assertEqual(f('fooba\\rba\\z'), 'foorz') f = PRTR('s/cat/dog/i') self.assertEqual(f('CATFISH'), 'dogFISH') f = PRTR('s/foo/foo\/bar/') self.assertEqual(f('foo'), 'foo/bar') f = PRTR('s/^/foo/') self.assertEqual(f('bar'), 'foobar') def testMultipleReplacer(self): replacer = utils.str.MultipleReplacer({'foo': 'bar', 'a': 'b'}) self.assertEqual(replacer('hi foo hi'), 'hi bar hi') def testMultipleRemover(self): remover = utils.str.MultipleRemover(['foo', 'bar']) self.assertEqual(remover('testfoobarbaz'), 'testbaz') def testPReToReplacerDifferentSeparator(self): f = utils.str.perlReToReplacer('s#foo#bar#') self.assertEqual(f('foobarbaz'), 'barbarbaz') def testPerlReToReplacerBug850931(self): f = utils.str.perlReToReplacer('s/\b(\w+)\b/\1./g') self.assertEqual(f('foo bar baz'), 'foo. bar. baz.') def testCommaAndify(self): f = utils.str.commaAndify L = ['foo'] original = L[:] self.assertEqual(f(L), 'foo') self.assertEqual(f(L, And='or'), 'foo') self.assertEqual(L, original) L.append('bar') original = L[:] self.assertEqual(f(L), 'foo and bar') self.assertEqual(f(L, And='or'), 'foo or bar') self.assertEqual(L, original) L.append('baz') original = L[:] self.assertEqual(f(L), 'foo, bar, and baz') self.assertEqual(f(L, And='or'), 'foo, bar, or baz') self.assertEqual(f(L, comma=';'), 'foo; bar; and baz') self.assertEqual(f(L, comma=';', And='or'), 'foo; bar; or baz') self.assertEqual(L, original) self.failUnless(f(set(L))) def testCommaAndifyRaisesTypeError(self): L = [(2,)] self.assertRaises(TypeError, utils.str.commaAndify, L) L.append((3,)) self.assertRaises(TypeError, utils.str.commaAndify, L) def testUnCommaThe(self): f = utils.str.unCommaThe self.assertEqual(f('foo bar'), 'foo bar') self.assertEqual(f('foo bar, the'), 'the foo bar') self.assertEqual(f('foo bar, The'), 'The foo bar') self.assertEqual(f('foo bar,the'), 'the foo bar') def testNormalizeWhitespace(self): f = utils.str.normalizeWhitespace self.assertEqual(f('foo bar'), 'foo bar') self.assertEqual(f('foo\nbar'), 'foo bar') self.assertEqual(f('foo\tbar'), 'foo bar') self.assertEqual(f('foo\rbar'), 'foo bar') def testNItems(self): nItems = utils.str.nItems self.assertEqual(nItems(0, 'tool'), '0 tools') self.assertEqual(nItems(1, 'tool', 'crazy'), '1 crazy tool') self.assertEqual(nItems(1, 'tool'), '1 tool') self.assertEqual(nItems(2, 'tool', 'crazy'), '2 crazy tools') self.assertEqual(nItems(2, 'tool'), '2 tools') def testOrdinal(self): ordinal = utils.str.ordinal self.assertEqual(ordinal(3), '3rd') self.assertEqual(ordinal('3'), '3rd') self.assertRaises(ValueError, ordinal, 'a') def testEllipsisify(self): f = utils.str.ellipsisify self.assertEqual(f('x'*30, 30), 'x'*30) self.failUnless(len(f('x'*35, 30)) <= 30) self.failUnless(f(' '.join(['xxxx']*10), 30)[:-3].endswith('xxxx')) class IterTest(SupyTestCase): def testLimited(self): L = range(10) self.assertEqual([], list(utils.iter.limited(L, 0))) self.assertEqual([0], list(utils.iter.limited(L, 1))) self.assertEqual([0, 1], list(utils.iter.limited(L, 2))) self.assertEqual(list(range(10)), list(utils.iter.limited(L, 10))) self.assertRaises(ValueError, list, utils.iter.limited(L, 11)) def testRandomChoice(self): choice = utils.iter.choice self.assertRaises(IndexError, choice, {}) self.assertRaises(IndexError, choice, []) self.assertRaises(IndexError, choice, ()) L = [1, 2] seenList = set() seenIterable = set() for n in xrange(300): # 2**266 > 10**80, the number of atoms in the known universe. # (ignoring dark matter, but that likely doesn't exist in atoms # anyway, so it shouldn't have a significant impact on that #) seenList.add(choice(L)) seenIterable.add(choice(iter(L))) self.assertEqual(len(L), len(seenList), 'choice isn\'t being random with lists') self.assertEqual(len(L), len(seenIterable), 'choice isn\'t being random with iterables') ## def testGroup(self): ## group = utils.iter.group ## s = '1. d4 d5 2. Nf3 Nc6 3. e3 Nf6 4. Nc3 e6 5. Bd3 a6' ## self.assertEqual(group(s.split(), 3)[:3], ## [['1.', 'd4', 'd5'], ## ['2.', 'Nf3', 'Nc6'], ## ['3.', 'e3', 'Nf6']]) def testAny(self): any = utils.iter.any self.failUnless(any(lambda i: i == 0, range(10))) self.failIf(any(None, range(1))) self.failUnless(any(None, range(2))) self.failIf(any(None, [])) def testAll(self): all = utils.iter.all self.failIf(all(lambda i: i == 0, range(10))) self.failIf(all(lambda i: i % 2, range(2))) self.failIf(all(lambda i: i % 2 == 0, [1, 3, 5])) self.failUnless(all(lambda i: i % 2 == 0, [2, 4, 6])) self.failUnless(all(None, ())) def testPartition(self): partition = utils.iter.partition L = range(10) def even(i): return not(i % 2) (yes, no) = partition(even, L) self.assertEqual(yes, [0, 2, 4, 6, 8]) self.assertEqual(no, [1, 3, 5, 7, 9]) def testIlen(self): ilen = utils.iter.ilen self.assertEqual(ilen(iter(range(10))), 10) def testSplit(self): itersplit = utils.iter.split L = [1, 2, 3] * 3 s = 'foo bar baz' self.assertEqual(list(itersplit(lambda x: x == 3, L)), [[1, 2], [1, 2], [1, 2]]) self.assertEqual(list(itersplit(lambda x: x == 3, L, yieldEmpty=True)), [[1, 2], [1, 2], [1, 2], []]) self.assertEqual(list(itersplit(lambda x: x, [])), []) self.assertEqual(list(itersplit(lambda c: c.isspace(), s)), list(map(list, s.split()))) self.assertEqual(list(itersplit('for'.__eq__, ['foo', 'for', 'bar'])), [['foo'], ['bar']]) self.assertEqual(list(itersplit('for'.__eq__, ['foo','for','bar','for', 'baz'], 1)), [['foo'], ['bar', 'for', 'baz']]) def testFlatten(self): def lflatten(seq): return list(utils.iter.flatten(seq)) self.assertEqual(lflatten([]), []) self.assertEqual(lflatten([1]), [1]) self.assertEqual(lflatten(range(10)), list(range(10))) twoRanges = list(range(10))*2 twoRanges.sort() self.assertEqual(lflatten(list(zip(range(10), range(10)))), twoRanges) self.assertEqual(lflatten([1, [2, 3], 4]), [1, 2, 3, 4]) self.assertEqual(lflatten([[[[[[[[[[]]]]]]]]]]), []) self.assertEqual(lflatten([1, [2, [3, 4], 5], 6]), [1, 2, 3, 4, 5, 6]) self.assertRaises(TypeError, lflatten, 1) class FileTest(SupyTestCase): def testLines(self): L = ['foo', 'bar', '#baz', ' ', 'biff'] self.assertEqual(list(utils.file.nonEmptyLines(L)), ['foo', 'bar', '#baz', 'biff']) self.assertEqual(list(utils.file.nonCommentLines(L)), ['foo', 'bar', ' ', 'biff']) self.assertEqual(list(utils.file.nonCommentNonEmptyLines(L)), ['foo', 'bar', 'biff']) def testMktemp(self): # This is mostly to test that it actually halts. self.failUnless(utils.file.mktemp()) self.failUnless(utils.file.mktemp()) self.failUnless(utils.file.mktemp()) class NetTest(SupyTestCase): def testEmailRe(self): emailRe = utils.net.emailRe self.failUnless(emailRe.match('jemfinch@supybot.com')) def testIsIP(self): isIP = utils.net.isIP self.failIf(isIP('a.b.c')) self.failIf(isIP('256.0.0.0')) self.failIf(isIP('127.0.0.1 127.0.0.1')) self.failUnless(isIP('0.0.0.0')) self.failUnless(isIP('100.100.100.100')) self.failUnless(isIP('255.255.255.255')) def testIsIPV6(self): f = utils.net.isIPV6 self.failIf(f('2001:: 2001::')) self.failUnless(f('2001::')) self.failUnless(f('2001:888:0:1::666')) class WebTest(SupyTestCase): def testGetDomain(self): url = 'http://slashdot.org/foo/bar.exe' self.assertEqual(utils.web.getDomain(url), 'slashdot.org') if network: def testGetUrlWithSize(self): url = 'http://slashdot.org/' self.failUnless(len(utils.web.getUrl(url, 1024)) == 1024) class FormatTestCase(SupyTestCase): def testNormal(self): format = utils.str.format self.assertEqual(format('I have %n of fruit: %L.', (3, 'kind'), ['apples', 'oranges', 'watermelon']), 'I have 3 kinds of fruit: ' 'apples, oranges, and watermelon.') def testPercentL(self): self.assertIn(format('%L', set(['apples', 'oranges', 'watermelon'])), [ 'apples, oranges, and watermelon', 'oranges, apples, and watermelon', 'apples, watermelon, and oranges', 'oranges, watermelon, and apples', 'watermelon, apples, and oranges', 'watermelon, oranges, and apples']) self.assertEqual(format('%L', (['apples', 'oranges', 'watermelon'], 'or')), 'apples, oranges, or watermelon') class RingBufferTestCase(SupyTestCase): def testInit(self): self.assertRaises(ValueError, RingBuffer, -1) self.assertRaises(ValueError, RingBuffer, 0) self.assertEqual(list(range(10)), list(RingBuffer(10, range(10)))) def testLen(self): b = RingBuffer(3) self.assertEqual(0, len(b)) b.append(1) self.assertEqual(1, len(b)) b.append(2) self.assertEqual(2, len(b)) b.append(3) self.assertEqual(3, len(b)) b.append(4) self.assertEqual(3, len(b)) b.append(5) self.assertEqual(3, len(b)) def testNonzero(self): b = RingBuffer(3) self.failIf(b) b.append(1) self.failUnless(b) def testAppend(self): b = RingBuffer(3) self.assertEqual([], list(b)) b.append(1) self.assertEqual([1], list(b)) b.append(2) self.assertEqual([1, 2], list(b)) b.append(3) self.assertEqual([1, 2, 3], list(b)) b.append(4) self.assertEqual([2, 3, 4], list(b)) b.append(5) self.assertEqual([3, 4, 5], list(b)) b.append(6) self.assertEqual([4, 5, 6], list(b)) def testContains(self): b = RingBuffer(3, range(3)) self.failUnless(0 in b) self.failUnless(1 in b) self.failUnless(2 in b) self.failIf(3 in b) def testGetitem(self): L = range(10) b = RingBuffer(len(L), L) for i in range(len(b)): self.assertEqual(L[i], b[i]) for i in range(len(b)): self.assertEqual(L[-i], b[-i]) for i in range(len(b)): b.append(i) for i in range(len(b)): self.assertEqual(L[i], b[i]) for i in range(len(b)): self.assertEqual(list(b), list(b[:i]) + list(b[i:])) def testSliceGetitem(self): L = list(range(10)) b = RingBuffer(len(L), L) for i in range(len(b)): self.assertEqual(L[:i], b[:i]) self.assertEqual(L[i:], b[i:]) self.assertEqual(L[i:len(b)-i], b[i:len(b)-i]) self.assertEqual(L[:-i], b[:-i]) self.assertEqual(L[-i:], b[-i:]) self.assertEqual(L[i:-i], b[i:-i]) for i in range(len(b)): b.append(i) for i in range(len(b)): self.assertEqual(L[:i], b[:i]) self.assertEqual(L[i:], b[i:]) self.assertEqual(L[i:len(b)-i], b[i:len(b)-i]) self.assertEqual(L[:-i], b[:-i]) self.assertEqual(L[-i:], b[-i:]) self.assertEqual(L[i:-i], b[i:-i]) def testSetitem(self): L = range(10) b = RingBuffer(len(L), [0]*len(L)) for i in range(len(b)): b[i] = i for i in range(len(b)): self.assertEqual(b[i], i) for i in range(len(b)): b.append(0) for i in range(len(b)): b[i] = i for i in range(len(b)): self.assertEqual(b[i], i) def testSliceSetitem(self): L = list(range(10)) b = RingBuffer(len(L), [0]*len(L)) self.assertRaises(ValueError, b.__setitem__, slice(0, 10), []) b[2:4] = L[2:4] self.assertEquals(b[2:4], L[2:4]) for _ in range(len(b)): b.append(0) b[2:4] = L[2:4] self.assertEquals(b[2:4], L[2:4]) def testExtend(self): b = RingBuffer(3, range(3)) self.assertEqual(list(b), list(range(3))) b.extend(range(6)) self.assertEqual(list(b), list(range(6)[3:])) def testRepr(self): b = RingBuffer(3) self.assertEqual(repr(b), 'RingBuffer(3, [])') b.append(1) self.assertEqual(repr(b), 'RingBuffer(3, [1])') b.append(2) self.assertEqual(repr(b), 'RingBuffer(3, [1, 2])') b.append(3) self.assertEqual(repr(b), 'RingBuffer(3, [1, 2, 3])') b.append(4) self.assertEqual(repr(b), 'RingBuffer(3, [2, 3, 4])') b.append(5) self.assertEqual(repr(b), 'RingBuffer(3, [3, 4, 5])') b.append(6) self.assertEqual(repr(b), 'RingBuffer(3, [4, 5, 6])') def testPickleCopy(self): b = RingBuffer(10, range(10)) self.assertEqual(pickle.loads(pickle.dumps(b)), b) def testEq(self): b = RingBuffer(3, range(3)) self.failIf(b == list(range(3))) b1 = RingBuffer(3) self.failIf(b == b1) b1.append(0) self.failIf(b == b1) b1.append(1) self.failIf(b == b1) b1.append(2) self.failUnless(b == b1) b = RingBuffer(100, range(10)) b1 = RingBuffer(10, range(10)) self.failIf(b == b1) def testIter(self): b = RingBuffer(3, range(3)) L = [] for elt in b: L.append(elt) self.assertEqual(L, list(range(3))) for elt in range(3): b.append(elt) del L[:] for elt in b: L.append(elt) self.assertEqual(L, list(range(3))) class QueueTest(SupyTestCase): def testReset(self): q = queue() q.enqueue(1) self.assertEqual(len(q), 1) q.reset() self.assertEqual(len(q), 0) def testGetitem(self): q = queue() n = 10 self.assertRaises(IndexError, q.__getitem__, 0) for i in xrange(n): q.enqueue(i) for i in xrange(n): self.assertEqual(q[i], i) for i in xrange(n, 0, -1): self.assertEqual(q[-i], n-i) for i in xrange(len(q)): self.assertEqual(list(q), list(q[:i]) + list(q[i:])) self.assertRaises(IndexError, q.__getitem__, -(n+1)) self.assertRaises(IndexError, q.__getitem__, n) self.assertEqual(q[3:7], queue([3, 4, 5, 6])) def testSetitem(self): q1 = queue() self.assertRaises(IndexError, q1.__setitem__, 0, 0) for i in xrange(10): q1.enqueue(i) q2 = eval(repr(q1)) for (i, elt) in enumerate(q2): q2[i] = elt*2 self.assertEqual([x*2 for x in q1], list(q2)) def testNonzero(self): q = queue() self.failIf(q, 'queue not zero after initialization') q.enqueue(1) self.failUnless(q, 'queue zero after adding element') q.dequeue() self.failIf(q, 'queue not zero after dequeue of only element') def testLen(self): q = queue() self.assertEqual(0, len(q), 'queue len not 0 after initialization') q.enqueue(1) self.assertEqual(1, len(q), 'queue len not 1 after enqueue') q.enqueue(2) self.assertEqual(2, len(q), 'queue len not 2 after enqueue') q.dequeue() self.assertEqual(1, len(q), 'queue len not 1 after dequeue') q.dequeue() self.assertEqual(0, len(q), 'queue len not 0 after dequeue') for i in range(10): L = range(i) q = queue(L) self.assertEqual(len(q), i) def testEq(self): q1 = queue() q2 = queue() self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') self.failUnless(q1 == q2, 'initialized queues not equal') q1.enqueue(1) self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') q2.enqueue(1) self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') self.failUnless(q1 == q2, 'queues not equal after identical enqueue') q1.dequeue() self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') self.failIf(q1 == q2, 'queues equal after one dequeue') q2.dequeue() self.failUnless(q1 == q2, 'queues not equal after both are dequeued') self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') def testInit(self): self.assertEqual(len(queue()), 0, 'queue len not 0 after init') q = queue() q.enqueue(1) q.enqueue(2) q.enqueue(3) self.assertEqual(queue((1, 2, 3)),q, 'init not equivalent to enqueues') q = queue((1, 2, 3)) self.assertEqual(q.dequeue(), 1, 'values not returned in proper order') self.assertEqual(q.dequeue(), 2, 'values not returned in proper order') self.assertEqual(q.dequeue(), 3, 'values not returned in proper order') def testRepr(self): q = queue() q.enqueue(1) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue('foo') self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue(None) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue(1.0) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue([]) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue(()) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue([1]) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue((1,)) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') def testEnqueueDequeue(self): q = queue() self.assertRaises(IndexError, q.dequeue) q.enqueue(1) self.assertEqual(q.dequeue(), 1, 'first dequeue didn\'t return same as first enqueue') q.enqueue(1) q.enqueue(2) q.enqueue(3) self.assertEqual(q.dequeue(), 1) self.assertEqual(q.dequeue(), 2) self.assertEqual(q.dequeue(), 3) def testPeek(self): q = queue() self.assertRaises(IndexError, q.peek) q.enqueue(1) self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue') q.enqueue(2) self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue') q.dequeue() self.assertEqual(q.peek(), 2, 'peek didn\'t return second enqueue') q.dequeue() self.assertRaises(IndexError, q.peek) def testContains(self): q = queue() self.failIf(1 in q, 'empty queue cannot have elements') q.enqueue(1) self.failUnless(1 in q, 'recent enqueued element not in q') q.enqueue(2) self.failUnless(1 in q, 'original enqueued element not in q') self.failUnless(2 in q, 'second enqueued element not in q') q.dequeue() self.failIf(1 in q, 'dequeued element in q') self.failUnless(2 in q, 'not dequeued element not in q') q.dequeue() self.failIf(2 in q, 'dequeued element in q') def testIter(self): q1 = queue((1, 2, 3)) q2 = queue() for i in q1: q2.enqueue(i) self.assertEqual(q1, q2, 'iterate didn\'t return all elements') for _ in queue(): self.fail('no elements should be in empty queue') def testPickleCopy(self): q = queue(range(10)) self.assertEqual(q, pickle.loads(pickle.dumps(q))) queue = smallqueue class SmallQueueTest(SupyTestCase): def testReset(self): q = queue() q.enqueue(1) self.assertEqual(len(q), 1) q.reset() self.assertEqual(len(q), 0) def testGetitem(self): q = queue() n = 10 self.assertRaises(IndexError, q.__getitem__, 0) for i in xrange(n): q.enqueue(i) for i in xrange(n): self.assertEqual(q[i], i) for i in xrange(n, 0, -1): self.assertEqual(q[-i], n-i) for i in xrange(len(q)): self.assertEqual(list(q), list(q[:i]) + list(q[i:])) self.assertRaises(IndexError, q.__getitem__, -(n+1)) self.assertRaises(IndexError, q.__getitem__, n) self.assertEqual(q[3:7], queue([3, 4, 5, 6])) def testSetitem(self): q1 = queue() self.assertRaises(IndexError, q1.__setitem__, 0, 0) for i in xrange(10): q1.enqueue(i) q2 = eval(repr(q1)) for (i, elt) in enumerate(q2): q2[i] = elt*2 self.assertEqual([x*2 for x in q1], list(q2)) def testNonzero(self): q = queue() self.failIf(q, 'queue not zero after initialization') q.enqueue(1) self.failUnless(q, 'queue zero after adding element') q.dequeue() self.failIf(q, 'queue not zero after dequeue of only element') def testLen(self): q = queue() self.assertEqual(0, len(q), 'queue len not 0 after initialization') q.enqueue(1) self.assertEqual(1, len(q), 'queue len not 1 after enqueue') q.enqueue(2) self.assertEqual(2, len(q), 'queue len not 2 after enqueue') q.dequeue() self.assertEqual(1, len(q), 'queue len not 1 after dequeue') q.dequeue() self.assertEqual(0, len(q), 'queue len not 0 after dequeue') for i in range(10): L = range(i) q = queue(L) self.assertEqual(len(q), i) def testEq(self): q1 = queue() q2 = queue() self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') self.failUnless(q1 == q2, 'initialized queues not equal') q1.enqueue(1) self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') q2.enqueue(1) self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') self.failUnless(q1 == q2, 'queues not equal after identical enqueue') q1.dequeue() self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') self.failIf(q1 == q2, 'queues equal after one dequeue') q2.dequeue() self.failUnless(q1 == q2, 'queues not equal after both are dequeued') self.failUnless(q1 == q1, 'queue not equal to itself') self.failUnless(q2 == q2, 'queue not equal to itself') def testInit(self): self.assertEqual(len(queue()), 0, 'queue len not 0 after init') q = queue() q.enqueue(1) q.enqueue(2) q.enqueue(3) self.assertEqual(queue((1, 2, 3)),q, 'init not equivalent to enqueues') q = queue((1, 2, 3)) self.assertEqual(q.dequeue(), 1, 'values not returned in proper order') self.assertEqual(q.dequeue(), 2, 'values not returned in proper order') self.assertEqual(q.dequeue(), 3, 'values not returned in proper order') def testRepr(self): q = queue() q.enqueue(1) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue('foo') self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue(None) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue(1.0) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue([]) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue(()) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue([1]) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') q.enqueue((1,)) self.assertEqual(q, eval(repr(q)), 'repr doesn\'t eval to same queue') def testEnqueueDequeue(self): q = queue() self.assertRaises(IndexError, q.dequeue) q.enqueue(1) self.assertEqual(q.dequeue(), 1, 'first dequeue didn\'t return same as first enqueue') q.enqueue(1) q.enqueue(2) q.enqueue(3) self.assertEqual(q.dequeue(), 1) self.assertEqual(q.dequeue(), 2) self.assertEqual(q.dequeue(), 3) def testPeek(self): q = queue() self.assertRaises(IndexError, q.peek) q.enqueue(1) self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue') q.enqueue(2) self.assertEqual(q.peek(), 1, 'peek didn\'t return first enqueue') q.dequeue() self.assertEqual(q.peek(), 2, 'peek didn\'t return second enqueue') q.dequeue() self.assertRaises(IndexError, q.peek) def testContains(self): q = queue() self.failIf(1 in q, 'empty queue cannot have elements') q.enqueue(1) self.failUnless(1 in q, 'recent enqueued element not in q') q.enqueue(2) self.failUnless(1 in q, 'original enqueued element not in q') self.failUnless(2 in q, 'second enqueued element not in q') q.dequeue() self.failIf(1 in q, 'dequeued element in q') self.failUnless(2 in q, 'not dequeued element not in q') q.dequeue() self.failIf(2 in q, 'dequeued element in q') def testIter(self): q1 = queue((1, 2, 3)) q2 = queue() for i in q1: q2.enqueue(i) self.assertEqual(q1, q2, 'iterate didn\'t return all elements') for _ in queue(): self.fail('no elements should be in empty queue') def testPickleCopy(self): q = queue(range(10)) self.assertEqual(q, pickle.loads(pickle.dumps(q))) class MaxLengthQueueTestCase(SupyTestCase): def testInit(self): q = MaxLengthQueue(3, (1, 2, 3)) self.assertEqual(list(q), [1, 2, 3]) self.assertRaises(TypeError, MaxLengthQueue, 3, 1, 2, 3) def testMaxLength(self): q = MaxLengthQueue(3) q.enqueue(1) self.assertEqual(len(q), 1) q.enqueue(2) self.assertEqual(len(q), 2) q.enqueue(3) self.assertEqual(len(q), 3) q.enqueue(4) self.assertEqual(len(q), 3) self.assertEqual(q.peek(), 2) q.enqueue(5) self.assertEqual(len(q), 3) self.assertEqual(q[0], 3) class TwoWayDictionaryTestCase(SupyTestCase): def testInit(self): d = TwoWayDictionary(foo='bar') self.failUnless('foo' in d) self.failUnless('bar' in d) d = TwoWayDictionary({1: 2}) self.failUnless(1 in d) self.failUnless(2 in d) def testSetitem(self): d = TwoWayDictionary() d['foo'] = 'bar' self.failUnless('foo' in d) self.failUnless('bar' in d) def testDelitem(self): d = TwoWayDictionary(foo='bar') del d['foo'] self.failIf('foo' in d) self.failIf('bar' in d) d = TwoWayDictionary(foo='bar') del d['bar'] self.failIf('bar' in d) self.failIf('foo' in d) class TestTimeoutQueue(SupyTestCase): def test(self): q = TimeoutQueue(1) q.enqueue(1) self.assertEqual(len(q), 1) q.enqueue(2) self.assertEqual(len(q), 2) q.enqueue(3) self.assertEqual(len(q), 3) self.assertEqual(sum(q), 6) time.sleep(1.1) self.assertEqual(len(q), 0) self.assertEqual(sum(q), 0) def testCallableTimeout(self): q = TimeoutQueue(lambda : 1) q.enqueue(1) self.assertEqual(len(q), 1) q.enqueue(2) self.assertEqual(len(q), 2) q.enqueue(3) self.assertEqual(len(q), 3) self.assertEqual(sum(q), 6) time.sleep(1.1) self.assertEqual(len(q), 0) self.assertEqual(sum(q), 0) def testContains(self): q = TimeoutQueue(1) q.enqueue(1) self.failUnless(1 in q) self.failUnless(1 in q) # For some reason, the second one might fail. self.failIf(2 in q) time.sleep(1.1) self.failIf(1 in q) def testReset(self): q = TimeoutQueue(10) q.enqueue(1) self.failUnless(1 in q) q.reset() self.failIf(1 in q) class TestCacheDict(SupyTestCase): def testMaxNeverExceeded(self): max = 10 d = CacheDict(10) for i in xrange(max**2): d[i] = i self.failUnless(len(d) <= max) self.failUnless(i in d) self.failUnless(d[i] == i) class TestTruncatableSet(SupyTestCase): def testBasics(self): s = TruncatableSet(['foo', 'bar', 'baz', 'qux']) self.assertEqual(s, set(['foo', 'bar', 'baz', 'qux'])) self.failUnless('foo' in s) self.failUnless('bar' in s) self.failIf('quux' in s) s.discard('baz') self.failUnless('foo' in s) self.failIf('baz' in s) s.add('quux') self.failUnless('quux' in s) def testTruncate(self): s = TruncatableSet(['foo', 'bar']) s.add('baz') s.add('qux') s.truncate(3) self.assertEqual(s, set(['bar', 'baz', 'qux'])) def testTruncateUnion(self): s = TruncatableSet(['bar', 'foo']) s |= set(['baz', 'qux']) s.truncate(3) self.assertEqual(s, set(['foo', 'baz', 'qux'])) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_standardSubstitute.py0000644000175000017500000000740713233426066021613 0ustar valval00000000000000### # 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 * import supybot.irclib as irclib from supybot.utils.iter import all import supybot.ircutils as ircutils class holder: users = set(map(str, range(1000))) class FunctionsTestCase(SupyTestCase): class irc: class state: channels = {'#foo': holder()} nick = 'foobar' network = 'testnet' @retry() def testStandardSubstitute(self): f = ircutils.standardSubstitute msg = ircmsgs.privmsg('#foo', 'filler', prefix='biff!quux@xyzzy') s = f(self.irc, msg, '$rand') try: int(s) except ValueError: self.fail('$rand wasn\'t an int.') s = f(self.irc, msg, '$randomInt') try: int(s) except ValueError: self.fail('$randomint wasn\'t an int.') self.assertEqual(f(self.irc, msg, '$botnick'), self.irc.nick) self.assertEqual(f(self.irc, msg, '$who'), msg.nick) self.assertEqual(f(self.irc, msg, '$WHO'), msg.nick, 'stand. sub. not case-insensitive.') self.assertEqual(f(self.irc, msg, '$nick'), msg.nick) self.assertNotEqual(f(self.irc, msg, '$randomdate'), '$randomdate') q = f(self.irc,msg,'$randomdate\t$randomdate') dl = q.split('\t') if dl[0] == dl[1]: self.fail ('Two $randomdates in the same string were the same') q = f(self.irc, msg, '$randomint\t$randomint') dl = q.split('\t') if dl[0] == dl[1]: self.fail ('Two $randomints in the same string were the same') self.assertNotEqual(f(self.irc, msg, '$today'), '$today') self.assertNotEqual(f(self.irc, msg, '$now'), '$now') n = f(self.irc, msg, '$randnick') self.failUnless(n in self.irc.state.channels['#foo'].users) n = f(self.irc, msg, '$randomnick') self.failUnless(n in self.irc.state.channels['#foo'].users) n = f(self.irc, msg, '$randomnick '*100) L = n.split() self.failIf(all(L[0].__eq__, L), 'all $randomnicks were the same') c = f(self.irc, msg, '$channel') self.assertEqual(c, msg.args[0]) net = f(self.irc, msg, '$network') self.assertEqual(net, self.irc.network) limnoria-2018.01.25/test/test_schedule.py0000644000175000017500000000774713233426066017522 0ustar valval00000000000000### # 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 * import time import supybot.schedule as schedule class TestSchedule(SupyTestCase): def testSchedule(self): sched = schedule.Schedule() i = [0] def add10(): i[0] = i[0] + 10 def add1(): i[0] = i[0] + 1 sched.addEvent(add10, time.time() + 3) sched.addEvent(add1, time.time() + 1) time.sleep(1.2) sched.run() self.assertEqual(i[0], 1) time.sleep(1.9) sched.run() self.assertEqual(i[0], 11) sched.addEvent(add10, time.time() + 3, 'test') sched.run() self.assertEqual(i[0], 11) sched.removeEvent('test') self.assertEqual(i[0], 11) time.sleep(3) self.assertEqual(i[0], 11) def testReschedule(self): sched = schedule.Schedule() i = [0] def inc(): i[0] += 1 n = sched.addEvent(inc, time.time() + 1) sched.rescheduleEvent(n, time.time() + 3) time.sleep(1.2) sched.run() self.assertEqual(i[0], 0) time.sleep(2) sched.run() self.assertEqual(i[0], 1) def testPeriodic(self): sched = schedule.Schedule() i = [0] def inc(): i[0] += 1 n = sched.addPeriodicEvent(inc, 1, name='test_periodic') time.sleep(0.6) sched.run() # 0.6 self.assertEqual(i[0], 1) time.sleep(0.6) sched.run() # 1.2 self.assertEqual(i[0], 2) time.sleep(0.6) sched.run() # 1.8 self.assertEqual(i[0], 2) time.sleep(0.6) sched.run() # 2.4 self.assertEqual(i[0], 3) sched.removePeriodicEvent(n) time.sleep(1) sched.run() # 3.4 self.assertEqual(i[0], 3) def testCountedPeriodic(self): sched = schedule.Schedule() i = [0] def inc(): i[0] += 1 n = sched.addPeriodicEvent(inc, 1, name='test_periodic', count=3) time.sleep(0.6) sched.run() # 0.6 self.assertEqual(i[0], 1) time.sleep(0.6) sched.run() # 1.2 self.assertEqual(i[0], 2) time.sleep(0.6) sched.run() # 1.8 self.assertEqual(i[0], 2) time.sleep(0.6) sched.run() # 2.4 self.assertEqual(i[0], 3) time.sleep(1) sched.run() # 3.4 self.assertEqual(i[0], 3) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_registry.py0000644000175000017500000001754213233426066017570 0ustar valval00000000000000### # Copyright (c) 2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from supybot.test import * import re import supybot.conf as conf import supybot.registry as registry join = registry.join split = registry.split escape = registry.escape unescape = registry.unescape class FunctionsTestCase(SupyTestCase): def testEscape(self): self.assertEqual('foo', escape('foo')) self.assertEqual('foo\\.bar', escape('foo.bar')) self.assertEqual('foo\\:bar', escape('foo:bar')) def testUnescape(self): self.assertEqual('foo', unescape('foo')) self.assertEqual('foo.bar', unescape('foo\\.bar')) self.assertEqual('foo:bar', unescape('foo\\:bar')) def testEscapeAndUnescapeAreInverses(self): for s in ['foo', 'foo.bar']: self.assertEqual(s, unescape(escape(s))) self.assertEqual(escape(s), escape(unescape(escape(s)))) def testSplit(self): self.assertEqual(['foo'], split('foo')) self.assertEqual(['foo', 'bar'], split('foo.bar')) self.assertEqual(['foo.bar'], split('foo\\.bar')) def testJoin(self): self.assertEqual('foo', join(['foo'])) self.assertEqual('foo.bar', join(['foo', 'bar'])) self.assertEqual('foo\\.bar', join(['foo.bar'])) def testJoinAndSplitAreInverses(self): for s in ['foo', 'foo.bar', 'foo\\.bar']: self.assertEqual(s, join(split(s))) self.assertEqual(split(s), split(join(split(s)))) class ValuesTestCase(SupyTestCase): def testBoolean(self): v = registry.Boolean(True, """Help""") self.failUnless(v()) v.setValue(False) self.failIf(v()) v.set('True') self.failUnless(v()) v.set('False') self.failIf(v()) v.set('On') self.failUnless(v()) v.set('Off') self.failIf(v()) v.set('enable') self.failUnless(v()) v.set('disable') self.failIf(v()) v.set('toggle') self.failUnless(v()) v.set('toggle') self.failIf(v()) def testInteger(self): v = registry.Integer(1, 'help') self.assertEqual(v(), 1) v.setValue(10) self.assertEqual(v(), 10) v.set('100') self.assertEqual(v(), 100) v.set('-1000') self.assertEqual(v(), -1000) def testPositiveInteger(self): v = registry.PositiveInteger(1, 'help') self.assertEqual(v(), 1) self.assertRaises(registry.InvalidRegistryValue, v.setValue, -1) self.assertRaises(registry.InvalidRegistryValue, v.set, '-1') def testFloat(self): v = registry.Float(1.0, 'help') self.assertEqual(v(), 1.0) v.setValue(10) self.assertEqual(v(), 10.0) v.set('0') self.assertEqual(v(), 0.0) def testString(self): v = registry.String('foo', 'help') self.assertEqual(v(), 'foo') v.setValue('bar') self.assertEqual(v(), 'bar') v.set('"biff"') self.assertEqual(v(), 'biff') v.set("'buff'") self.assertEqual(v(), 'buff') v.set('"xyzzy') self.assertEqual(v(), '"xyzzy') def testJson(self): data = {'foo': ['bar', 'baz', 5], 'qux': None} v = registry.Json('foo', 'help') self.assertEqual(v(), 'foo') v.setValue(data) self.assertEqual(v(), data) self.assertIsNot(v(), data) with v.editable() as dict_: dict_['supy'] = 'bot' del dict_['qux'] self.assertNotIn('supy', v()) self.assertIn('qux', v()) self.assertIn('supy', v()) self.assertEqual(v()['supy'], 'bot') self.assertIsNot(v()['supy'], 'bot') self.assertNotIn('qux', v()) def testNormalizedString(self): v = registry.NormalizedString("""foo bar baz biff """, 'help') self.assertEqual(v(), 'foo bar baz biff') v.setValue('foo bar baz') self.assertEqual(v(), 'foo bar baz') v.set('"foo bar baz"') self.assertEqual(v(), 'foo bar baz') def testStringSurroundedBySpaces(self): v = registry.StringSurroundedBySpaces('foo', 'help') self.assertEqual(v(), ' foo ') v.setValue('||') self.assertEqual(v(), ' || ') v.set('&&') self.assertEqual(v(), ' && ') def testCommaSeparatedListOfStrings(self): v = registry.CommaSeparatedListOfStrings(['foo', 'bar'], 'help') self.assertEqual(v(), ['foo', 'bar']) v.setValue(['foo', 'bar', 'baz']) self.assertEqual(v(), ['foo', 'bar', 'baz']) v.set('foo,bar') self.assertEqual(v(), ['foo', 'bar']) def testRegexp(self): v = registry.Regexp(None, 'help') self.assertEqual(v(), None) v.set('m/foo/') self.failUnless(v().match('foo')) v.set('') self.assertEqual(v(), None) self.assertRaises(registry.InvalidRegistryValue, v.setValue, re.compile(r'foo')) def testBackslashes(self): conf.supybot.reply.whenAddressedBy.chars.set('\\') filename = conf.supybot.directories.conf.dirize('backslashes.conf') registry.close(conf.supybot, filename) registry.open_registry(filename) self.assertEqual(conf.supybot.reply.whenAddressedBy.chars(), '\\') def testWith(self): v = registry.String('foo', 'help') self.assertEqual(v(), 'foo') with v.context('bar'): self.assertEqual(v(), 'bar') self.assertEqual(v(), 'foo') class SecurityTestCase(SupyTestCase): def testPrivate(self): v = registry.String('foo', 'help') self.assertFalse(v._private) v = registry.String('foo', 'help', private=True) self.assertTrue(v._private) g = registry.Group('foo') v = registry.String('foo', 'help') g.register('val', v) self.assertFalse(g._private) self.assertFalse(g.val._private) g = registry.Group('foo', private=True) v = registry.String('foo', 'help') g.register('val', v) self.assertTrue(g._private) self.assertTrue(g.val._private) g = registry.Group('foo') v = registry.String('foo', 'help', private=True) g.register('val', v) self.assertFalse(g._private) self.assertTrue(g.val._private) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_plugins.py0000644000175000017500000000326513233426066017376 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # 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.irclib as irclib import supybot.plugins as plugins limnoria-2018.01.25/test/test_plugin.py0000644000175000017500000000404313233426066017206 0ustar valval00000000000000### # 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. ### from supybot.test import * import supybot.plugin as plugin class FunctionsTestCase(SupyTestCase): def testLoadPluginModule(self): self.assertRaises(ImportError, plugin.loadPluginModule, 'asldj') self.failUnless(plugin.loadPluginModule('Owner')) # I haven't yet figured out a way to get case-insensitivity back for # "directoried" plugins. #self.failUnless(plugin.loadPluginModule('owner')) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_ircutils.py0000644000175000017500000004332213233426066017551 0ustar valval00000000000000### # 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 * import copy import random import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils # The test framework used to provide these, but not it doesn't. We'll add # messages to as we find bugs (if indeed we find bugs). msgs = [] rawmsgs = [] class FunctionsTestCase(SupyTestCase): hostmask = 'foo!bar@baz' def testHostmaskPatternEqual(self): for msg in msgs: if msg.prefix and ircutils.isUserHostmask(msg.prefix): s = msg.prefix self.failUnless(ircutils.hostmaskPatternEqual(s, s), '%r did not match itself.' % s) banmask = ircutils.banmask(s) self.failUnless(ircutils.hostmaskPatternEqual(banmask, s), '%r did not match %r' % (s, banmask)) s = 'supybot!~supybot@dhcp065-024-075-056.columbus.rr.com' self.failUnless(ircutils.hostmaskPatternEqual(s, s)) s = 'jamessan|work!~jamessan@209-6-166-196.c3-0.' \ 'abr-ubr1.sbo-abr.ma.cable.rcn.com' self.failUnless(ircutils.hostmaskPatternEqual(s, s)) def testIsUserHostmask(self): self.failUnless(ircutils.isUserHostmask(self.hostmask)) self.failUnless(ircutils.isUserHostmask('a!b@c')) self.failIf(ircutils.isUserHostmask('!bar@baz')) self.failIf(ircutils.isUserHostmask('!@baz')) self.failIf(ircutils.isUserHostmask('!bar@')) self.failIf(ircutils.isUserHostmask('!@')) self.failIf(ircutils.isUserHostmask('foo!@baz')) self.failIf(ircutils.isUserHostmask('foo!bar@')) self.failIf(ircutils.isUserHostmask('')) self.failIf(ircutils.isUserHostmask('!')) self.failIf(ircutils.isUserHostmask('@')) self.failIf(ircutils.isUserHostmask('!bar@baz')) def testIsChannel(self): self.failUnless(ircutils.isChannel('#')) self.failUnless(ircutils.isChannel('&')) self.failUnless(ircutils.isChannel('+')) self.failUnless(ircutils.isChannel('!')) self.failUnless(ircutils.isChannel('#foo')) self.failUnless(ircutils.isChannel('&foo')) self.failUnless(ircutils.isChannel('+foo')) self.failUnless(ircutils.isChannel('!foo')) self.failIf(ircutils.isChannel('#foo bar')) self.failIf(ircutils.isChannel('#foo,bar')) self.failIf(ircutils.isChannel('#foobar\x07')) self.failIf(ircutils.isChannel('foo')) self.failIf(ircutils.isChannel('')) def testBold(self): s = ircutils.bold('foo') self.assertEqual(s[0], '\x02') self.assertEqual(s[-1], '\x02') def testItalic(self): s = ircutils.italic('foo') self.assertEqual(s[0], '\x1d') self.assertEqual(s[-1], '\x1d') def testUnderline(self): s = ircutils.underline('foo') self.assertEqual(s[0], '\x1f') self.assertEqual(s[-1], '\x1f') def testReverse(self): s = ircutils.reverse('foo') self.assertEqual(s[0], '\x16') self.assertEqual(s[-1], '\x16') def testMircColor(self): # No colors provided should return the same string s = 'foo' self.assertEqual(s, ircutils.mircColor(s)) # Test positional args self.assertEqual('\x0300foo\x03', ircutils.mircColor(s, 'white')) self.assertEqual('\x031,02foo\x03',ircutils.mircColor(s,'black','blue')) self.assertEqual('\x0300,03foo\x03', ircutils.mircColor(s, None, 'green')) # Test keyword args self.assertEqual('\x0304foo\x03', ircutils.mircColor(s, fg='red')) self.assertEqual('\x0300,05foo\x03', ircutils.mircColor(s, bg='brown')) self.assertEqual('\x036,07foo\x03', ircutils.mircColor(s, bg='orange', fg='purple')) # Commented out because we don't map numbers to colors anymore. ## def testMircColors(self): ## # Make sure all (k, v) pairs are also (v, k) pairs. ## for (k, v) in ircutils.mircColors.items(): ## if k: ## self.assertEqual(ircutils.mircColors[v], k) def testStripBold(self): self.assertEqual(ircutils.stripBold(ircutils.bold('foo')), 'foo') def testStripItalic(self): self.assertEqual(ircutils.stripItalic(ircutils.italic('foo')), 'foo') def testStripColor(self): self.assertEqual(ircutils.stripColor('\x02bold\x0302,04foo\x03bar\x0f'), '\x02boldfoobar\x0f') self.assertEqual(ircutils.stripColor('\x03foo\x03'), 'foo') self.assertEqual(ircutils.stripColor('\x03foo\x0F'), 'foo\x0F') self.assertEqual(ircutils.stripColor('\x0312foo\x03'), 'foo') self.assertEqual(ircutils.stripColor('\x0312,14foo\x03'), 'foo') self.assertEqual(ircutils.stripColor('\x03,14foo\x03'), 'foo') self.assertEqual(ircutils.stripColor('\x03,foo\x03'), ',foo') self.assertEqual(ircutils.stripColor('\x0312foo\x0F'), 'foo\x0F') self.assertEqual(ircutils.stripColor('\x0312,14foo\x0F'), 'foo\x0F') self.assertEqual(ircutils.stripColor('\x03,14foo\x0F'), 'foo\x0F') self.assertEqual(ircutils.stripColor('\x03,foo\x0F'), ',foo\x0F') def testStripReverse(self): self.assertEqual(ircutils.stripReverse(ircutils.reverse('foo')), 'foo') def testStripUnderline(self): self.assertEqual(ircutils.stripUnderline(ircutils.underline('foo')), 'foo') def testStripFormatting(self): self.assertEqual(ircutils.stripFormatting(ircutils.bold('foo')), 'foo') self.assertEqual(ircutils.stripFormatting(ircutils.italic('foo')), 'foo') self.assertEqual(ircutils.stripFormatting(ircutils.reverse('foo')), 'foo') self.assertEqual(ircutils.stripFormatting(ircutils.underline('foo')), 'foo') self.assertEqual(ircutils.stripFormatting('\x02bold\x0302,04foo\x03' 'bar\x0f'), 'boldfoobar') s = ircutils.mircColor('[', 'blue') + ircutils.bold('09:21') self.assertEqual(ircutils.stripFormatting(s), '[09:21') def testSafeArgument(self): s = 'I have been running for 9 seconds' bolds = ircutils.bold(s) colors = ircutils.mircColor(s, 'pink', 'orange') self.assertEqual(s, ircutils.safeArgument(s)) self.assertEqual(bolds, ircutils.safeArgument(bolds)) self.assertEqual(colors, ircutils.safeArgument(colors)) def testSafeArgumentConvertsToString(self): self.assertEqual('1', ircutils.safeArgument(1)) self.assertEqual(str(None), ircutils.safeArgument(None)) def testIsNick(self): try: original = conf.supybot.protocols.irc.strictRfc() conf.supybot.protocols.irc.strictRfc.setValue(True) self.failUnless(ircutils.isNick('jemfinch')) self.failUnless(ircutils.isNick('jemfinch0')) self.failUnless(ircutils.isNick('[0]')) self.failUnless(ircutils.isNick('{jemfinch}')) self.failUnless(ircutils.isNick('[jemfinch]')) self.failUnless(ircutils.isNick('jem|finch')) self.failUnless(ircutils.isNick('\\```')) self.failUnless(ircutils.isNick('`')) self.failUnless(ircutils.isNick('A')) self.failIf(ircutils.isNick('')) self.failIf(ircutils.isNick('8foo')) self.failIf(ircutils.isNick('10')) self.failIf(ircutils.isNick('-')) self.failIf(ircutils.isNick('-foo')) conf.supybot.protocols.irc.strictRfc.setValue(False) self.failUnless(ircutils.isNick('services@something.undernet.net')) finally: conf.supybot.protocols.irc.strictRfc.setValue(original) def testIsNickNeverAllowsSpaces(self): try: original = conf.supybot.protocols.irc.strictRfc() conf.supybot.protocols.irc.strictRfc.setValue(True) self.failIf(ircutils.isNick('foo bar')) conf.supybot.protocols.irc.strictRfc.setValue(False) self.failIf(ircutils.isNick('foo bar')) finally: conf.supybot.protocols.irc.strictRfc.setValue(original) def testStandardSubstitute(self): # Stub out random msg and irc objects that provide what # standardSubstitute wants msg = ircmsgs.IrcMsg(':%s PRIVMSG #channel :stuff' % self.hostmask) class Irc(object): nick = 'bob' network = 'testnet' irc = Irc() f = ircutils.standardSubstitute vars = {'foo': 'bar', 'b': 'c', 'i': 100, 'f': lambda: 'called'} self.assertEqual(f(irc, msg, '$foo', vars), 'bar') self.assertEqual(f(irc, None, '$foo', vars), 'bar') self.assertEqual(f(None, None, '$foo', vars), 'bar') self.assertEqual(f(None, msg, '$foo', vars), 'bar') self.assertEqual(f(irc, msg, '${foo}', vars), 'bar') self.assertEqual(f(irc, msg, '$b', vars), 'c') self.assertEqual(f(irc, msg, '${b}', vars), 'c') self.assertEqual(f(irc, msg, '$i', vars), '100') self.assertEqual(f(irc, msg, '${i}', vars), '100') self.assertEqual(f(irc, msg, '$f', vars), 'called') self.assertEqual(f(irc, msg, '${f}', vars), 'called') self.assertEqual(f(irc, msg, '$b:$i', vars), 'c:100') def testBanmask(self): for msg in msgs: if ircutils.isUserHostmask(msg.prefix): banmask = ircutils.banmask(msg.prefix) self.failUnless(ircutils.hostmaskPatternEqual(banmask, msg.prefix), '%r didn\'t match %r' % (msg.prefix, banmask)) self.assertEqual(ircutils.banmask('foobar!user@host'), '*!*@host') self.assertEqual(ircutils.banmask('foobar!user@host.tld'), '*!*@host.tld') self.assertEqual(ircutils.banmask('foobar!user@sub.host.tld'), '*!*@*.host.tld') self.assertEqual(ircutils.banmask('foo!bar@2001::'), '*!*@2001::*') def testSeparateModes(self): self.assertEqual(ircutils.separateModes(['+ooo', 'x', 'y', 'z']), [('+o', 'x'), ('+o', 'y'), ('+o', 'z')]) self.assertEqual(ircutils.separateModes(['+o-o', 'x', 'y']), [('+o', 'x'), ('-o', 'y')]) self.assertEqual(ircutils.separateModes(['+s-o', 'x']), [('+s', None), ('-o', 'x')]) self.assertEqual(ircutils.separateModes(['+sntl', '100']), [('+s', None),('+n', None),('+t', None),('+l', 100)]) def testNickFromHostmask(self): self.assertEqual(ircutils.nickFromHostmask('nick!user@host.domain.tld'), 'nick') # Hostmasks with user prefixes are sent via userhost-in-names. We need to # properly handle the case where ! is a prefix and not grab '' as the nick # instead. self.assertEqual(ircutils.nickFromHostmask('@nick!user@some.other.host'), '@nick') self.assertEqual(ircutils.nickFromHostmask('!@nick!user@some.other.host'), '!@nick') def testToLower(self): self.assertEqual('jemfinch', ircutils.toLower('jemfinch')) self.assertEqual('{}|^', ircutils.toLower('[]\\~')) def testReplyTo(self): prefix = 'foo!bar@baz' channel = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) private = ircmsgs.privmsg('jemfinch', 'bar baz', prefix=prefix) self.assertEqual(ircutils.replyTo(channel), channel.args[0]) self.assertEqual(ircutils.replyTo(private), private.nick) def testJoinModes(self): plusE = ('+e', '*!*@*ohio-state.edu') plusB = ('+b', '*!*@*umich.edu') minusL = ('-l', None) modes = [plusB, plusE, minusL] self.assertEqual(ircutils.joinModes(modes), ['+be-l', plusB[1], plusE[1]]) def testDccIpStuff(self): def randomIP(): def rand(): return random.randrange(0, 256) return '.'.join(map(str, [rand(), rand(), rand(), rand()])) for _ in range(100): # 100 should be good :) ip = randomIP() self.assertEqual(ip, ircutils.unDccIP(ircutils.dccIP(ip))) class IrcDictTestCase(SupyTestCase): def test(self): d = ircutils.IrcDict() d['#FOO'] = 'bar' self.assertEqual(d['#FOO'], 'bar') self.assertEqual(d['#Foo'], 'bar') self.assertEqual(d['#foo'], 'bar') del d['#fOO'] d['jemfinch{}'] = 'bar' self.assertEqual(d['jemfinch{}'], 'bar') self.assertEqual(d['jemfinch[]'], 'bar') self.assertEqual(d['JEMFINCH[]'], 'bar') def testKeys(self): d = ircutils.IrcDict() self.assertEqual(d.keys(), []) def testSetdefault(self): d = ircutils.IrcDict() d.setdefault('#FOO', []).append(1) self.assertEqual(d['#foo'], [1]) self.assertEqual(d['#fOO'], [1]) self.assertEqual(d['#FOO'], [1]) def testGet(self): d = ircutils.IrcDict() self.assertEqual(d.get('#FOO'), None) d['#foo'] = 1 self.assertEqual(d.get('#FOO'), 1) def testContains(self): d = ircutils.IrcDict() d['#FOO'] = None self.failUnless('#foo' in d) d['#fOOBAR[]'] = None self.failUnless('#foobar{}' in d) def testGetSetItem(self): d = ircutils.IrcDict() d['#FOO'] = 12 self.assertEqual(12, d['#foo']) d['#fOOBAR[]'] = 'blah' self.assertEqual('blah', d['#foobar{}']) def testCopyable(self): d = ircutils.IrcDict() d['foo'] = 'bar' self.failUnless(d == copy.copy(d)) self.failUnless(d == copy.deepcopy(d)) class IrcSetTestCase(SupyTestCase): def test(self): s = ircutils.IrcSet() s.add('foo') s.add('bar') self.failUnless('foo' in s) self.failUnless('FOO' in s) s.discard('alfkj') s.remove('FOo') self.failIf('foo' in s) self.failIf('FOo' in s) def testCopy(self): s = ircutils.IrcSet() s.add('foo') s.add('bar') s1 = copy.deepcopy(s) self.failUnless('foo' in s) self.failUnless('FOO' in s) s.discard('alfkj') s.remove('FOo') self.failIf('foo' in s) self.failIf('FOo' in s) self.failUnless('foo' in s1) self.failUnless('FOO' in s1) s1.discard('alfkj') s1.remove('FOo') self.failIf('foo' in s1) self.failIf('FOo' in s1) class IrcStringTestCase(SupyTestCase): def testEquality(self): self.assertEqual('#foo', ircutils.IrcString('#foo')) self.assertEqual('#foo', ircutils.IrcString('#FOO')) self.assertEqual('#FOO', ircutils.IrcString('#foo')) self.assertEqual('#FOO', ircutils.IrcString('#FOO')) self.assertEqual(hash(ircutils.IrcString('#FOO')), hash(ircutils.IrcString('#foo'))) def testInequality(self): s1 = 'supybot' s2 = ircutils.IrcString('Supybot') self.failUnless(s1 == s2) self.failIf(s1 != s2) class AuthenticateTestCase(SupyTestCase): PAIRS = [ (b'', ['+']), (b'foo'*150, [ 'Zm9v'*100, 'Zm9v'*50 ]), (b'foo'*200, [ 'Zm9v'*100, 'Zm9v'*100, '+']) ] def assertMessages(self, got, should): got = list(got) for (s1, s2) in zip(got, should): self.assertEqual(s1, s2, (got, should)) def testGenerator(self): for (decoded, encoded) in self.PAIRS: self.assertMessages( ircutils.authenticate_generator(decoded), encoded) def testDecoder(self): for (decoded, encoded) in self.PAIRS: decoder = ircutils.AuthenticateDecoder() for chunk in encoded: self.assertFalse(decoder.ready, (decoded, encoded)) decoder.feed(ircmsgs.IrcMsg(command='AUTHENTICATE', args=(chunk,))) self.assertTrue(decoder.ready) self.assertEqual(decoder.get(), decoded) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_ircmsgs.py0000644000175000017500000003003313233426066017355 0ustar valval00000000000000### # 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 * import time import copy import pickle import supybot.conf as conf import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils # The test framework used to provide these, but not it doesn't. We'll add # messages to as we find bugs (if indeed we find bugs). msgs = [] rawmsgs = [] class IrcMsgTestCase(SupyTestCase): def testLen(self): for msg in msgs: if msg.prefix: strmsg = str(msg) self.failIf(len(msg) != len(strmsg) and \ strmsg.replace(':', '') == strmsg) def testRepr(self): IrcMsg = ircmsgs.IrcMsg for msg in msgs: self.assertEqual(msg, eval(repr(msg))) def testStr(self): for (rawmsg, msg) in zip(rawmsgs, msgs): strmsg = str(msg).strip() self.failIf(rawmsg != strmsg and \ strmsg.replace(':', '') == strmsg) def testEq(self): for msg in msgs: self.assertEqual(msg, msg) self.failIf(msgs and msgs[0] == []) # Comparison to unhashable type. def testNe(self): for msg in msgs: self.failIf(msg != msg) ## def testImmutability(self): ## s = 'something else' ## t = ('foo', 'bar', 'baz') ## for msg in msgs: ## self.assertRaises(AttributeError, setattr, msg, 'prefix', s) ## self.assertRaises(AttributeError, setattr, msg, 'nick', s) ## self.assertRaises(AttributeError, setattr, msg, 'user', s) ## self.assertRaises(AttributeError, setattr, msg, 'host', s) ## self.assertRaises(AttributeError, setattr, msg, 'command', s) ## self.assertRaises(AttributeError, setattr, msg, 'args', t) ## if msg.args: ## def setArgs(msg): ## msg.args[0] = s ## self.assertRaises(TypeError, setArgs, msg) def testInit(self): for msg in msgs: self.assertEqual(msg, ircmsgs.IrcMsg(prefix=msg.prefix, command=msg.command, args=msg.args)) self.assertEqual(msg, ircmsgs.IrcMsg(msg=msg)) self.assertRaises(ValueError, ircmsgs.IrcMsg, args=('foo', 'bar'), prefix='foo!bar@baz') m = ircmsgs.IrcMsg(prefix='foo!bar@baz', args=('foo', 'bar'), command='CMD') self.assertIs(m.time, None) m.time = 24 self.assertEqual(ircmsgs.IrcMsg(msg=m).time, 24) def testPickleCopy(self): for msg in msgs: self.assertEqual(msg, pickle.loads(pickle.dumps(msg))) self.assertEqual(msg, copy.copy(msg)) def testHashNotZero(self): zeroes = 0 for msg in msgs: if hash(msg) == 0: zeroes += 1 self.failIf(zeroes > (len(msgs)/10), 'Too many zero hashes.') def testMsgKeywordHandledProperly(self): msg = ircmsgs.notice('foo', 'bar') msg2 = ircmsgs.IrcMsg(msg=msg, command='PRIVMSG') self.assertEqual(msg2.command, 'PRIVMSG') self.assertEqual(msg2.args, msg.args) def testMalformedIrcMsgRaised(self): self.assertRaises(ircmsgs.MalformedIrcMsg, ircmsgs.IrcMsg, ':foo') self.assertRaises(ircmsgs.MalformedIrcMsg, ircmsgs.IrcMsg, args=('biff',), prefix='foo!bar@baz') def testTags(self): m = ircmsgs.privmsg('foo', 'bar') self.failIf(m.repliedTo) m.tag('repliedTo') self.failUnless(m.repliedTo) m.tag('repliedTo') self.failUnless(m.repliedTo) m.tag('repliedTo', 12) self.assertEqual(m.repliedTo, 12) def testServerTags(self): s = '@aaa=b\\:bb;ccc;example.com/ddd=ee\\\\se ' \ ':nick!ident@host.com PRIVMSG me :Hello' m = ircmsgs.IrcMsg(s) self.assertEqual(m.server_tags, { 'aaa': 'b;bb', 'ccc': None, 'example.com/ddd': 'ee\\se'}) self.assertEqual(m.prefix, 'nick!ident@host.com') self.assertEqual(m.command, 'PRIVMSG') self.assertEqual(m.args, ('me', 'Hello')) self.assertEqual(str(m), s + '\n') def testTime(self): before = time.time() msg = ircmsgs.IrcMsg('PRIVMSG #foo :foo') after = time.time() self.assertTrue(before <= msg.time <= after) msg = ircmsgs.IrcMsg('@time=2011-10-19T16:40:51.620Z ' ':Angel!angel@example.org PRIVMSG Wiz :Hello') self.assertEqual(msg.time, 1319042451.62) class FunctionsTestCase(SupyTestCase): def testIsAction(self): L = [':jemfinch!~jfincher@ts26-2.homenet.ohio-state.edu PRIVMSG' ' #sourcereview :ACTION does something', ':supybot!~supybot@underthemain.net PRIVMSG #sourcereview ' ':ACTION beats angryman senseless with a Unix manual (#2)', ':supybot!~supybot@underthemain.net PRIVMSG #sourcereview ' ':ACTION beats ang senseless with a 50lb Unix manual (#2)', ':supybot!~supybot@underthemain.net PRIVMSG #sourcereview ' ':ACTION resizes angryman\'s terminal to 40x24 (#16)'] msgs = list(map(ircmsgs.IrcMsg, L)) for msg in msgs: self.failUnless(ircmsgs.isAction(msg)) def testIsActionIsntStupid(self): m = ircmsgs.privmsg('#x', '\x01NOTANACTION foo\x01') self.failIf(ircmsgs.isAction(m)) m = ircmsgs.privmsg('#x', '\x01ACTION foo bar\x01') self.failUnless(ircmsgs.isAction(m)) def testIsCtcp(self): self.failUnless(ircmsgs.isCtcp(ircmsgs.privmsg('foo', '\x01VERSION\x01'))) self.failIf(ircmsgs.isCtcp(ircmsgs.privmsg('foo', '\x01'))) def testIsActionFalseWhenNoSpaces(self): msg = ircmsgs.IrcMsg('PRIVMSG #foo :\x01ACTIONfoobar\x01') self.failIf(ircmsgs.isAction(msg)) def testUnAction(self): s = 'foo bar baz' msg = ircmsgs.action('#foo', s) self.assertEqual(ircmsgs.unAction(msg), s) def testPrivmsg(self): self.assertEqual(str(ircmsgs.privmsg('foo', 'bar')), 'PRIVMSG foo :bar\r\n') self.assertEqual(str(ircmsgs.privmsg('foo,bar', 'baz')), 'PRIVMSG foo,bar :baz\r\n') def testWhois(self): with conf.supybot.protocols.irc.strictRfc.context(True): self.assertEqual(str(ircmsgs.whois('foo')), 'WHOIS :foo\r\n') self.assertEqual(str(ircmsgs.whois('foo,bar')), 'WHOIS :foo,bar\r\n') self.assertRaises(AssertionError, ircmsgs.whois, '#foo') self.assertRaises(AssertionError, ircmsgs.whois, 'foo,#foo') def testBan(self): channel = '#osu' ban = '*!*@*.edu' exception = '*!*@*ohio-state.edu' noException = ircmsgs.ban(channel, ban) self.assertEqual(ircutils.separateModes(noException.args[1:]), [('+b', ban)]) withException = ircmsgs.ban(channel, ban, exception) self.assertEqual(ircutils.separateModes(withException.args[1:]), [('+b', ban), ('+e', exception)]) def testBans(self): channel = '#osu' bans = ['*!*@*', 'jemfinch!*@*'] exceptions = ['*!*@*ohio-state.edu'] noException = ircmsgs.bans(channel, bans) self.assertEqual(ircutils.separateModes(noException.args[1:]), [('+b', bans[0]), ('+b', bans[1])]) withExceptions = ircmsgs.bans(channel, bans, exceptions) self.assertEqual(ircutils.separateModes(withExceptions.args[1:]), [('+b', bans[0]), ('+b', bans[1]), ('+e', exceptions[0])]) def testUnban(self): channel = '#supybot' ban = 'foo!bar@baz' self.assertEqual(str(ircmsgs.unban(channel, ban)), 'MODE %s -b :%s\r\n' % (channel, ban)) def testJoin(self): channel = '#osu' key = 'michiganSucks' self.assertEqual(ircmsgs.join(channel).args, ('#osu',)) self.assertEqual(ircmsgs.join(channel, key).args, ('#osu', 'michiganSucks')) def testJoins(self): channels = ['#osu', '#umich'] keys = ['michiganSucks', 'osuSucks'] self.assertEqual(ircmsgs.joins(channels).args, ('#osu,#umich',)) self.assertEqual(ircmsgs.joins(channels, keys).args, ('#osu,#umich', 'michiganSucks,osuSucks')) keys.pop() self.assertEqual(ircmsgs.joins(channels, keys).args, ('#osu,#umich', 'michiganSucks')) def testQuit(self): self.failUnless(ircmsgs.quit(prefix='foo!bar@baz')) def testOps(self): m = ircmsgs.ops('#foo', ['foo', 'bar', 'baz']) self.assertEqual(str(m), 'MODE #foo +ooo foo bar :baz\r\n') def testDeops(self): m = ircmsgs.deops('#foo', ['foo', 'bar', 'baz']) self.assertEqual(str(m), 'MODE #foo -ooo foo bar :baz\r\n') def testVoices(self): m = ircmsgs.voices('#foo', ['foo', 'bar', 'baz']) self.assertEqual(str(m), 'MODE #foo +vvv foo bar :baz\r\n') def testDevoices(self): m = ircmsgs.devoices('#foo', ['foo', 'bar', 'baz']) self.assertEqual(str(m), 'MODE #foo -vvv foo bar :baz\r\n') def testHalfops(self): m = ircmsgs.halfops('#foo', ['foo', 'bar', 'baz']) self.assertEqual(str(m), 'MODE #foo +hhh foo bar :baz\r\n') def testDehalfops(self): m = ircmsgs.dehalfops('#foo', ['foo', 'bar', 'baz']) self.assertEqual(str(m), 'MODE #foo -hhh foo bar :baz\r\n') def testMode(self): m = ircmsgs.mode('#foo', ('-b', 'foo!bar@baz')) s = str(m) self.assertEqual(s, 'MODE #foo -b :foo!bar@baz\r\n') def testIsSplit(self): m = ircmsgs.IrcMsg(prefix="caker!~caker@ns.theshore.net", command="QUIT", args=('jupiter.oftc.net quasar.oftc.net',)) self.failUnless(ircmsgs.isSplit(m)) m = ircmsgs.IrcMsg(prefix="bzbot!Brad2901@ACC87473.ipt.aol.com", command="QUIT", args=('Read error: 110 (Connection timed out)',)) self.failIf(ircmsgs.isSplit(m)) m = ircmsgs.IrcMsg(prefix="JibberJim!~none@8212cl.b0nwbeoe.co.uk", command="QUIT", args=('"Bye!"',)) self.failIf(ircmsgs.isSplit(m)) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_irclib.py0000644000175000017500000007247313233426066017170 0ustar valval00000000000000## # 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 * import copy import pickle import supybot.conf as conf import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs # The test framework used to provide these, but not it doesn't. We'll add # messages to as we find bugs (if indeed we find bugs). msgs = [] rawmsgs = [] class IrcMsgQueueTestCase(SupyTestCase): mode = ircmsgs.op('#foo', 'jemfinch') msg = ircmsgs.privmsg('#foo', 'hey, you') msgs = [ircmsgs.privmsg('#foo', str(i)) for i in range(10)] kick = ircmsgs.kick('#foo', 'PeterB') pong = ircmsgs.pong('123') ping = ircmsgs.ping('123') topic = ircmsgs.topic('#foo') notice = ircmsgs.notice('jemfinch', 'supybot here') join = ircmsgs.join('#foo') who = ircmsgs.who('#foo') def testInit(self): q = irclib.IrcMsgQueue([self.msg, self.topic, self.ping]) self.assertEqual(len(q), 3) def testLen(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) self.assertEqual(len(q), 1) q.enqueue(self.mode) self.assertEqual(len(q), 2) q.enqueue(self.kick) self.assertEqual(len(q), 3) q.enqueue(self.topic) self.assertEqual(len(q), 4) q.dequeue() self.assertEqual(len(q), 3) q.dequeue() self.assertEqual(len(q), 2) q.dequeue() self.assertEqual(len(q), 1) q.dequeue() self.assertEqual(len(q), 0) def testContains(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) q.enqueue(self.msg) q.enqueue(self.msg) self.failUnless(self.msg in q) q.dequeue() self.failUnless(self.msg in q) q.dequeue() self.failUnless(self.msg in q) q.dequeue() self.failIf(self.msg in q) def testRepr(self): q = irclib.IrcMsgQueue() self.assertEqual(repr(q), 'IrcMsgQueue([])') q.enqueue(self.msg) try: repr(q) except Exception as e: self.fail('repr(q) raised an exception: %s' % utils.exnToString(e)) def testEmpty(self): q = irclib.IrcMsgQueue() self.failIf(q) def testEnqueueDequeue(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) self.failUnless(q) self.assertEqual(self.msg, q.dequeue()) self.failIf(q) q.enqueue(self.msg) q.enqueue(self.notice) self.assertEqual(self.msg, q.dequeue()) self.assertEqual(self.notice, q.dequeue()) for msg in self.msgs: q.enqueue(msg) for msg in self.msgs: self.assertEqual(msg, q.dequeue()) def testPrioritizing(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) q.enqueue(self.mode) self.assertEqual(self.mode, q.dequeue()) self.assertEqual(self.msg, q.dequeue()) q.enqueue(self.msg) q.enqueue(self.kick) self.assertEqual(self.kick, q.dequeue()) self.assertEqual(self.msg, q.dequeue()) q.enqueue(self.ping) q.enqueue(self.msgs[0]) q.enqueue(self.kick) q.enqueue(self.msgs[1]) q.enqueue(self.mode) self.assertEqual(self.kick, q.dequeue()) self.assertEqual(self.mode, q.dequeue()) self.assertEqual(self.ping, q.dequeue()) self.assertEqual(self.msgs[0], q.dequeue()) self.assertEqual(self.msgs[1], q.dequeue()) def testNoIdenticals(self): configVar = conf.supybot.protocols.irc.queuing.duplicates original = configVar() try: configVar.setValue(True) q = irclib.IrcMsgQueue() q.enqueue(self.msg) q.enqueue(self.msg) self.assertEqual(self.msg, q.dequeue()) self.failIf(q) finally: configVar.setValue(original) def testJoinBeforeWho(self): q = irclib.IrcMsgQueue() q.enqueue(self.join) q.enqueue(self.who) self.assertEqual(self.join, q.dequeue()) self.assertEqual(self.who, q.dequeue()) ## q.enqueue(self.who) ## q.enqueue(self.join) ## self.assertEqual(self.join, q.dequeue()) ## self.assertEqual(self.who, q.dequeue()) def testTopicBeforePrivmsg(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) q.enqueue(self.topic) self.assertEqual(self.topic, q.dequeue()) self.assertEqual(self.msg, q.dequeue()) def testModeBeforePrivmsg(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) q.enqueue(self.mode) self.assertEqual(self.mode, q.dequeue()) self.assertEqual(self.msg, q.dequeue()) q.enqueue(self.mode) q.enqueue(self.msg) self.assertEqual(self.mode, q.dequeue()) self.assertEqual(self.msg, q.dequeue()) class ChannelStateTestCase(SupyTestCase): def testPickleCopy(self): c = irclib.ChannelState() self.assertEqual(pickle.loads(pickle.dumps(c)), c) c.addUser('jemfinch') c1 = pickle.loads(pickle.dumps(c)) self.assertEqual(c, c1) c.removeUser('jemfinch') self.failIf('jemfinch' in c.users) self.failUnless('jemfinch' in c1.users) def testCopy(self): c = irclib.ChannelState() c.addUser('jemfinch') c1 = copy.deepcopy(c) c.removeUser('jemfinch') self.failIf('jemfinch' in c.users) self.failUnless('jemfinch' in c1.users) def testAddUser(self): c = irclib.ChannelState() c.addUser('foo') self.failUnless('foo' in c.users) self.failIf('foo' in c.ops) self.failIf('foo' in c.voices) self.failIf('foo' in c.halfops) c.addUser('+bar') self.failUnless('bar' in c.users) self.failUnless('bar' in c.voices) self.failIf('bar' in c.ops) self.failIf('bar' in c.halfops) c.addUser('%baz') self.failUnless('baz' in c.users) self.failUnless('baz' in c.halfops) self.failIf('baz' in c.voices) self.failIf('baz' in c.ops) c.addUser('@quuz') self.failUnless('quuz' in c.users) self.failUnless('quuz' in c.ops) self.failIf('quuz' in c.halfops) self.failIf('quuz' in c.voices) class IrcStateTestCase(SupyTestCase): class FakeIrc: nick = 'nick' prefix = 'nick!user@host' irc = FakeIrc() def testKickRemovesChannel(self): st = irclib.IrcState() st.channels['#foo'] = irclib.ChannelState() m = ircmsgs.kick('#foo', self.irc.nick, prefix=self.irc.prefix) st.addMsg(self.irc, m) self.failIf('#foo' in st.channels) def testAddMsgRemovesOpsProperly(self): st = irclib.IrcState() st.channels['#foo'] = irclib.ChannelState() st.channels['#foo'].ops.add('bar') m = ircmsgs.mode('#foo', ('-o', 'bar')) st.addMsg(self.irc, m) self.failIf('bar' in st.channels['#foo'].ops) def testNickChangesChangeChannelUsers(self): st = irclib.IrcState() st.channels['#foo'] = irclib.ChannelState() st.channels['#foo'].addUser('@bar') self.failUnless('bar' in st.channels['#foo'].users) self.failUnless(st.channels['#foo'].isOp('bar')) st.addMsg(self.irc, ircmsgs.IrcMsg(':bar!asfd@asdf.com NICK baz')) self.failIf('bar' in st.channels['#foo'].users) self.failIf(st.channels['#foo'].isOp('bar')) self.failUnless('baz' in st.channels['#foo'].users) self.failUnless(st.channels['#foo'].isOp('baz')) def testHistory(self): if len(msgs) < 10: return maxHistoryLength = conf.supybot.protocols.irc.maxHistoryLength with maxHistoryLength.context(10): state = irclib.IrcState() for msg in msgs: try: state.addMsg(self.irc, msg) except Exception: pass self.failIf(len(state.history) > maxHistoryLength()) self.assertEqual(len(state.history), maxHistoryLength()) self.assertEqual(list(state.history), msgs[len(msgs) - maxHistoryLength():]) def testWasteland005(self): state = irclib.IrcState() # Here we're testing if PREFIX works without the (ov) there. state.addMsg(self.irc, ircmsgs.IrcMsg(':desolate.wasteland.org 005 jemfinch NOQUIT WATCH=128 SAFELIST MODES=6 MAXCHANNELS=10 MAXBANS=100 NICKLEN=30 TOPICLEN=307 KICKLEN=307 CHANTYPES=&# PREFIX=@+ NETWORK=DALnet SILENCE=10 :are available on this server')) self.assertEqual(state.supported['prefix']['o'], '@') self.assertEqual(state.supported['prefix']['v'], '+') def testIRCNet005(self): state = irclib.IrcState() # Testing IRCNet's misuse of MAXBANS state.addMsg(self.irc, ircmsgs.IrcMsg(':irc.inet.tele.dk 005 adkwbot WALLCHOPS KNOCK EXCEPTS INVEX MODES=4 MAXCHANNELS=20 MAXBANS=beI:100 MAXTARGETS=4 NICKLEN=9 TOPICLEN=120 KICKLEN=90 :are supported by this server')) self.assertEqual(state.supported['maxbans'], 100) def testSupportedUmodes(self): state = irclib.IrcState() state.addMsg(self.irc, ircmsgs.IrcMsg(':coulomb.oftc.net 004 testnick coulomb.oftc.net hybrid-7.2.2+oftc1.6.8 CDGPRSabcdfgiklnorsuwxyz biklmnopstveI bkloveI')) self.assertEqual(state.supported['umodes'], frozenset('CDGPRSabcdfgiklnorsuwxyz')) self.assertEqual(state.supported['chanmodes'], frozenset('biklmnopstveI')) def testEmptyTopic(self): state = irclib.IrcState() state.addMsg(self.irc, ircmsgs.topic('#foo')) def testPickleCopy(self): state = irclib.IrcState() self.assertEqual(state, pickle.loads(pickle.dumps(state))) for msg in msgs: try: state.addMsg(self.irc, msg) except Exception: pass self.assertEqual(state, pickle.loads(pickle.dumps(state))) def testCopy(self): state = irclib.IrcState() self.assertEqual(state, state.copy()) for msg in msgs: try: state.addMsg(self.irc, msg) except Exception: pass self.assertEqual(state, state.copy()) def testCopyCopiesChannels(self): state = irclib.IrcState() stateCopy = state.copy() state.channels['#foo'] = None self.failIf('#foo' in stateCopy.channels) def testJoin(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix)) self.failUnless('#foo' in st.channels) self.failUnless(self.irc.nick in st.channels['#foo'].users) st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz')) self.failUnless('foo' in st.channels['#foo'].users) st2 = st.copy() st.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) self.failIf('foo' in st.channels['#foo'].users) self.failUnless('foo' in st2.channels['#foo'].users) def testEq(self): state1 = irclib.IrcState() state2 = irclib.IrcState() self.assertEqual(state1, state2) for msg in msgs: try: state1.addMsg(self.irc, msg) state2.addMsg(self.irc, msg) self.assertEqual(state1, state2) except Exception: pass def testHandlesModes(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix)) self.failIf('bar' in st.channels['#foo'].ops) st.addMsg(self.irc, ircmsgs.op('#foo', 'bar')) self.failUnless('bar' in st.channels['#foo'].ops) st.addMsg(self.irc, ircmsgs.deop('#foo', 'bar')) self.failIf('bar' in st.channels['#foo'].ops) self.failIf('bar' in st.channels['#foo'].voices) st.addMsg(self.irc, ircmsgs.voice('#foo', 'bar')) self.failUnless('bar' in st.channels['#foo'].voices) st.addMsg(self.irc, ircmsgs.devoice('#foo', 'bar')) self.failIf('bar' in st.channels['#foo'].voices) self.failIf('bar' in st.channels['#foo'].halfops) st.addMsg(self.irc, ircmsgs.halfop('#foo', 'bar')) self.failUnless('bar' in st.channels['#foo'].halfops) st.addMsg(self.irc, ircmsgs.dehalfop('#foo', 'bar')) self.failIf('bar' in st.channels['#foo'].halfops) def testDoModeOnlyChannels(self): st = irclib.IrcState() self.assert_(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1) class IrcTestCase(SupyTestCase): def setUp(self): self.irc = irclib.Irc('test') #m = self.irc.takeMsg() #self.failUnless(m.command == 'PASS', 'Expected PASS, got %r.' % m) m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.failUnless(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.failUnless(m.command == 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.failUnless(m.command == 'USER', 'Expected USER, got %r.' % m) # TODO self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', '*', 'account-tag multi-prefix'))) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'extended-join'))) m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) # NOTE: Capabilities are requested in alphabetic order, because # sets are unordered, and their "order" is nondeterministic. self.assertEqual(m.args[1], 'account-tag extended-join multi-prefix') self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'ACK', 'account-tag multi-prefix extended-join'))) m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) m = self.irc.takeMsg() self.failUnless(m is None, m) def testPingResponse(self): self.irc.feedMsg(ircmsgs.ping('123')) self.assertEqual(ircmsgs.pong('123'), self.irc.takeMsg()) def test433Response(self): # This is necessary; it won't change nick if irc.originalName==irc.nick self.irc.nick = 'somethingElse' self.irc.feedMsg(ircmsgs.IrcMsg('433 * %s :Nickname already in use.' %\ self.irc.nick)) msg = self.irc.takeMsg() self.failUnless(msg.command == 'NICK' and msg.args[0] != self.irc.nick) self.irc.feedMsg(ircmsgs.IrcMsg('433 * %s :Nickname already in use.' %\ self.irc.nick)) msg = self.irc.takeMsg() self.failUnless(msg.command == 'NICK' and msg.args[0] != self.irc.nick) def testSendBeforeQueue(self): while self.irc.takeMsg() is not None: self.irc.takeMsg() self.irc.queueMsg(ircmsgs.IrcMsg('NOTICE #foo bar')) self.irc.sendMsg(ircmsgs.IrcMsg('PRIVMSG #foo yeah!')) msg = self.irc.takeMsg() self.failUnless(msg.command == 'PRIVMSG') msg = self.irc.takeMsg() self.failUnless(msg.command == 'NOTICE') def testNoMsgLongerThan512(self): self.irc.queueMsg(ircmsgs.privmsg('whocares', 'x'*1000)) msg = self.irc.takeMsg() self.failUnless(len(msg) <= 512, 'len(msg) was %s' % len(msg)) def testReset(self): for msg in msgs: try: self.irc.feedMsg(msg) except: pass self.irc.reset() self.failIf(self.irc.state.history) self.failIf(self.irc.state.channels) self.failIf(self.irc.outstandingPing) def testHistory(self): self.irc.reset() msg1 = ircmsgs.IrcMsg('PRIVMSG #linux :foo bar baz!') self.irc.feedMsg(msg1) self.assertEqual(self.irc.state.history[0], msg1) msg2 = ircmsgs.IrcMsg('JOIN #sourcereview') self.irc.feedMsg(msg2) self.assertEqual(list(self.irc.state.history), [msg1, msg2]) def testQuit(self): self.irc.reset() self.irc.feedMsg(ircmsgs.IrcMsg(':someuser JOIN #foo')) self.irc.feedMsg(ircmsgs.IrcMsg(':someuser JOIN #bar')) self.irc.feedMsg(ircmsgs.IrcMsg(':someuser2 JOIN #bar2')) class Callback(irclib.IrcCallback): channels_set = None def name(self): return 'testcallback' def doQuit(self2, irc, msg): self2.channels_set = msg.tagged('channels') c = Callback() self.irc.addCallback(c) try: self.irc.feedMsg(ircmsgs.IrcMsg(':someuser QUIT')) finally: self.irc.removeCallback(c.name()) self.assertEqual(c.channels_set, ircutils.IrcSet(['#foo', '#bar'])) def testNick(self): self.irc.reset() self.irc.feedMsg(ircmsgs.IrcMsg(':someuser JOIN #foo')) self.irc.feedMsg(ircmsgs.IrcMsg(':someuser JOIN #bar')) self.irc.feedMsg(ircmsgs.IrcMsg(':someuser2 JOIN #bar2')) class Callback(irclib.IrcCallback): channels_set = None def name(self): return 'testcallback' def doNick(self2, irc, msg): self2.channels_set = msg.tagged('channels') c = Callback() self.irc.addCallback(c) try: self.irc.feedMsg(ircmsgs.IrcMsg(':someuser NICK newuser')) finally: self.irc.removeCallback(c.name()) self.assertEqual(c.channels_set, ircutils.IrcSet(['#foo', '#bar'])) def testBatch(self): self.irc.reset() self.irc.feedMsg(ircmsgs.IrcMsg(':someuser1 JOIN #foo')) self.irc.feedMsg(ircmsgs.IrcMsg(':host BATCH +name netjoin')) m1 = ircmsgs.IrcMsg('@batch=name :someuser2 JOIN #foo') self.irc.feedMsg(m1) self.irc.feedMsg(ircmsgs.IrcMsg(':someuser3 JOIN #foo')) m2 = ircmsgs.IrcMsg('@batch=name :someuser4 JOIN #foo') self.irc.feedMsg(m2) class Callback(irclib.IrcCallback): batch = None def name(self): return 'testcallback' def doBatch(self2, irc, msg): self2.batch = msg.tagged('batch') c = Callback() self.irc.addCallback(c) try: self.irc.feedMsg(ircmsgs.IrcMsg(':host BATCH -name')) finally: self.irc.removeCallback(c.name()) self.assertEqual(c.batch, irclib.Batch('netjoin', (), [m1, m2])) class SaslTestCase(SupyTestCase): def setUp(self): pass def startCapNegociation(self, caps='sasl'): m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.failUnless(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.failUnless(m.command == 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.failUnless(m.command == 'USER', 'Expected USER, got %r.' % m) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', caps))) if caps: m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'sasl') self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'ACK', 'sasl'))) def endCapNegociation(self): m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) def testPlain(self): try: conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') self.irc = irclib.Irc('test') finally: conf.supybot.networks.test.sasl.username.setValue('') conf.supybot.networks.test.sasl.password.setValue('') self.assertEqual(self.irc.sasl_current_mechanism, None) self.assertEqual(self.irc.sasl_next_mechanisms, ['plain']) self.startCapNegociation() m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('PLAIN',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('+',))) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('amlsbGVzAGppbGxlcwBzZXNhbWU=',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='900', args=('jilles',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='903', args=('jilles',))) self.endCapNegociation() def testExternalFallbackToPlain(self): try: conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') conf.supybot.networks.test.certfile.setValue('foo') self.irc = irclib.Irc('test') finally: conf.supybot.networks.test.sasl.username.setValue('') conf.supybot.networks.test.sasl.password.setValue('') conf.supybot.networks.test.certfile.setValue('') self.assertEqual(self.irc.sasl_current_mechanism, None) self.assertEqual(self.irc.sasl_next_mechanisms, ['external', 'plain']) self.startCapNegociation() m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('EXTERNAL',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='904', args=('mechanism not available',))) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('PLAIN',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('+',))) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('amlsbGVzAGppbGxlcwBzZXNhbWU=',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='900', args=('jilles',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='903', args=('jilles',))) self.endCapNegociation() def testFilter(self): try: conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') conf.supybot.networks.test.certfile.setValue('foo') self.irc = irclib.Irc('test') finally: conf.supybot.networks.test.sasl.username.setValue('') conf.supybot.networks.test.sasl.password.setValue('') conf.supybot.networks.test.certfile.setValue('') self.assertEqual(self.irc.sasl_current_mechanism, None) self.assertEqual(self.irc.sasl_next_mechanisms, ['external', 'plain']) self.startCapNegociation(caps='sasl=foo,plain,bar') m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('PLAIN',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('+',))) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('amlsbGVzAGppbGxlcwBzZXNhbWU=',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='900', args=('jilles',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='903', args=('jilles',))) self.endCapNegociation() def testReauthenticate(self): try: conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') self.irc = irclib.Irc('test') finally: conf.supybot.networks.test.sasl.username.setValue('') conf.supybot.networks.test.sasl.password.setValue('') self.assertEqual(self.irc.sasl_current_mechanism, None) self.assertEqual(self.irc.sasl_next_mechanisms, ['plain']) self.startCapNegociation(caps='') self.endCapNegociation() while self.irc.takeMsg(): pass self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'NEW', 'sasl=EXTERNAL'))) self.irc.takeMsg() # None. But even if it was CAP REQ sasl, it would be ok self.assertEqual(self.irc.takeMsg(), None) try: conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'DEL', 'sasl'))) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'NEW', 'sasl=PLAIN'))) finally: conf.supybot.networks.test.sasl.username.setValue('') conf.supybot.networks.test.sasl.password.setValue('') m = self.irc.takeMsg() self.failUnless(m.command == 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'sasl') self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'ACK', 'sasl'))) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('PLAIN',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('+',))) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('amlsbGVzAGppbGxlcwBzZXNhbWU=',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='900', args=('jilles',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='903', args=('jilles',))) class IrcCallbackTestCase(SupyTestCase): class FakeIrc: pass irc = FakeIrc() def testName(self): class UnnamedIrcCallback(irclib.IrcCallback): pass unnamed = UnnamedIrcCallback() class NamedIrcCallback(irclib.IrcCallback): myName = 'foobar' def name(self): return self.myName named = NamedIrcCallback() self.assertEqual(unnamed.name(), unnamed.__class__.__name__) self.assertEqual(named.name(), named.myName) def testDoCommand(self): def makeCommand(msg): return 'do' + msg.command.capitalize() class DoCommandCatcher(irclib.IrcCallback): def __init__(self): self.L = [] def __getattr__(self, attr): self.L.append(attr) return lambda *args: None doCommandCatcher = DoCommandCatcher() for msg in msgs: doCommandCatcher(self.irc, msg) commands = list(map(makeCommand, msgs)) self.assertEqual(doCommandCatcher.L, commands) def testFirstCommands(self): try: originalNick = conf.supybot.nick() originalUser = conf.supybot.user() originalPassword = conf.supybot.networks.test.password() nick = 'nick' conf.supybot.nick.setValue(nick) user = 'user any user' conf.supybot.user.setValue(user) expected = [ ircmsgs.IrcMsg(command='CAP', args=('LS', '302')), ircmsgs.nick(nick), ircmsgs.user('limnoria', user), ] irc = irclib.Irc('test') msgs = [irc.takeMsg()] while msgs[-1] is not None: msgs.append(irc.takeMsg()) msgs.pop() self.assertEqual(msgs, expected) password = 'password' conf.supybot.networks.test.password.setValue(password) irc = irclib.Irc('test') msgs = [irc.takeMsg()] while msgs[-1] is not None: msgs.append(irc.takeMsg()) msgs.pop() expected.insert(1, ircmsgs.password(password)) self.assertEqual(msgs, expected) finally: conf.supybot.nick.setValue(originalNick) conf.supybot.user.setValue(originalUser) conf.supybot.networks.test.password.setValue(originalPassword) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_ircdb.py0000644000175000017500000005666413233426066017013 0ustar valval00000000000000### # 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 * import os import unittest import supybot.conf as conf import supybot.world as world import supybot.ircdb as ircdb import supybot.ircutils as ircutils class IrcdbTestCase(SupyTestCase): def setUp(self): world.testing = False SupyTestCase.setUp(self) def tearDown(self): world.testing = True SupyTestCase.tearDown(self) class FunctionsTestCase(IrcdbTestCase): def testIsAntiCapability(self): self.failIf(ircdb.isAntiCapability('foo')) self.failIf(ircdb.isAntiCapability('#foo,bar')) self.failUnless(ircdb.isAntiCapability('-foo')) self.failUnless(ircdb.isAntiCapability('#foo,-bar')) self.failUnless(ircdb.isAntiCapability('#foo.bar,-baz')) def testIsChannelCapability(self): self.failIf(ircdb.isChannelCapability('foo')) self.failUnless(ircdb.isChannelCapability('#foo,bar')) self.failUnless(ircdb.isChannelCapability('#foo.bar,baz')) self.failUnless(ircdb.isChannelCapability('#foo,bar.baz')) def testMakeAntiCapability(self): self.assertEqual(ircdb.makeAntiCapability('foo'), '-foo') self.assertEqual(ircdb.makeAntiCapability('#foo,bar'), '#foo,-bar') def testMakeChannelCapability(self): self.assertEqual(ircdb.makeChannelCapability('#f', 'b'), '#f,b') self.assertEqual(ircdb.makeChannelCapability('#f', '-b'), '#f,-b') def testFromChannelCapability(self): self.assertEqual(ircdb.fromChannelCapability('#foo,bar'), ['#foo', 'bar']) self.assertEqual(ircdb.fromChannelCapability('#foo.bar,baz'), ['#foo.bar', 'baz']) self.assertEqual(ircdb.fromChannelCapability('#foo,bar.baz'), ['#foo', 'bar.baz']) def testUnAntiCapability(self): self.assertEqual(ircdb.unAntiCapability('-bar'), 'bar') self.assertEqual(ircdb.unAntiCapability('#foo,-bar'), '#foo,bar') self.assertEqual(ircdb.unAntiCapability('#foo.bar,-baz'), '#foo.bar,baz') def testInvertCapability(self): self.assertEqual(ircdb.invertCapability('bar'), '-bar') self.assertEqual(ircdb.invertCapability('-bar'), 'bar') self.assertEqual(ircdb.invertCapability('#foo,bar'), '#foo,-bar') self.assertEqual(ircdb.invertCapability('#foo,-bar'), '#foo,bar') class CapabilitySetTestCase(SupyTestCase): def testGeneral(self): d = ircdb.CapabilitySet() self.assertRaises(KeyError, d.check, 'foo') d = ircdb.CapabilitySet(('foo',)) self.failUnless(d.check('foo')) self.failIf(d.check('-foo')) d.add('bar') self.failUnless(d.check('bar')) self.failIf(d.check('-bar')) d.add('-baz') self.failIf(d.check('baz')) self.failUnless(d.check('-baz')) d.add('-bar') self.failIf(d.check('bar')) self.failUnless(d.check('-bar')) d.remove('-bar') self.assertRaises(KeyError, d.check, '-bar') self.assertRaises(KeyError, d.check, 'bar') def testReprEval(self): s = ircdb.UserCapabilitySet() self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__)) s.add('foo') self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__)) s.add('bar') self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__)) def testContains(self): s = ircdb.CapabilitySet() self.failIf('foo' in s) self.failIf('-foo' in s) s.add('foo') self.failUnless('foo' in s) self.failUnless('-foo' in s) s.remove('foo') self.failIf('foo' in s) self.failIf('-foo' in s) s.add('-foo') self.failUnless('foo' in s) self.failUnless('-foo' in s) def testCheck(self): s = ircdb.CapabilitySet() self.assertRaises(KeyError, s.check, 'foo') self.assertRaises(KeyError, s.check, '-foo') s.add('foo') self.failUnless(s.check('foo')) self.failIf(s.check('-foo')) s.remove('foo') self.assertRaises(KeyError, s.check, 'foo') self.assertRaises(KeyError, s.check, '-foo') s.add('-foo') self.failIf(s.check('foo')) self.failUnless(s.check('-foo')) s.remove('-foo') self.assertRaises(KeyError, s.check, 'foo') self.assertRaises(KeyError, s.check, '-foo') def testAdd(self): s = ircdb.CapabilitySet() s.add('foo') s.add('-foo') self.failIf(s.check('foo')) self.failUnless(s.check('-foo')) s.add('foo') self.failUnless(s.check('foo')) self.failIf(s.check('-foo')) class UserCapabilitySetTestCase(SupyTestCase): def testOwnerHasAll(self): d = ircdb.UserCapabilitySet(('owner',)) self.failIf(d.check('-foo')) self.failUnless(d.check('foo')) def testOwnerIsAlwaysPresent(self): d = ircdb.UserCapabilitySet() self.failUnless('owner' in d) self.failUnless('-owner' in d) self.failIf(d.check('owner')) d.add('owner') self.failUnless(d.check('owner')) def testReprEval(self): s = ircdb.UserCapabilitySet() self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__)) s.add('foo') self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__)) s.add('bar') self.assertEqual(s, eval(repr(s), ircdb.__dict__, ircdb.__dict__)) def testOwner(self): s = ircdb.UserCapabilitySet() s.add('owner') self.failUnless('foo' in s) self.failUnless('-foo' in s) self.failUnless(s.check('owner')) self.failIf(s.check('-owner')) self.failIf(s.check('-foo')) self.failUnless(s.check('foo')) ## def testWorksAfterReload(self): ## s = ircdb.UserCapabilitySet(['owner']) ## self.failUnless(s.check('owner')) ## import sets ## reload(sets) ## self.failUnless(s.check('owner')) class IrcUserTestCase(IrcdbTestCase): def testCapabilities(self): u = ircdb.IrcUser() u.addCapability('foo') self.failUnless(u._checkCapability('foo')) self.failIf(u._checkCapability('-foo')) u.addCapability('-bar') self.failUnless(u._checkCapability('-bar')) self.failIf(u._checkCapability('bar')) u.removeCapability('foo') u.removeCapability('-bar') self.assertRaises(KeyError, u._checkCapability, 'foo') self.assertRaises(KeyError, u._checkCapability, '-bar') def testAddhostmask(self): u = ircdb.IrcUser() self.assertRaises(ValueError, u.addHostmask, '*!*@*') def testRemoveHostmask(self): u = ircdb.IrcUser() u.addHostmask('foo!bar@baz') self.failUnless(u.checkHostmask('foo!bar@baz')) u.addHostmask('foo!bar@baz') u.removeHostmask('foo!bar@baz') self.failIf(u.checkHostmask('foo!bar@baz')) def testOwner(self): u = ircdb.IrcUser() u.addCapability('owner') self.failUnless(u._checkCapability('foo')) self.failIf(u._checkCapability('-foo')) def testInitCapabilities(self): u = ircdb.IrcUser(capabilities=['foo']) self.failUnless(u._checkCapability('foo')) def testPassword(self): u = ircdb.IrcUser() u.setPassword('foobar') self.failUnless(u.checkPassword('foobar')) self.failIf(u.checkPassword('somethingelse')) def testTimeoutAuth(self): orig = conf.supybot.databases.users.timeoutIdentification() try: conf.supybot.databases.users.timeoutIdentification.setValue(2) u = ircdb.IrcUser() u.addAuth('foo!bar@baz') self.failUnless(u.checkHostmask('foo!bar@baz')) time.sleep(2.1) self.failIf(u.checkHostmask('foo!bar@baz')) finally: conf.supybot.databases.users.timeoutIdentification.setValue(orig) def testMultipleAuth(self): orig = conf.supybot.databases.users.timeoutIdentification() try: conf.supybot.databases.users.timeoutIdentification.setValue(2) u = ircdb.IrcUser() u.addAuth('foo!bar@baz') self.failUnless(u.checkHostmask('foo!bar@baz')) u.addAuth('foo!bar@baz') self.failUnless(u.checkHostmask('foo!bar@baz')) self.failUnless(len(u.auth) == 1) u.addAuth('boo!far@fizz') self.failUnless(u.checkHostmask('boo!far@fizz')) time.sleep(2.1) self.failIf(u.checkHostmask('foo!bar@baz')) self.failIf(u.checkHostmask('boo!far@fizz')) finally: conf.supybot.databases.users.timeoutIdentification.setValue(orig) def testHashedPassword(self): u = ircdb.IrcUser() u.setPassword('foobar', hashed=True) self.failUnless(u.checkPassword('foobar')) self.failIf(u.checkPassword('somethingelse')) self.assertNotEqual(u.password, 'foobar') def testHostmasks(self): prefix = 'foo12341234!bar@baz.domain.tld' hostmasks = ['*!bar@baz.domain.tld', 'foo12341234!*@*'] u = ircdb.IrcUser() self.failIf(u.checkHostmask(prefix)) for hostmask in hostmasks: u.addHostmask(hostmask) self.failUnless(u.checkHostmask(prefix)) def testAuth(self): prefix = 'foo!bar@baz' u = ircdb.IrcUser() u.addAuth(prefix) self.failUnless(u.auth) u.clearAuth() self.failIf(u.auth) def testIgnore(self): u = ircdb.IrcUser(ignore=True) self.failIf(u._checkCapability('foo')) self.failUnless(u._checkCapability('-foo')) def testRemoveCapability(self): u = ircdb.IrcUser(capabilities=('foo',)) self.assertRaises(KeyError, u.removeCapability, 'bar') class IrcChannelTestCase(IrcdbTestCase): def testInit(self): c = ircdb.IrcChannel() self.failIf(c._checkCapability('op')) self.failIf(c._checkCapability('voice')) self.failIf(c._checkCapability('halfop')) self.failIf(c._checkCapability('protected')) def testCapabilities(self): c = ircdb.IrcChannel(defaultAllow=False) self.failIf(c._checkCapability('foo')) c.addCapability('foo') self.failUnless(c._checkCapability('foo')) c.removeCapability('foo') self.failIf(c._checkCapability('foo')) def testDefaultCapability(self): c = ircdb.IrcChannel() c.setDefaultCapability(False) self.failIf(c._checkCapability('foo')) self.failUnless(c._checkCapability('-foo')) c.setDefaultCapability(True) self.failUnless(c._checkCapability('foo')) self.failIf(c._checkCapability('-foo')) def testLobotomized(self): c = ircdb.IrcChannel(lobotomized=True) self.failUnless(c.checkIgnored('foo!bar@baz')) def testIgnored(self): prefix = 'foo!bar@baz' banmask = ircutils.banmask(prefix) c = ircdb.IrcChannel() self.failIf(c.checkIgnored(prefix)) c.addIgnore(banmask) self.failUnless(c.checkIgnored(prefix)) c.removeIgnore(banmask) self.failIf(c.checkIgnored(prefix)) c.addBan(banmask) self.failUnless(c.checkIgnored(prefix)) c.removeBan(banmask) self.failIf(c.checkIgnored(prefix)) class UsersDictionaryTestCase(IrcdbTestCase): filename = os.path.join(conf.supybot.directories.conf(), 'UsersDictionaryTestCase.conf') def setUp(self): try: os.remove(self.filename) except: pass self.users = ircdb.UsersDictionary() IrcdbTestCase.setUp(self) def testIterAndNumUsers(self): self.assertEqual(self.users.numUsers(), 0) u = self.users.newUser() hostmask = 'foo!xyzzy@baz.domain.com' banmask = ircutils.banmask(hostmask) u.addHostmask(banmask) u.name = 'foo' self.users.setUser(u) self.assertEqual(self.users.numUsers(), 1) u = self.users.newUser() hostmask = 'biff!fladksfj@blakjdsf' banmask = ircutils.banmask(hostmask) u.addHostmask(banmask) u.name = 'biff' self.users.setUser(u) self.assertEqual(self.users.numUsers(), 2) self.users.delUser(2) self.assertEqual(self.users.numUsers(), 1) self.users.delUser(1) self.assertEqual(self.users.numUsers(), 0) def testGetSetDelUser(self): self.assertRaises(KeyError, self.users.getUser, 'foo') self.assertRaises(KeyError, self.users.getUser, 'foo!xyzzy@baz.domain.com') u = self.users.newUser() hostmask = 'foo!xyzzy@baz.domain.com' banmask = ircutils.banmask(hostmask) u.addHostmask(banmask) u.addHostmask(hostmask) u.name = 'foo' self.users.setUser(u) self.assertEqual(self.users.getUser('foo'), u) self.assertEqual(self.users.getUser('FOO'), u) self.assertEqual(self.users.getUser(hostmask), u) self.assertEqual(self.users.getUser(banmask), u) # The UsersDictionary shouldn't allow users to be added whose hostmasks # match another user's already in the database. u2 = self.users.newUser() u2.addHostmask('*!xyzzy@baz.domain.c?m') self.assertRaises(ValueError, self.users.setUser, u2) class CheckCapabilityTestCase(IrcdbTestCase): filename = os.path.join(conf.supybot.directories.conf(), 'CheckCapabilityTestCase.conf') owner = 'owner!owner@owner' nothing = 'nothing!nothing@nothing' justfoo = 'justfoo!justfoo@justfoo' antifoo = 'antifoo!antifoo@antifoo' justchanfoo = 'justchanfoo!justchanfoo@justchanfoo' antichanfoo = 'antichanfoo!antichanfoo@antichanfoo' securefoo = 'securefoo!securefoo@securefoo' channel = '#channel' cap = 'foo' anticap = ircdb.makeAntiCapability(cap) chancap = ircdb.makeChannelCapability(channel, cap) antichancap = ircdb.makeAntiCapability(chancap) chanop = ircdb.makeChannelCapability(channel, 'op') channelnothing = ircdb.IrcChannel() channelcap = ircdb.IrcChannel() channelcap.addCapability(cap) channelanticap = ircdb.IrcChannel() channelanticap.addCapability(anticap) def setUp(self): IrcdbTestCase.setUp(self) try: os.remove(self.filename) except: pass self.users = ircdb.UsersDictionary() #self.users.open(self.filename) self.channels = ircdb.ChannelsDictionary() #self.channels.open(self.filename) owner = self.users.newUser() owner.name = 'owner' owner.addCapability('owner') owner.addHostmask(self.owner) self.users.setUser(owner) nothing = self.users.newUser() nothing.name = 'nothing' nothing.addHostmask(self.nothing) self.users.setUser(nothing) justfoo = self.users.newUser() justfoo.name = 'justfoo' justfoo.addCapability(self.cap) justfoo.addHostmask(self.justfoo) self.users.setUser(justfoo) antifoo = self.users.newUser() antifoo.name = 'antifoo' antifoo.addCapability(self.anticap) antifoo.addHostmask(self.antifoo) self.users.setUser(antifoo) justchanfoo = self.users.newUser() justchanfoo.name = 'justchanfoo' justchanfoo.addCapability(self.chancap) justchanfoo.addHostmask(self.justchanfoo) self.users.setUser(justchanfoo) antichanfoo = self.users.newUser() antichanfoo.name = 'antichanfoo' antichanfoo.addCapability(self.antichancap) antichanfoo.addHostmask(self.antichanfoo) self.users.setUser(antichanfoo) securefoo = self.users.newUser() securefoo.name = 'securefoo' securefoo.addCapability(self.cap) securefoo.secure = True securefoo.addHostmask(self.securefoo) self.users.setUser(securefoo) channel = ircdb.IrcChannel() self.channels.setChannel(self.channel, channel) def checkCapability(self, hostmask, capability): return ircdb.checkCapability(hostmask, capability, self.users, self.channels) def testOwner(self): self.failUnless(self.checkCapability(self.owner, self.cap)) self.failIf(self.checkCapability(self.owner, self.anticap)) self.failUnless(self.checkCapability(self.owner, self.chancap)) self.failIf(self.checkCapability(self.owner, self.antichancap)) self.channels.setChannel(self.channel, self.channelanticap) self.failUnless(self.checkCapability(self.owner, self.cap)) self.failIf(self.checkCapability(self.owner, self.anticap)) def testNothingAgainstChannel(self): self.channels.setChannel(self.channel, self.channelnothing) self.assertEqual(self.checkCapability(self.nothing, self.chancap), self.channelnothing.defaultAllow) self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow self.channels.setChannel(self.channel, self.channelnothing) self.assertEqual(self.checkCapability(self.nothing, self.chancap), self.channelnothing.defaultAllow) self.channels.setChannel(self.channel, self.channelcap) self.failUnless(self.checkCapability(self.nothing, self.chancap)) self.failIf(self.checkCapability(self.nothing, self.antichancap)) self.channels.setChannel(self.channel, self.channelanticap) self.failIf(self.checkCapability(self.nothing, self.chancap)) self.failUnless(self.checkCapability(self.nothing, self.antichancap)) def testNothing(self): self.assertEqual(self.checkCapability(self.nothing, self.cap), conf.supybot.capabilities.default()) self.assertEqual(self.checkCapability(self.nothing, self.anticap), not conf.supybot.capabilities.default()) def testJustFoo(self): self.failUnless(self.checkCapability(self.justfoo, self.cap)) self.failIf(self.checkCapability(self.justfoo, self.anticap)) def testAntiFoo(self): self.failUnless(self.checkCapability(self.antifoo, self.anticap)) self.failIf(self.checkCapability(self.antifoo, self.cap)) def testJustChanFoo(self): self.channels.setChannel(self.channel, self.channelnothing) self.failUnless(self.checkCapability(self.justchanfoo, self.chancap)) self.failIf(self.checkCapability(self.justchanfoo, self.antichancap)) self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow self.failUnless(self.checkCapability(self.justchanfoo, self.chancap)) self.failIf(self.checkCapability(self.justchanfoo, self.antichancap)) self.channels.setChannel(self.channel, self.channelanticap) self.failUnless(self.checkCapability(self.justchanfoo, self.chancap)) self.failIf(self.checkCapability(self.justchanfoo, self.antichancap)) def testChanOpCountsAsEverything(self): self.channels.setChannel(self.channel, self.channelanticap) id = self.users.getUserId('nothing') u = self.users.getUser(id) u.addCapability(self.chanop) self.users.setUser(u) self.failUnless(self.checkCapability(self.nothing, self.chancap)) self.channels.setChannel(self.channel, self.channelnothing) self.failUnless(self.checkCapability(self.nothing, self.chancap)) self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow self.failUnless(self.checkCapability(self.nothing, self.chancap)) def testAntiChanFoo(self): self.channels.setChannel(self.channel, self.channelnothing) self.failIf(self.checkCapability(self.antichanfoo, self.chancap)) self.failUnless(self.checkCapability(self.antichanfoo, self.antichancap)) def testSecurefoo(self): self.failUnless(self.checkCapability(self.securefoo, self.cap)) id = self.users.getUserId(self.securefoo) u = self.users.getUser(id) u.addAuth(self.securefoo) self.users.setUser(u) try: originalConfDefaultAllow = conf.supybot.capabilities.default() conf.supybot.capabilities.default.set('False') self.failIf(self.checkCapability('a' + self.securefoo, self.cap)) finally: conf.supybot.capabilities.default.set(str(originalConfDefaultAllow)) class PersistanceTestCase(IrcdbTestCase): filename = os.path.join(conf.supybot.directories.conf(), 'PersistanceTestCase.conf') def setUp(self): IrcdbTestCase.setUp(self) try: os.remove(self.filename) except OSError: pass super(PersistanceTestCase, self).setUp() def testAddUser(self): db = ircdb.UsersDictionary() db.filename = self.filename u = db.newUser() u.name = 'foouser' u.addCapability('foocapa') u.addHostmask('*!fooident@foohost') db.setUser(u) db.flush() db2 = ircdb.UsersDictionary() db2.open(self.filename) self.assertEqual(list(db2.users), [1]) self.assertEqual(db2.users[1].name, 'foouser') db.reload() self.assertEqual(list(db.users), [1]) self.assertEqual(db.users[1].name, 'foouser') def testAddRemoveUser(self): db = ircdb.UsersDictionary() db.filename = self.filename u = db.newUser() u.name = 'foouser' u.addCapability('foocapa') u.addHostmask('*!fooident@foohost') db.setUser(u) db2 = ircdb.UsersDictionary() db2.open(self.filename) self.assertEqual(list(db2.users), [1]) self.assertEqual(db2.users[1].name, 'foouser') db.delUser(1) self.assertEqual(list(db.users), []) db2 = ircdb.UsersDictionary() db2.open(self.filename) self.assertEqual(list(db.users), []) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_i18n.py0000644000175000017500000000541313233426066016471 0ustar valval00000000000000# -*- coding: utf8 -*- ### # Copyright (c) 2012, 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. ### from supybot.test import * from supybot.commands import wrap from supybot.i18n import PluginInternationalization, internationalizeDocstring import supybot.conf as conf msg_en = 'The operation succeeded.' msg_fr = 'Opération effectuée avec succès.' _ = PluginInternationalization() @internationalizeDocstring def foo(): """The operation succeeded.""" pass @wrap def bar(): """The operation succeeded.""" pass class I18nTestCase(SupyTestCase): def testPluginInternationalization(self): self.assertEqual(_(msg_en), msg_en) with conf.supybot.language.context('fr'): self.assertEqual(_(msg_en), msg_fr) conf.supybot.language.setValue('en') self.assertEqual(_(msg_en), msg_en) multiline = '%s\n\n%s' % (msg_en, msg_en) self.assertEqual(_(multiline), multiline) @retry() def testDocstring(self): self.assertEqual(foo.__doc__, msg_en) self.assertEqual(bar.__doc__, msg_en) with conf.supybot.language.context('fr'): self.assertEqual(foo.__doc__, msg_fr) self.assertEqual(bar.__doc__, msg_fr) self.assertEqual(foo.__doc__, msg_en) self.assertEqual(bar.__doc__, msg_en) limnoria-2018.01.25/test/test_format.py0000644000175000017500000000353113233426066017201 0ustar valval00000000000000### # 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. ### from supybot.test import * class FormatTestCase(SupyTestCase): def test_t_acceptsNone(self): self.failUnless(format('%t', None)) def testFloatingPoint(self): self.assertEqual(format('%.2f', 0.12345), '0.12') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_firewall.py0000644000175000017500000000615513233426066017523 0ustar valval00000000000000### # Copyright (c) 2008, 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 * from supybot import log import supybot.utils.minisix as minisix class FirewallTestCase(SupyTestCase): def setUp(self): log.testing = False def tearDown(self): log.testing = True # Python 3's syntax for metaclasses is incompatible with Python 3 so # using Python 3's syntax directly will raise a SyntaxError on Python 2. exec(""" class C(%s __firewalled__ = {'foo': None} class MyException(Exception): pass def foo(self): raise self.MyException()""" % ('metaclass=log.MetaFirewall):\n' if minisix.PY3 else 'object):\n __metaclass__ = log.MetaFirewall')) def testCFooDoesNotRaise(self): c = self.C() self.assertEqual(c.foo(), None) class D(C): def foo(self): raise self.MyException() def testDFooDoesNotRaise(self): d = self.D() self.assertEqual(d.foo(), None) class E(C): __firewalled__ = {'bar': None} def foo(self): raise self.MyException() def bar(self): raise self.MyException() def testEFooDoesNotRaise(self): e = self.E() self.assertEqual(e.foo(), None) def testEBarDoesNotRaise(self): e = self.E() self.assertEqual(e.bar(), None) class F(C): __firewalled__ = {'bar': lambda self: 2} def bar(self): raise self.MyException() def testFBarReturns2(self): f = self.F() self.assertEqual(f.bar(), 2) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_dynamicScope.py0000644000175000017500000000455613233426066020337 0ustar valval00000000000000### # 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. ### from supybot.test import * class TestDynamic(SupyTestCase): def test(self): def f(x): i = 2 return g(x) def g(y): j = 3 return h(y) def h(z): self.assertEqual(dynamic.z, z) self.assertEqual(dynamic.j, 3) self.assertEqual(dynamic.i, 2) self.assertEqual(dynamic.y, z) self.assertEqual(dynamic.x, z) #self.assertRaises(NameError, getattr, dynamic, 'asdfqwerqewr') self.assertEqual(dynamic.self, self) return z self.assertEqual(f(10), 10) def testCommonUsage(self): foo = 'bar' def f(): foo = dynamic.foo self.assertEqual(foo, 'bar') f() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_commands.py0000644000175000017500000002134113233426066017511 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2015, James McCoy # 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 getopt from supybot.test import * from supybot.commands import * import supybot.conf as conf import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs import supybot.utils.minisix as minisix import supybot.callbacks as callbacks class CommandsTestCase(SupyTestCase): def assertState(self, spec, given, expected, target='test', **kwargs): msg = ircmsgs.privmsg(target, 'foo') realIrc = getTestIrc() realIrc.nick = 'test' realIrc.state.supported['chantypes'] = '#' irc = callbacks.SimpleProxy(realIrc, msg) myspec = Spec(spec, **kwargs) state = myspec(irc, msg, given) self.assertEqual(state.args, expected, 'Expected %r, got %r' % (expected, state.args)) def assertError(self, spec, given): self.assertRaises(callbacks.Error, self.assertState, spec, given, given) def assertStateErrored(self, spec, given, target='test', errored=True, **kwargs): msg = ircmsgs.privmsg(target, 'foo') realIrc = getTestIrc() realIrc.nick = 'test' realIrc.state.supported['chantypes'] = '#' irc = callbacks.SimpleProxy(realIrc, msg) myspec = Spec(spec, **kwargs) state = myspec(irc, msg, given) self.assertEqual(state.errored, errored, 'Expected %r, got %r' % (errored, state.errored)) class GeneralContextTestCase(CommandsTestCase): def testEmptySpec(self): self.assertState([], [], []) def testSpecInt(self): self.assertState(['int'], ['1'], [1]) self.assertState(['int', 'int', 'int'], ['1', '2', '3'], [1, 2, 3]) self.assertError(['int'], ['9e999']) def testSpecNick(self): strict = conf.supybot.protocols.irc.strictRfc() try: conf.supybot.protocols.irc.strictRfc.setValue(True) self.assertError(['nick'], ['1abc']) conf.supybot.protocols.irc.strictRfc.setValue(False) self.assertState(['nick'], ['1abc'], ['1abc']) finally: conf.supybot.protocols.irc.strictRfc.setValue(strict) if minisix.PY2: def testSpecLong(self): self.assertState(['long'], ['1'], [long(1)]) self.assertState(['long', 'long', 'long'], ['1', '2', '3'], [long(1), long(2), long(3)]) def testRestHandling(self): self.assertState([rest(None)], ['foo', 'bar', 'baz'], ['foo bar baz']) def testRestRequiresArgs(self): self.assertError([rest('something')], []) def testOptional(self): spec = [optional('int', 999), None] self.assertState(spec, ['12', 'foo'], [12, 'foo']) self.assertState(spec, ['foo'], [999, 'foo']) def testAdditional(self): spec = [additional('int', 999)] self.assertState(spec, ['12'], [12]) self.assertState(spec, [], [999]) self.assertError(spec, ['foo']) def testReverse(self): spec = [reverse('positiveInt'), 'float', 'text'] self.assertState(spec, ['-1.0', 'foo', '1'], [1, -1.0, 'foo']) def testGetopts(self): spec = ['int', getopts({'foo': None, 'bar': 'int'}), 'int'] self.assertState(spec, ['12', '--foo', 'baz', '--bar', '13', '15'], [12, [('foo', 'baz'), ('bar', 13)], 15]) def testGetoptsShort(self): spec = ['int', getopts({'foo': None, 'bar': 'int'}), 'int'] self.assertState(spec, ['12', '--f', 'baz', '--ba', '13', '15'], [12, [('foo', 'baz'), ('bar', 13)], 15]) def testGetoptsConflict(self): spec = ['int', getopts({'foo': None, 'fbar': 'int'}), 'int'] self.assertRaises(getopt.GetoptError, self.assertStateErrored, spec, ['12', '--f', 'baz', '--ba', '13', '15']) def testAny(self): self.assertState([any('int')], ['1', '2', '3'], [[1, 2, 3]]) self.assertState([None, any('int')], ['1', '2', '3'], ['1', [2, 3]]) self.assertState([any('int')], [], [[]]) self.assertState([any('int', continueOnError=True), 'text'], ['1', '2', 'test'], [[1, 2], 'test']) def testMany(self): spec = [many('int')] self.assertState(spec, ['1', '2', '3'], [[1, 2, 3]]) self.assertError(spec, []) def testChannelRespectsNetwork(self): spec = ['channel', 'text'] self.assertState(spec, ['#foo', '+s'], ['#foo', '+s']) self.assertState(spec, ['+s'], ['#foo', '+s'], target='#foo') def testGlob(self): spec = ['glob'] self.assertState(spec, ['foo'], ['*foo*']) self.assertState(spec, ['?foo'], ['?foo']) self.assertState(spec, ['foo*'], ['foo*']) def testGetId(self): spec = ['id'] self.assertState(spec, ['#12'], [12]) def testCommaList(self): spec = [commalist('int')] self.assertState(spec, ['12'], [[12]]) self.assertState(spec, ['12,', '10'], [[12, 10]]) self.assertState(spec, ['12,11,10,', '9'], [[12, 11, 10, 9]]) spec.append('int') self.assertState(spec, ['12,11,10', '9'], [[12, 11, 10], 9]) def testLiteral(self): spec = [('literal', ['foo', 'bar', 'baz'])] self.assertState(spec, ['foo'], ['foo']) self.assertState(spec, ['fo'], ['foo']) self.assertState(spec, ['f'], ['foo']) self.assertState(spec, ['bar'], ['bar']) self.assertState(spec, ['baz'], ['baz']) self.assertError(spec, ['ba']) class ConverterTestCase(CommandsTestCase): def testUrlAllowsHttps(self): url = 'https://foo.bar/baz' self.assertState(['url'], [url], [url]) self.assertState(['httpUrl'], [url], [url]) def testEmail(self): email = 'jemfinch@supybot.com' self.assertState(['email'], [email], [email]) self.assertError(['email'], ['foo']) self.assertError(['email'], ['foo@']) self.assertError(['email'], ['@foo']) class FirstTestCase(CommandsTestCase): def testRepr(self): self.failUnless(repr(first('int'))) def testFirstConverterFailsAndNotErroredState(self): self.assertStateErrored([first('int', 'something')], ['words'], errored=False) def testLongRegexp(self): spec = [first('regexpMatcher', 'regexpReplacer'), 'text'] self.assertStateErrored(spec, ['s/foo/bar/', 'x' * 512], errored=False) class GetoptTestCase(PluginTestCase): plugins = ('Misc',) # We put something so it does not complain class Foo(callbacks.Plugin): def bar(self, irc, msg, args, optlist): irc.reply(' '.join(sorted(['%s:%d'%x for x in optlist]))) bar = wrap(bar, [getopts({'foo': 'int', 'fbar': 'int'})], checkDoc=False) def testGetoptsExact(self): self.irc.addCallback(self.Foo(self.irc)) self.assertResponse('bar --foo 3 --fbar 4', 'fbar:4 foo:3') self.assertResponse('bar --fo 3 --fb 4', 'fbar:4 foo:3') self.assertResponse('bar --f 3 --fb 5', 'Error: Invalid arguments for bar.') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test_callbacks.py0000644000175000017500000007220613233426066017635 0ustar valval00000000000000# -*- coding: utf8 -*- ### # 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 * import supybot.conf as conf import supybot.utils as utils import supybot.ircmsgs as ircmsgs import supybot.utils.minisix as minisix import supybot.callbacks as callbacks tokenize = callbacks.tokenize class TokenizerTestCase(SupyTestCase): def testEmpty(self): self.assertEqual(tokenize(''), []) def testNullCharacter(self): self.assertEqual(tokenize(utils.str.dqrepr('\0')), ['\0']) def testSingleDQInDQString(self): self.assertEqual(tokenize('"\\""'), ['"']) def testDQsWithBackslash(self): self.assertEqual(tokenize('"\\\\"'), ["\\"]) def testDoubleQuotes(self): self.assertEqual(tokenize('"\\"foo\\""'), ['"foo"']) def testSingleWord(self): self.assertEqual(tokenize('foo'), ['foo']) def testMultipleSimpleWords(self): words = 'one two three four five six seven eight'.split() for i in range(len(words)): self.assertEqual(tokenize(' '.join(words[:i])), words[:i]) def testSingleQuotesNotQuotes(self): self.assertEqual(tokenize("it's"), ["it's"]) def testQuotedWords(self): self.assertEqual(tokenize('"foo bar"'), ['foo bar']) self.assertEqual(tokenize('""'), ['']) self.assertEqual(tokenize('foo "" bar'), ['foo', '', 'bar']) self.assertEqual(tokenize('foo "bar baz" quux'), ['foo', 'bar baz', 'quux']) _testUnicode = """ def testUnicode(self): self.assertEqual(tokenize(u'好'), [u'好']) self.assertEqual(tokenize(u'"好"'), [u'好'])""" if minisix.PY3: _testUnicode = _testUnicode.replace("u'", "'") exec(_testUnicode) def testNesting(self): self.assertEqual(tokenize('[]'), [[]]) self.assertEqual(tokenize('[foo]'), [['foo']]) self.assertEqual(tokenize('[ foo ]'), [['foo']]) self.assertEqual(tokenize('foo [bar]'), ['foo', ['bar']]) self.assertEqual(tokenize('foo bar [baz quux]'), ['foo', 'bar', ['baz', 'quux']]) try: orig = conf.supybot.commands.nested() conf.supybot.commands.nested.setValue(False) self.assertEqual(tokenize('[]'), ['[]']) self.assertEqual(tokenize('[foo]'), ['[foo]']) self.assertEqual(tokenize('foo [bar]'), ['foo', '[bar]']) self.assertEqual(tokenize('foo bar [baz quux]'), ['foo', 'bar', '[baz', 'quux]']) finally: conf.supybot.commands.nested.setValue(orig) def testError(self): self.assertRaises(SyntaxError, tokenize, '[foo') #] self.assertRaises(SyntaxError, tokenize, '"foo') #" def testPipe(self): try: conf.supybot.commands.nested.pipeSyntax.setValue(True) self.assertRaises(SyntaxError, tokenize, '| foo') self.assertRaises(SyntaxError, tokenize, 'foo ||bar') self.assertRaises(SyntaxError, tokenize, 'bar |') self.assertEqual(tokenize('foo|bar'), ['bar', ['foo']]) self.assertEqual(tokenize('foo | bar'), ['bar', ['foo']]) self.assertEqual(tokenize('foo | bar | baz'), ['baz', ['bar',['foo']]]) self.assertEqual(tokenize('foo bar | baz'), ['baz', ['foo', 'bar']]) self.assertEqual(tokenize('foo | bar baz'), ['bar', 'baz', ['foo']]) self.assertEqual(tokenize('foo bar | baz quux'), ['baz', 'quux', ['foo', 'bar']]) finally: conf.supybot.commands.nested.pipeSyntax.setValue(False) self.assertEqual(tokenize('foo|bar'), ['foo|bar']) self.assertEqual(tokenize('foo | bar'), ['foo', '|', 'bar']) self.assertEqual(tokenize('foo | bar | baz'), ['foo', '|', 'bar', '|', 'baz']) self.assertEqual(tokenize('foo bar | baz'), ['foo', 'bar', '|', 'baz']) def testQuoteConfiguration(self): f = callbacks.tokenize self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('"[foo]"'), ['[foo]']) try: original = conf.supybot.commands.quotes() conf.supybot.commands.quotes.setValue('`') self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('`[foo]`'), ['[foo]']) conf.supybot.commands.quotes.setValue('\'') self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('\'[foo]\''), ['[foo]']) conf.supybot.commands.quotes.setValue('`\'') self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('`[foo]`'), ['[foo]']) self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('\'[foo]\''), ['[foo]']) finally: conf.supybot.commands.quotes.setValue(original) def testBold(self): s = '\x02foo\x02' self.assertEqual(tokenize(s), [s]) s = s[:-1] + '\x0f' self.assertEqual(tokenize(s), [s]) def testColor(self): s = '\x032,3foo\x03' self.assertEqual(tokenize(s), [s]) s = s[:-1] + '\x0f' self.assertEqual(tokenize(s), [s]) class FunctionsTestCase(SupyTestCase): def testCanonicalName(self): self.assertEqual('foo', callbacks.canonicalName('foo')) self.assertEqual('foobar', callbacks.canonicalName('foo-bar')) self.assertEqual('foobar', callbacks.canonicalName('foo_bar')) self.assertEqual('foobar', callbacks.canonicalName('FOO-bar')) self.assertEqual('foobar', callbacks.canonicalName('FOOBAR')) self.assertEqual('foobar', callbacks.canonicalName('foo___bar')) self.assertEqual('foobar', callbacks.canonicalName('_f_o_o-b_a_r')) # The following seems to be a hack for the Karma plugin; I'm not # entirely sure that it's completely necessary anymore. self.assertEqual('foobar--', callbacks.canonicalName('foobar--')) def testAddressed(self): oldprefixchars = str(conf.supybot.reply.whenAddressedBy.chars) nick = 'supybot' conf.supybot.reply.whenAddressedBy.chars.set('~!@') inChannel = ['~foo', '@foo', '!foo', '%s: foo' % nick, '%s foo' % nick, '%s: foo' % nick.capitalize(), '%s: foo' % nick.upper()] inChannel = [ircmsgs.privmsg('#foo', s) for s in inChannel] badmsg = ircmsgs.privmsg('#foo', '%s:foo' % nick) self.failIf(callbacks.addressed(nick, badmsg)) badmsg = ircmsgs.privmsg('#foo', '%s^: foo' % nick) self.failIf(callbacks.addressed(nick, badmsg)) for msg in inChannel: self.assertEqual('foo', callbacks.addressed(nick, msg), msg) msg = ircmsgs.privmsg(nick, 'foo') self.assertEqual('foo', callbacks.addressed(nick, msg)) conf.supybot.reply.whenAddressedBy.chars.set(oldprefixchars) msg = ircmsgs.privmsg('#foo', '%s::::: bar' % nick) self.assertEqual('bar', callbacks.addressed(nick, msg)) msg = ircmsgs.privmsg('#foo', '%s: foo' % nick.upper()) self.assertEqual('foo', callbacks.addressed(nick, msg)) badmsg = ircmsgs.privmsg('#foo', '%s`: foo' % nick) self.failIf(callbacks.addressed(nick, badmsg)) def testAddressedReplyWhenNotAddressed(self): msg1 = ircmsgs.privmsg('#foo', '@bar') msg2 = ircmsgs.privmsg('#foo', 'bar') self.assertEqual(callbacks.addressed('blah', msg1), 'bar') self.assertEqual(callbacks.addressed('blah', msg2), '') try: original = conf.supybot.reply.whenNotAddressed() conf.supybot.reply.whenNotAddressed.setValue(True) # need to recreate the msg objects since the old ones have already # been tagged msg1 = ircmsgs.privmsg('#foo', '@bar') msg2 = ircmsgs.privmsg('#foo', 'bar') self.assertEqual(callbacks.addressed('blah', msg1), 'bar') self.assertEqual(callbacks.addressed('blah', msg2), 'bar') finally: conf.supybot.reply.whenNotAddressed.setValue(original) def testAddressedWithMultipleNicks(self): msg = ircmsgs.privmsg('#foo', 'bar: baz') self.assertEqual(callbacks.addressed('bar', msg), 'baz') # need to recreate the msg objects since the old ones have already # been tagged msg = ircmsgs.privmsg('#foo', 'bar: baz') self.assertEqual(callbacks.addressed('biff', msg, nicks=['bar']), 'baz') def testAddressedWithNickAtEnd(self): msg = ircmsgs.privmsg('#foo', 'baz, bar') self.assertEqual(callbacks.addressed('bar', msg, whenAddressedByNickAtEnd=True), 'baz') def testAddressedPrefixCharsTakePrecedenceOverNickAtEnd(self): msg = ircmsgs.privmsg('#foo', '@echo foo') self.assertEqual(callbacks.addressed('foo', msg, whenAddressedByNickAtEnd=True, prefixChars='@'), 'echo foo') def testReply(self): prefix = 'foo!bar@baz' channelMsg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) nonChannelMsg = ircmsgs.privmsg('supybot', 'bar baz', prefix=prefix) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks.reply(channelMsg, 'foo', private=True)) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks.reply(nonChannelMsg, 'foo')) self.assertEqual(ircmsgs.privmsg(channelMsg.args[0], '%s: foo' % channelMsg.nick), callbacks.reply(channelMsg, 'foo')) self.assertEqual(ircmsgs.privmsg(channelMsg.args[0], 'foo'), callbacks.reply(channelMsg, 'foo', prefixNick=False)) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks.reply(channelMsg, 'foo', notice=True, private=True)) def testReplyTo(self): prefix = 'foo!bar@baz' msg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) self.assertEqual(callbacks.reply(msg, 'blah', to='blah'), ircmsgs.privmsg('#foo', 'blah: blah')) self.assertEqual(callbacks.reply(msg, 'blah', to='blah', private=True), ircmsgs.notice('blah', 'blah')) def testTokenize(self): self.assertEqual(callbacks.tokenize(''), []) self.assertEqual(callbacks.tokenize('foo'), ['foo']) self.assertEqual(callbacks.tokenize('foo'), ['foo']) self.assertEqual(callbacks.tokenize('bar [baz]'), ['bar', ['baz']]) class AmbiguityTestCase(PluginTestCase): plugins = ('Misc',) # Something so it doesn't complain. class Foo(callbacks.Plugin): def bar(self, irc, msg, args): irc.reply('foo.bar') class Bar(callbacks.Plugin): def bar(self, irc, msg, args): irc.reply('bar.bar') def testAmbiguityWithCommandSameNameAsPlugin(self): self.irc.addCallback(self.Foo(self.irc)) self.assertResponse('bar', 'foo.bar') self.irc.addCallback(self.Bar(self.irc)) self.assertResponse('bar', 'bar.bar') class ProperStringificationOfReplyArgs(PluginTestCase): plugins = ('Misc',) # Same as above. class NonString(callbacks.Plugin): def int(self, irc, msg, args): irc.reply(1) class ExpectsString(callbacks.Plugin): def lower(self, irc, msg, args): irc.reply(args[0].lower()) def test(self): self.irc.addCallback(self.NonString(self.irc)) self.irc.addCallback(self.ExpectsString(self.irc)) self.assertResponse('expectsstring lower [nonstring int]', '1') ## class PrivmsgTestCaseWithKarma(ChannelPluginTestCase): ## plugins = ('Utilities', 'Misc', 'Web', 'Karma', 'String') ## conf.allowEval = True ## timeout = 2 ## def testSecondInvalidCommandRespondsWithThreadedInvalidCommands(self): ## try: ## orig = conf.supybot.plugins.Karma.response() ## conf.supybot.plugins.Karma.response.setValue(True) ## self.assertNotRegexp('echo [foo++] [foo++]', 'not a valid') ## _ = self.irc.takeMsg() ## finally: ## conf.supybot.plugins.Karma.response.setValue(orig) class PrivmsgTestCase(ChannelPluginTestCase): plugins = ('Utilities', 'Misc', 'Web', 'String') conf.allowEval = True timeout = 2 def testEmptySquareBrackets(self): self.assertError('echo []') ## def testHelpNoNameError(self): ## # This will raise a NameError if some dynamic scoping isn't working ## self.assertNotError('load Http') ## self.assertHelp('extension') def testMaximumNestingDepth(self): original = conf.supybot.commands.nested.maximum() try: conf.supybot.commands.nested.maximum.setValue(3) self.assertResponse('echo foo', 'foo') self.assertResponse('echo [echo foo]', 'foo') self.assertResponse('echo [echo [echo foo]]', 'foo') self.assertResponse('echo [echo [echo [echo foo]]]', 'foo') self.assertError('echo [echo [echo [echo [echo foo]]]]') finally: conf.supybot.commands.nested.maximum.setValue(original) def testSimpleReply(self): self.assertResponse("eval irc.reply('foo')", 'foo') def testSimpleReplyAction(self): self.assertResponse("eval irc.reply('foo', action=True)", '\x01ACTION foo\x01') def testReplyWithNickPrefix(self): self.feedMsg('@len foo') m = self.irc.takeMsg() self.failUnless(m is not None, 'm: %r' % m) self.failUnless(m.args[1].startswith(self.nick)) try: original = conf.supybot.reply.withNickPrefix() conf.supybot.reply.withNickPrefix.setValue(False) self.feedMsg('@len foobar') m = self.irc.takeMsg() self.failUnless(m is not None) self.failIf(m.args[1].startswith(self.nick)) finally: conf.supybot.reply.withNickPrefix.setValue(original) def testErrorPrivateKwarg(self): try: original = conf.supybot.reply.error.inPrivate() conf.supybot.reply.error.inPrivate.setValue(False) m = self.getMsg("eval irc.error('foo', private=True)") self.failUnless(m, 'No message returned.') self.failIf(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.error.inPrivate.setValue(original) def testErrorNoArgumentIsArgumentError(self): self.assertHelp('eval irc.error()') def testErrorWithNotice(self): try: original = conf.supybot.reply.error.withNotice() conf.supybot.reply.error.withNotice.setValue(True) m = self.getMsg("eval irc.error('foo')") self.failUnless(m, 'No message returned.') self.failUnless(m.command == 'NOTICE') finally: conf.supybot.reply.error.withNotice.setValue(original) def testErrorReplyPrivate(self): try: original = str(conf.supybot.reply.error.inPrivate) conf.supybot.reply.error.inPrivate.set('False') # If this doesn't raise an error, we've got a problem, so the next # two assertions shouldn't run. So we first check that what we # expect to error actually does so we don't go on a wild goose # chase because our command never errored in the first place :) s = 're s/foo/bar baz' # will error; should be "re s/foo/bar/ baz" self.assertError(s) m = self.getMsg(s) self.failUnless(ircutils.isChannel(m.args[0])) conf.supybot.reply.error.inPrivate.set('True') m = self.getMsg(s) self.failIf(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.error.inPrivate.set(original) # Now for stuff not based on the plugins. class First(callbacks.Plugin): def firstcmd(self, irc, msg, args): """First""" irc.reply('foo') class Second(callbacks.Plugin): def secondcmd(self, irc, msg, args): """Second""" irc.reply('bar') class FirstRepeat(callbacks.Plugin): def firstcmd(self, irc, msg, args): """FirstRepeat""" irc.reply('baz') class Third(callbacks.Plugin): def third(self, irc, msg, args): """Third""" irc.reply(' '.join(args)) def tearDown(self): if hasattr(self.First, 'first'): del self.First.first if hasattr(self.Second, 'second'): del self.Second.second if hasattr(self.FirstRepeat, 'firstrepeat'): del self.FirstRepeat.firstrepeat ChannelPluginTestCase.tearDown(self) def testDispatching(self): self.irc.addCallback(self.First(self.irc)) self.irc.addCallback(self.Second(self.irc)) self.assertResponse('firstcmd', 'foo') self.assertResponse('secondcmd', 'bar') self.assertResponse('first firstcmd', 'foo') self.assertResponse('second secondcmd', 'bar') self.assertRegexp('first first firstcmd', 'there is no command named "first" in it') def testAmbiguousError(self): self.irc.addCallback(self.First(self.irc)) self.assertNotError('firstcmd') self.irc.addCallback(self.FirstRepeat(self.irc)) self.assertError('firstcmd') self.assertError('firstcmd [firstcmd]') self.assertNotRegexp('firstcmd', '(foo.*baz|baz.*foo)') self.assertResponse('first firstcmd', 'foo') self.assertResponse('firstrepeat firstcmd', 'baz') def testAmbiguousHelpError(self): self.irc.addCallback(self.First(self.irc)) self.irc.addCallback(self.FirstRepeat(self.irc)) self.assertError('help first') def testHelpDispatching(self): self.irc.addCallback(self.First(self.irc)) self.assertHelp('help firstcmd') self.assertHelp('help first firstcmd') self.irc.addCallback(self.FirstRepeat(self.irc)) self.assertError('help firstcmd') self.assertRegexp('help first firstcmd', 'First', 0) # no re.I flag. self.assertRegexp('help firstrepeat firstcmd', 'FirstRepeat', 0) class TwoRepliesFirstAction(callbacks.Plugin): def testactionreply(self, irc, msg, args): irc.reply('foo', action=True) irc.reply('bar') # We're going to check that this isn't an action. def testNotActionSecondReply(self): self.irc.addCallback(self.TwoRepliesFirstAction(self.irc)) self.assertAction('testactionreply', 'foo') m = self.getMsg(' ') self.failIf(m.args[1].startswith('\x01ACTION')) def testEmptyNest(self): try: conf.supybot.reply.whenNotCommand.set('True') self.assertError('echo []') conf.supybot.reply.whenNotCommand.set('False') self.assertResponse('echo []', '[]') finally: conf.supybot.reply.whenNotCommand.set('False') def testDispatcherHelp(self): self.assertNotRegexp('help first', r'\(dispatcher') self.assertNotRegexp('help first', r'%s') def testDefaultCommand(self): self.irc.addCallback(self.First(self.irc)) self.irc.addCallback(self.Third(self.irc)) self.assertError('first blah') self.assertResponse('third foo bar baz', 'foo bar baz') def testSyntaxErrorNotEscaping(self): self.assertError('load [foo') self.assertError('load foo]') def testNoEscapingAttributeErrorFromTokenizeWithFirstElementList(self): self.assertError('[plugin list] list') class InvalidCommand(callbacks.Plugin): def invalidCommand(self, irc, msg, tokens): irc.reply('foo') def testInvalidCommandOneReplyOnly(self): try: original = str(conf.supybot.reply.whenNotCommand) conf.supybot.reply.whenNotCommand.set('True') self.assertRegexp('asdfjkl', 'not a valid command') self.irc.addCallback(self.InvalidCommand(self.irc)) self.assertResponse('asdfjkl', 'foo') self.assertNoResponse(' ', 2) finally: conf.supybot.reply.whenNotCommand.set(original) class BadInvalidCommand(callbacks.Plugin): def invalidCommand(self, irc, msg, tokens): s = 'This shouldn\'t keep Misc.invalidCommand from being called' raise Exception(s) def testBadInvalidCommandDoesNotKillAll(self): try: original = str(conf.supybot.reply.whenNotCommand) conf.supybot.reply.whenNotCommand.set('True') self.irc.addCallback(self.BadInvalidCommand(self.irc)) self.assertRegexp('asdfjkl', 'not a valid command', expectException=True) finally: conf.supybot.reply.whenNotCommand.set(original) class PluginRegexpTestCase(PluginTestCase): plugins = () class PCAR(callbacks.PluginRegexp): def test(self, irc, msg, args): "" raise callbacks.ArgumentError def testNoEscapingArgumentError(self): self.irc.addCallback(self.PCAR(self.irc)) self.assertResponse('test', 'test ') class RichReplyMethodsTestCase(PluginTestCase): plugins = ('Config',) class NoCapability(callbacks.Plugin): def error(self, irc, msg, args): irc.errorNoCapability('admin') def testErrorNoCapability(self): self.irc.addCallback(self.NoCapability(self.irc)) self.assertRegexp('error', 'You don\'t have the admin capability') self.assertNotError('config capabilities.private admin') self.assertRegexp('error', 'Error: You\'re missing some capability') self.assertNotError('config capabilities.private ""') class SourceNestedPluginTestCase(PluginTestCase): plugins = ('Utilities',) class E(callbacks.Plugin): def f(self, irc, msg, args): """takes no arguments F """ irc.reply('f') def empty(self, irc, msg, args): pass class g(callbacks.Commands): def h(self, irc, msg, args): """takes no arguments H """ irc.reply('h') class i(callbacks.Commands): def j(self, irc, msg, args): """takes no arguments J """ irc.reply('j') class same(callbacks.Commands): def same(self, irc, msg, args): """takes no arguments same """ irc.reply('same') def test(self): cb = self.E(self.irc) self.irc.addCallback(cb) self.assertEqual(cb.getCommand(['f']), ['f']) self.assertEqual(cb.getCommand(['same']), ['same']) self.assertEqual(cb.getCommand(['e', 'f']), ['e', 'f']) self.assertEqual(cb.getCommand(['e', 'g', 'h']), ['e', 'g', 'h']) self.assertEqual(cb.getCommand(['e', 'g', 'i', 'j']), ['e', 'g', 'i', 'j']) self.assertResponse('e f', 'f') self.assertResponse('e same', 'same') self.assertResponse('e g h', 'h') self.assertResponse('e g i j', 'j') self.assertHelp('help f') self.assertHelp('help empty') self.assertHelp('help same') self.assertHelp('help e g h') self.assertHelp('help e g i j') self.assertRegexp('list e', 'f, g h, g i j, and same') def testCommandSameNameAsNestedPlugin(self): cb = self.E(self.irc) self.irc.addCallback(cb) self.assertResponse('e f', 'f') # Just to make sure it was loaded. self.assertEqual(cb.getCommand(['e', 'same']), ['e', 'same']) self.assertResponse('e same', 'same') class WithPrivateNoticeTestCase(ChannelPluginTestCase): plugins = ('Utilities',) class WithPrivateNotice(callbacks.Plugin): def normal(self, irc, msg, args): irc.reply('should be with private notice') def explicit(self, irc, msg, args): irc.reply('should not be with private notice', private=False, notice=False) def implicit(self, irc, msg, args): irc.reply('should be with notice due to private', private=True) def test(self): self.irc.addCallback(self.WithPrivateNotice(self.irc)) # Check normal behavior. m = self.assertNotError('normal') self.failIf(m.command == 'NOTICE') self.failUnless(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') self.failIf(m.command == 'NOTICE') self.failUnless(ircutils.isChannel(m.args[0])) # Check abnormal behavior. originalInPrivate = conf.supybot.reply.inPrivate() originalWithNotice = conf.supybot.reply.withNotice() try: conf.supybot.reply.inPrivate.setValue(True) conf.supybot.reply.withNotice.setValue(True) m = self.assertNotError('normal') self.failUnless(m.command == 'NOTICE') self.failIf(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') self.failIf(m.command == 'NOTICE') self.failUnless(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.inPrivate.setValue(originalInPrivate) conf.supybot.reply.withNotice.setValue(originalWithNotice) orig = conf.supybot.reply.withNoticeWhenPrivate() try: conf.supybot.reply.withNoticeWhenPrivate.setValue(True) m = self.assertNotError('implicit') self.failUnless(m.command == 'NOTICE') self.failIf(ircutils.isChannel(m.args[0])) m = self.assertNotError('normal') self.failIf(m.command == 'NOTICE') self.failUnless(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(orig) def testWithNoticeWhenPrivateNotChannel(self): original = conf.supybot.reply.withNoticeWhenPrivate() try: conf.supybot.reply.withNoticeWhenPrivate.setValue(True) m = self.assertNotError("eval irc.reply('y',to='x',private=True)") self.failUnless(m.command == 'NOTICE') m = self.getMsg(' ') m = self.assertNotError("eval irc.reply('y',to='#x',private=True)") self.failIf(m.command == 'NOTICE') finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(original) class ProxyTestCase(SupyTestCase): def testHashing(self): msg = ircmsgs.ping('0') irc = irclib.Irc('test') proxy = callbacks.SimpleProxy(irc, msg) # First one way... self.failIf(proxy != irc) self.failUnless(proxy == irc) self.assertEqual(hash(proxy), hash(irc)) # Then the other! self.failIf(irc != proxy) self.failUnless(irc == proxy) self.assertEqual(hash(irc), hash(proxy)) # And now dictionaries... d = {} d[irc] = 'foo' self.failUnless(len(d) == 1) self.failUnless(d[irc] == 'foo') self.failUnless(d[proxy] == 'foo') d[proxy] = 'bar' self.failUnless(len(d) == 1) self.failUnless(d[irc] == 'bar') self.failUnless(d[proxy] == 'bar') d[irc] = 'foo' self.failUnless(len(d) == 1) self.failUnless(d[irc] == 'foo') self.failUnless(d[proxy] == 'foo') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/test/test.py0000644000175000017500000000425513233426066015635 0ustar valval00000000000000### # 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 sys import os.path import unittest import supybot.test as test load = unittest.defaultTestLoader.loadTestsFromModule GLOBALS = globals() dirname = os.path.dirname(__file__) sys.path.append(dirname) filenames = os.listdir(dirname) # Uncomment these if you need some consistency in the order these tests run. # filenames.sort() # filenames.reverse() for filename in filenames: if filename.startswith('test_') and filename.endswith('.py'): name = filename[:-3] plugin = __import__(name) test.suites.append(load(plugin)) module = None # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/0000755000175000017500000000000013233426077014110 5ustar valval00000000000000limnoria-2018.01.25/src/world.py0000644000175000017500000002011013233426066015601 0ustar valval00000000000000### # 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. ### """ Module for general worldly stuff, like global variables and whatnot. """ import gc import os import sys import time import atexit import select import threading import multiprocessing import re from . import conf, drivers, ircutils, log, registry from .utils import minisix startedAt = time.time() # Just in case it doesn't get set later. starting = False mainThread = threading.currentThread() def isMainThread(): return mainThread is threading.currentThread() threadsSpawned = 1 # Starts at one for the initial "thread." class SupyThread(threading.Thread, object): def __init__(self, *args, **kwargs): global threadsSpawned threadsSpawned += 1 super(SupyThread, self).__init__(*args, **kwargs) log.debug('Spawning thread %q.', self.getName()) processesSpawned = 1 # Starts at one for the initial process. class SupyProcess(multiprocessing.Process): def __init__(self, *args, **kwargs): global processesSpawned processesSpawned += 1 super(SupyProcess, self).__init__(*args, **kwargs) log.debug('Spawning process %q.', self.name) if sys.version_info[0:3] == (3, 3, 1) and hasattr(select, 'poll'): # http://bugs.python.org/issue17707 import multiprocessing.connection def _poll(fds, timeout): if timeout is not None: timeout = int(timeout * 1000) # timeout is in milliseconds fd_map = {} pollster = select.poll() for fd in fds: pollster.register(fd, select.POLLIN) if hasattr(fd, 'fileno'): fd_map[fd.fileno()] = fd else: fd_map[fd] = fd ls = [] for fd, event in pollster.poll(timeout): if event & select.POLLNVAL: raise ValueError('invalid file descriptor %i' % fd) ls.append(fd_map[fd]) return ls multiprocessing.connection._poll = _poll commandsProcessed = 0 ircs = [] # A list of all the IRCs. def getIrc(network): network = network.lower() for irc in ircs: if irc.network.lower() == network: return irc return None def _flushUserData(): userdataFilename = os.path.join(conf.supybot.directories.conf(), 'userdata.conf') registry.close(conf.users, userdataFilename) flushers = [_flushUserData] # A periodic function will flush all these. registryFilename = None def flush(): """Flushes all the registered flushers.""" for (i, f) in enumerate(flushers): try: f() except Exception: log.exception('Uncaught exception in flusher #%s (%s):', i, f) def debugFlush(s=''): if conf.supybot.debug.flushVeryOften(): if s: log.debug(s) flush() def upkeep(): """Does upkeep (like flushing, garbage collection, etc.)""" # Just in case, let's clear the exception info. try: sys.exc_clear() except AttributeError: # Python 3 does not have sys.exc_clear. The except statement clears # the info itself (and we've just entered an except statement) pass if os.name == 'nt': try: import msvcrt msvcrt.heapmin() except ImportError: pass except IOError: # Win98 pass if conf.daemonized: # If we're daemonized, sys.stdout has been replaced with a StringIO # object, so let's see if anything's been printed, and if so, let's # log.warning it (things shouldn't be printed, and we're more likely # to get bug reports if we make it a warning). if not hasattr(sys.stdout, 'getvalue'): # Stupid twisted sometimes replaces our stdout with theirs, because # "The Twisted Way Is The Right Way" (ha!). So we're stuck simply # returning. log.warning('Expected cStringIO as stdout, got %r.', sys.stdout) return s = sys.stdout.getvalue() if s: log.warning('Printed to stdout after daemonization: %s', s) sys.stdout.seek(0) sys.stdout.truncate() # Truncates to current offset. s = sys.stderr.getvalue() if s: log.error('Printed to stderr after daemonization: %s', s) sys.stderr.seek(0) sys.stderr.truncate() # Truncates to current offset. doFlush = conf.supybot.flush() and not starting if doFlush: flush() # This is so registry._cache gets filled. # This seems dumb, so we'll try not doing it anymore. #if registryFilename is not None: # registry.open(registryFilename) if not dying: if minisix.PY2: log.debug('Regexp cache size: %s', len(re._cache)) log.debug('Pattern cache size: %s', len(ircutils._patternCache)) log.debug('HostmaskPatternEqual cache size: %s', len(ircutils._hostmaskPatternEqualCache)) #timestamp = log.timestamp() if doFlush: log.info('Flushers flushed and garbage collected.') else: log.info('Garbage collected.') collected = gc.collect() if gc.garbage: log.warning('Noncollectable garbage (file this as a bug on SF.net): %s', gc.garbage) return collected def makeDriversDie(): """Kills drivers.""" log.info('Killing Driver objects.') for driver in drivers._drivers.values(): driver.die() def makeIrcsDie(): """Kills Ircs.""" log.info('Killing Irc objects.') for irc in ircs[:]: if not irc.zombie: irc.die() else: log.debug('Not killing %s, it\'s already a zombie.', irc) def startDying(): """Starts dying.""" log.info('Shutdown initiated.') global dying dying = True def finished(): log.info('Shutdown complete.') # These are in order; don't reorder them for cosmetic purposes. The order # in which they're registered is the reverse order in which they will run. atexit.register(finished) atexit.register(upkeep) atexit.register(makeIrcsDie) atexit.register(makeDriversDie) atexit.register(startDying) ################################################## ################################################## ################################################## ## Don't even *think* about messing with these. ## ################################################## ################################################## ################################################## dying = False testing = False starting = False profiling = False documenting = False # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/version.py0000644000175000017500000000025713233426077016153 0ustar valval00000000000000version = '2018.01.25' try: # For import from setup.py import supybot.utils.python supybot.utils.python._debug_software_version = version except ImportError: pass limnoria-2018.01.25/src/unpreserve.py0000644000175000017500000000601013233426066016653 0ustar valval00000000000000### # 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. ### class Reader(object): def __init__(self, Creator, *args, **kwargs): self.Creator = Creator self.args = args self.kwargs = kwargs self.creator = None self.modifiedCreator = False self.indent = None def normalizeCommand(self, s): return s.lower() def readFile(self, filename): self.read(open(filename)) def read(self, fd): lineno = 0 for line in fd: lineno += 1 if not line.strip(): continue line = line.rstrip('\r\n') line = line.expandtabs() s = line.lstrip(' ') indent = len(line) - len(s) if indent != self.indent: # New indentation level. if self.creator is not None: self.creator.finish() self.creator = self.Creator(*self.args, **self.kwargs) self.modifiedCreator = False self.indent = indent (command, rest) = s.split(None, 1) command = self.normalizeCommand(command) self.modifiedCreator = True if hasattr(self.creator, command): command = getattr(self.creator, command) command(rest, lineno) else: self.creator.badCommand(command, rest, lineno) if self.modifiedCreator: self.creator.finish() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/test.py0000644000175000017500000006230013233426066015440 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2011, James McCoy # 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 gc import os import re import sys import time import shutil import urllib import unittest import functools import threading from . import (callbacks, conf, drivers, httpserver, i18n, ircdb, irclib, ircmsgs, ircutils, log, plugin, registry, utils, world) from .utils import minisix if minisix.PY2: from httplib import HTTPConnection from urllib import splithost, splituser from urllib import URLopener else: from http.client import HTTPConnection from urllib.parse import splithost, splituser from urllib.request import URLopener class verbosity: NONE = 0 EXCEPTIONS = 1 MESSAGES = 2 i18n.import_conf() network = True setuid = True # This is the global list of suites that are to be run. suites = [] timeout = 10 originalCallbacksGetHelp = callbacks.getHelp lastGetHelp = 'x' * 1000 def cachingGetHelp(method, name=None, doc=None): global lastGetHelp lastGetHelp = originalCallbacksGetHelp(method, name, doc) return lastGetHelp callbacks.getHelp = cachingGetHelp def retry(tries=3): assert tries > 0 def decorator(f): @functools.wraps(f) def newf(self): try: f(self) except AssertionError as e: first_exception = e for _ in range(1, tries): try: f(self) except AssertionError as e: pass else: break else: # All failed raise first_exception return newf return decorator def getTestIrc(): irc = irclib.Irc('test') # Gotta clear the connect messages (USER, NICK, etc.) while irc.takeMsg(): pass return irc class TimeoutError(AssertionError): def __str__(self): return '%r timed out' % self.args[0] class TestPlugin(callbacks.Plugin): def eval(self, irc, msg, args): """ This is the help for eval. Since Owner doesn't have an eval command anymore, we needed to add this so as not to invalidate any of the tests that depended on that eval command. """ try: irc.reply(repr(eval(' '.join(args)))) except callbacks.ArgumentError: raise except Exception as e: irc.reply(utils.exnToString(e)) # Since we know we don't now need the Irc object, we just give None. This # might break if callbacks.Privmsg ever *requires* the Irc object. TestInstance = TestPlugin(None) conf.registerPlugin('TestPlugin', True, public=False) class SupyTestCase(unittest.TestCase): """This class exists simply for extra logging. It's come in useful in the past.""" def setUp(self): log.critical('Beginning test case %s', self.id()) threads = [t.getName() for t in threading.enumerate()] log.critical('Threads: %L', threads) unittest.TestCase.setUp(self) def tearDown(self): for irc in world.ircs[:]: irc._reallyDie() if sys.version_info < (2, 7, 0): def assertIn(self, member, container, msg=None): """Just like self.assertTrue(a in b), but with a nicer default message.""" if member not in container: standardMsg = '%s not found in %s' % (repr(member), repr(container)) self.fail(self._formatMessage(msg, standardMsg)) def assertNotIn(self, member, container, msg=None): """Just like self.assertTrue(a not in b), but with a nicer default message.""" if member in container: standardMsg = '%s unexpectedly found in %s' % (repr(member), repr(container)) self.fail(self._formatMessage(msg, standardMsg)) def assertIs(self, expr1, expr2, msg=None): """Just like self.assertTrue(a is b), but with a nicer default message.""" if expr1 is not expr2: standardMsg = '%s is not %s' % (repr(expr1), repr(expr2)) self.fail(self._formatMessage(msg, standardMsg)) def assertIsNot(self, expr1, expr2, msg=None): """Just like self.assertTrue(a is not b), but with a nicer default message.""" if expr1 is expr2: standardMsg = 'unexpectedly identical: %s' % (repr(expr1),) self.fail(self._formatMessage(msg, standardMsg)) class PluginTestCase(SupyTestCase): """Subclass this to write a test case for a plugin. See plugins/Plugin/test.py for an example. """ plugins = None cleanConfDir = True cleanDataDir = True config = {} def __init__(self, methodName='runTest'): self.timeout = timeout originalRunTest = getattr(self, methodName) def runTest(self): run = True if hasattr(self, 'irc') and self.irc: for cb in self.irc.callbacks: cbModule = sys.modules[cb.__class__.__module__] if hasattr(cbModule, 'deprecated') and cbModule.deprecated: print('') print('Ignored, %s is deprecated.' % cb.name()) run = False if run: originalRunTest() runTest = utils.python.changeFunctionName(runTest, methodName) setattr(self.__class__, methodName, runTest) SupyTestCase.__init__(self, methodName=methodName) self.originals = {} def setUp(self, nick='test', forceSetup=False): if not forceSetup and \ self.__class__ in (PluginTestCase, ChannelPluginTestCase): # Necessary because there's a test in here that shouldn\'t run. return SupyTestCase.setUp(self) # Just in case, let's do this. Too many people forget to call their # super methods. for irc in world.ircs[:]: irc._reallyDie() # Set conf variables appropriately. conf.supybot.reply.whenAddressedBy.chars.setValue('@') conf.supybot.reply.error.detailed.setValue(True) conf.supybot.reply.whenNotCommand.setValue(True) self.myVerbose = world.myVerbose def rmFiles(dir): for filename in os.listdir(dir): file = os.path.join(dir, filename) if os.path.isfile(file): os.remove(file) else: shutil.rmtree(file) if self.cleanConfDir: rmFiles(conf.supybot.directories.conf()) if self.cleanDataDir: rmFiles(conf.supybot.directories.data()) ircdb.users.reload() ircdb.ignores.reload() ircdb.channels.reload() if self.plugins is None: raise ValueError('PluginTestCase must have a "plugins" attribute.') self.nick = nick self.prefix = ircutils.joinHostmask(nick, 'user', 'host.domain.tld') self.irc = getTestIrc() MiscModule = plugin.loadPluginModule('Misc') OwnerModule = plugin.loadPluginModule('Owner') ConfigModule = plugin.loadPluginModule('Config') plugin.loadPluginClass(self.irc, MiscModule) plugin.loadPluginClass(self.irc, OwnerModule) plugin.loadPluginClass(self.irc, ConfigModule) if isinstance(self.plugins, str): self.plugins = [self.plugins] else: for name in self.plugins: if name not in ('Owner', 'Misc', 'Config'): module = plugin.loadPluginModule(name, ignoreDeprecation=True) plugin.loadPluginClass(self.irc, module) self.irc.addCallback(TestInstance) for (name, value) in self.config.items(): group = conf.supybot parts = registry.split(name) if parts[0] == 'supybot': parts.pop(0) for part in parts: group = group.get(part) self.originals[group] = group() group.setValue(value) def tearDown(self): if self.__class__ in (PluginTestCase, ChannelPluginTestCase): # Necessary because there's a test in here that shouldn\'t run. return for (group, original) in self.originals.items(): group.setValue(original) ircdb.users.close() ircdb.ignores.close() ircdb.channels.close() SupyTestCase.tearDown(self) self.irc = None gc.collect() def _feedMsg(self, query, timeout=None, to=None, frm=None, usePrefixChar=True, expectException=False): if to is None: to = self.irc.nick if frm is None: frm = self.prefix if timeout is None: timeout = self.timeout if self.myVerbose >= verbosity.MESSAGES: print('') # Extra newline, so it's pretty. prefixChars = conf.supybot.reply.whenAddressedBy.chars() if not usePrefixChar and query[0] in prefixChars: query = query[1:] if minisix.PY2: query = query.encode('utf8') # unicode->str msg = ircmsgs.privmsg(to, query, prefix=frm) if self.myVerbose >= verbosity.MESSAGES: print('Feeding: %r' % msg) if not expectException and self.myVerbose >= verbosity.EXCEPTIONS: conf.supybot.log.stdout.setValue(True) self.irc.feedMsg(msg) fed = time.time() response = self.irc.takeMsg() while response is None and time.time() - fed < timeout: time.sleep(0.01) # So it doesn't suck up 100% cpu. drivers.run() response = self.irc.takeMsg() if self.myVerbose >= verbosity.MESSAGES: print('Response: %r' % response) if not expectException and self.myVerbose >= verbosity.EXCEPTIONS: conf.supybot.log.stdout.setValue(False) return response def getMsg(self, query, **kwargs): return self._feedMsg(query, **kwargs) def feedMsg(self, query, to=None, frm=None): """Just feeds it a message, that's all.""" if to is None: to = self.irc.nick if frm is None: frm = self.prefix self.irc.feedMsg(ircmsgs.privmsg(to, query, prefix=frm)) # These assertError/assertNoError are somewhat fragile. The proper way to # do them would be to use a proxy for the irc object and intercept .error. # But that would be hard, so I don't bother. When this breaks, it'll get # fixed, but not until then. def assertError(self, query, **kwargs): m = self._feedMsg(query, expectException=True, **kwargs) if m is None: raise TimeoutError(query) if lastGetHelp not in m.args[1]: self.failUnless(m.args[1].startswith('Error:'), '%r did not error: %s' % (query, m.args[1])) return m def assertSnarfError(self, query, **kwargs): return self.assertError(query, usePrefixChar=False, **kwargs) def assertNotError(self, query, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) self.failIf(m.args[1].startswith('Error:'), '%r errored: %s' % (query, m.args[1])) self.failIf(lastGetHelp in m.args[1], '%r returned the help string.' % query) return m def assertSnarfNotError(self, query, **kwargs): return self.assertNotError(query, usePrefixChar=False, **kwargs) def assertHelp(self, query, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) msg = m.args[1] if 'more message' in msg: msg = msg[0:-27] # Strip (XXX more messages) self.failUnless(msg in lastGetHelp, '%s is not the help (%s)' % (m.args[1], lastGetHelp)) return m def assertNoResponse(self, query, timeout=0, **kwargs): m = self._feedMsg(query, timeout=timeout, **kwargs) self.failIf(m, 'Unexpected response: %r' % m) return m def assertSnarfNoResponse(self, query, timeout=0, **kwargs): return self.assertNoResponse(query, timeout=timeout, usePrefixChar=False, **kwargs) def assertResponse(self, query, expectedResponse, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) self.assertEqual(m.args[1], expectedResponse, '%r != %r' % (expectedResponse, m.args[1])) return m def assertSnarfResponse(self, query, expectedResponse, **kwargs): return self.assertResponse(query, expectedResponse, usePrefixChar=False, **kwargs) def assertRegexp(self, query, regexp, flags=re.I, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) self.failUnless(re.search(regexp, m.args[1], flags), '%r does not match %r' % (m.args[1], regexp)) return m def assertSnarfRegexp(self, query, regexp, flags=re.I, **kwargs): return self.assertRegexp(query, regexp, flags=re.I, usePrefixChar=False, **kwargs) def assertNotRegexp(self, query, regexp, flags=re.I, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) self.failUnless(re.search(regexp, m.args[1], flags) is None, '%r matched %r' % (m.args[1], regexp)) return m def assertSnarfNotRegexp(self, query, regexp, flags=re.I, **kwargs): return self.assertNotRegexp(query, regexp, flags=re.I, usePrefixChar=False, **kwargs) def assertAction(self, query, expectedResponse=None, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) self.failUnless(ircmsgs.isAction(m), '%r is not an action.' % m) if expectedResponse is not None: s = ircmsgs.unAction(m) self.assertEqual(s, expectedResponse, '%r != %r' % (s, expectedResponse)) return m def assertSnarfAction(self, query, expectedResponse=None, **kwargs): return self.assertAction(query, expectedResponse=None, usePrefixChar=False, **kwargs) def assertActionRegexp(self, query, regexp, flags=re.I, **kwargs): m = self._feedMsg(query, **kwargs) if m is None: raise TimeoutError(query) self.failUnless(ircmsgs.isAction(m)) s = ircmsgs.unAction(m) self.failUnless(re.search(regexp, s, flags), '%r does not match %r' % (s, regexp)) def assertSnarfActionRegexp(self, query, regexp, flags=re.I, **kwargs): return self.assertActionRegexp(query, regexp, flags=re.I, usePrefixChar=False, **kwargs) _noTestDoc = ('Admin', 'Channel', 'Config', 'Misc', 'Owner', 'User', 'TestPlugin') def TestDocumentation(self): if self.__class__ in (PluginTestCase, ChannelPluginTestCase): return for cb in self.irc.callbacks: name = cb.name() if ((name in self._noTestDoc) and \ not name.lower() in self.__class__.__name__.lower()): continue self.failUnless(sys.modules[cb.__class__.__name__].__doc__, '%s has no module documentation.' % name) if hasattr(cb, 'isCommandMethod'): for attr in dir(cb): if cb.isCommandMethod(attr) and \ attr == callbacks.canonicalName(attr): self.failUnless(getattr(cb, attr, None).__doc__, '%s.%s has no help.' % (name, attr)) class ChannelPluginTestCase(PluginTestCase): channel = '#test' def setUp(self, nick='test', forceSetup=False): if not forceSetup and \ self.__class__ in (PluginTestCase, ChannelPluginTestCase): return PluginTestCase.setUp(self) self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) m = self.irc.takeMsg() self.failIf(m is None, 'No message back from joining channel.') self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.failIf(m is None, 'No message back from joining channel.') self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.failIf(m is None, 'No message back from joining channel.') self.assertEqual(m.command, 'WHO') def _feedMsg(self, query, timeout=None, to=None, frm=None, private=False, usePrefixChar=True, expectException=False): if to is None: if private: to = self.irc.nick else: to = self.channel if frm is None: frm = self.prefix if timeout is None: timeout = self.timeout if self.myVerbose >= verbosity.MESSAGES: print('') # Newline, just like PluginTestCase. prefixChars = conf.supybot.reply.whenAddressedBy.chars() if query[0] not in prefixChars and usePrefixChar: query = prefixChars[0] + query if minisix.PY2 and isinstance(query, unicode): query = query.encode('utf8') # unicode->str if not expectException and self.myVerbose >= verbosity.EXCEPTIONS: conf.supybot.log.stdout.setValue(True) msg = ircmsgs.privmsg(to, query, prefix=frm) if self.myVerbose >= verbosity.MESSAGES: print('Feeding: %r' % msg) self.irc.feedMsg(msg) fed = time.time() response = self.irc.takeMsg() while response is None and time.time() - fed < timeout: time.sleep(0.1) drivers.run() response = self.irc.takeMsg() if response is not None: if response.command == 'PRIVMSG': args = list(response.args) # Strip off nick: at beginning of response. if args[1].startswith(self.nick) or \ args[1].startswith(ircutils.nickFromHostmask(self.prefix)): try: args[1] = args[1].split(' ', 1)[1] except IndexError: # Odd. We'll skip this. pass ret = ircmsgs.privmsg(*args) else: ret = response else: ret = None if self.myVerbose >= verbosity.MESSAGES: print('Returning: %r' % ret) if not expectException and self.myVerbose >= verbosity.EXCEPTIONS: conf.supybot.log.stdout.setValue(False) return ret def feedMsg(self, query, to=None, frm=None, private=False): """Just feeds it a message, that's all.""" if to is None: if private: to = self.irc.nick else: to = self.channel if frm is None: frm = self.prefix self.irc.feedMsg(ircmsgs.privmsg(to, query, prefix=frm)) class TestRequestHandler(httpserver.SupyHTTPRequestHandler): def __init__(self, rfile, wfile, *args, **kwargs): self._headers_mode = True self.rfile = rfile self.wfile = wfile self.handle_one_request() def send_response(self, code): assert self._headers_mode self._response = code def send_headers(self, name, value): assert self._headers_mode self._headers[name] = value def end_headers(self): assert self._headers_mode self._headers_mode = False def do_X(self, *args, **kwargs): assert httpserver.http_servers, \ 'The HTTP server is not started.' self.server = httpserver.http_servers[0] httpserver.SupyHTTPRequestHandler.do_X(self, *args, **kwargs) httpserver.http_servers = [httpserver.TestSupyHTTPServer()] # Partially stolen from the standard Python library :) def open_http(url, data=None): """Use HTTP protocol.""" user_passwd = None proxy_passwd= None if isinstance(url, str): host, selector = splithost(url) if host: user_passwd, host = splituser(host) host = urllib.unquote(host) realhost = host else: host, selector = url # check whether the proxy contains authorization information proxy_passwd, host = splituser(host) # now we proceed with the url we want to obtain urltype, rest = urllib.splittype(selector) url = rest user_passwd = None if urltype.lower() != 'http': realhost = None else: realhost, rest = splithost(rest) if realhost: user_passwd, realhost = splituser(realhost) if user_passwd: selector = "%s://%s%s" % (urltype, realhost, rest) if urllib.proxy_bypass(realhost): host = realhost #print "proxy via http:", host, selector if not host: raise IOError('http error', 'no host given') if proxy_passwd: import base64 proxy_auth = base64.b64encode(proxy_passwd).strip() else: proxy_auth = None if user_passwd: import base64 auth = base64.b64encode(user_passwd).strip() else: auth = None c = FakeHTTPConnection(host) if data is not None: c.putrequest('POST', selector) c.putheader('Content-Type', 'application/x-www-form-urlencoded') c.putheader('Content-Length', '%d' % len(data)) else: c.putrequest('GET', selector) if proxy_auth: c.putheader('Proxy-Authorization', 'Basic %s' % proxy_auth) if auth: c.putheader('Authorization', 'Basic %s' % auth) if realhost: c.putheader('Host', realhost) for args in URLopener().addheaders: c.putheader(*args) c.endheaders() return c class FakeHTTPConnection(HTTPConnection): _data = '' _headers = {} def __init__(self, rfile, wfile): HTTPConnection.__init__(self, 'localhost') self.rfile = rfile self.wfile = wfile def send(self, data): if minisix.PY3 and isinstance(data, bytes): data = data.decode() self.wfile.write(data) #def putheader(self, name, value): # self._headers[name] = value #def connect(self, *args, **kwargs): # self.sock = self.wfile #def getresponse(self, *args, **kwargs): # pass class HTTPPluginTestCase(PluginTestCase): def setUp(self): PluginTestCase.setUp(self, forceSetup=True) def request(self, url, method='GET', read=True, data={}): assert url.startswith('/') wfile = minisix.io.StringIO() rfile = minisix.io.StringIO() connection = FakeHTTPConnection(wfile, rfile) connection.putrequest(method, url) connection.endheaders() rfile.seek(0) wfile.seek(0) handler = TestRequestHandler(rfile, wfile) if read: return (handler._response, wfile.read()) else: return handler._response def assertHTTPResponse(self, uri, expectedResponse, **kwargs): response = self.request(uri, read=False, **kwargs) self.assertEqual(response, expectedResponse) def assertNotHTTPResponse(self, uri, expectedResponse, **kwargs): response = self.request(uri, read=False, **kwargs) self.assertNotEqual(response, expectedResponse) class ChannelHTTPPluginTestCase(ChannelPluginTestCase, HTTPPluginTestCase): def setUp(self): ChannelPluginTestCase.setUp(self, forceSetup=True) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/shlex.py0000644000175000017500000002000613233426066015601 0ustar valval00000000000000"""A lexical analyzer class for simple shell-like syntaxes.""" # Module and documentation by Eric S. Raymond, 21 Dec 1998 # Input stacking and error message cleanup added by ESR, March 2000 # push_source() and pop_source() made explicit by ESR, January 2001. import os.path import sys from .utils import minisix __all__ = ["shlex"] class shlex: "A lexical analyzer class for simple shell-like syntaxes." def __init__(self, instream=None, infile=None): if instream is not None: self.instream = instream self.infile = infile else: self.instream = sys.stdin self.infile = None self.commenters = '#' self.whitespace = ' \t\r\n' self.separators = self.whitespace self.quotes = '\'"' self.state = ' ' self.pushback = [] self.lineno = 1 self.debug = 0 self.token = '' self.backslash = False self.filestack = [] self.source = None if self.debug: print('shlex: reading from %s, line %d' \ % (self.instream, self.lineno)) def push_token(self, tok): "Push a token onto the stack popped by the get_token method" if self.debug >= 1: print("shlex: pushing token " + repr(tok)) self.pushback = [tok] + self.pushback def push_source(self, newstream, newfile=None): "Push an input source onto the lexer's input source stack." self.filestack.insert(0, (self.infile, self.instream, self.lineno)) self.infile = newfile self.instream = newstream self.lineno = 1 if self.debug: if newfile is not None: print('shlex: pushing to file %s' % (self.infile,)) else: print('shlex: pushing to stream %s' % (self.instream,)) def pop_source(self): "Pop the input source stack." self.instream.close() (self.infile, self.instream, self.lineno) = self.filestack[0] self.filestack = self.filestack[1:] if self.debug: print('shlex: popping to %s, line %d' \ % (self.instream, self.lineno)) self.state = ' ' def get_token(self): "Get a token from the input stream (or from stack if it's nonempty)" if self.pushback: tok = self.pushback[0] self.pushback = self.pushback[1:] if self.debug >= 1: print("shlex: popping token " + repr(tok)) return tok # No pushback. Get a token. raw = self.read_token() # Handle inclusions while raw == self.source: spec = self.sourcehook(self.read_token()) if spec: (newfile, newstream) = spec self.push_source(newstream, newfile) raw = self.get_token() # Maybe we got EOF instead? while raw == "": if len(self.filestack) == 0: return "" else: self.pop_source() raw = self.get_token() # Neither inclusion nor EOF if self.debug >= 1: if raw: print("shlex: token=" + repr(raw)) else: print("shlex: token=EOF") return raw def read_token(self): "Read a token from the input stream (no pushback or inclusions)" while True: nextchar = self.instream.read(1) if nextchar == '\n': self.lineno = self.lineno + 1 if self.debug >= 3: print("shlex: in state", repr(self.state), \ "I see character:", repr(nextchar)) if self.state is None: self.token = '' # past end of file break elif self.state == ' ': if not nextchar: self.state = None # end of file break elif nextchar in self.whitespace: if self.debug >= 2: print("shlex: I see whitespace in whitespace state") if self.token: break # emit current token else: continue elif nextchar in self.commenters: self.instream.readline() self.lineno = self.lineno + 1 elif nextchar not in self.separators: self.token = nextchar self.state = 'a' elif nextchar in self.quotes: self.token = nextchar self.state = nextchar else: self.token = nextchar if self.token: break # emit current token else: continue elif self.state in self.quotes: self.token = self.token + nextchar if nextchar == '\\': if self.backslash: self.backslash = False else: self.backslash = True else: if not self.backslash and nextchar == self.state: self.state = ' ' break elif self.backslash: self.backslash = False elif not nextchar: # end of file if self.debug >= 2: print("shlex: I see EOF in quotes state") # XXX what error should be raised here? raise ValueError("No closing quotation") elif self.state == 'a': if not nextchar: self.state = None # end of file break elif nextchar in self.whitespace: if self.debug >= 2: print("shlex: I see whitespace in word state") self.state = ' ' if self.token: break # emit current token else: continue elif nextchar in self.commenters: self.instream.readline() self.lineno = self.lineno + 1 elif nextchar not in self.separators or nextchar in self.quotes: self.token = self.token + nextchar else: self.pushback = [nextchar] + self.pushback if self.debug >= 2: print("shlex: I see punctuation in word state") self.state = ' ' if self.token: break # emit current token else: continue result = self.token self.token = '' if self.debug > 1: if result: print("shlex: raw token=" + repr(result)) else: print("shlex: raw token=EOF") return result def sourcehook(self, newfile): "Hook called on a filename to be sourced." if newfile[0] == '"': newfile = newfile[1:-1] # This implements cpp-like semantics for relative-path inclusion. if isinstance(self.infile, minisix.string_types) and not os.path.isabs(newfile): newfile = os.path.join(os.path.dirname(self.infile), newfile) return (newfile, open(newfile, "r")) def error_leader(self, infile=None, lineno=None): "Emit a C-compiler-like, Emacs-friendly error-message leader." if infile is None: infile = self.infile if lineno is None: lineno = self.lineno return "\"%s\", line %d: " % (infile, lineno) if __name__ == '__main__': if len(sys.argv) == 1: lexer = shlex() else: file = sys.argv[1] with open(file) as fd: lexer = shlex(fd, file) while True: tt = lexer.get_token() if tt: print("Token: " + repr(tt)) else: break limnoria-2018.01.25/src/schedule.py0000644000175000017500000001333213233426066016256 0ustar valval00000000000000### # 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. ### """ Schedule plugin with a subclass of drivers.IrcDriver in order to be run as a Supybot driver. """ from __future__ import with_statement import time import heapq import functools from threading import Lock from . import drivers, log, world class mytuple(tuple): def __cmp__(self, other): return cmp(self[0], other[0]) def __le__(self, other): return self[0] <= other[0] def __lt__(self, other): return self[0] < other[0] def __gt__(self, other): return self[0] > other[0] def __ge__(self, other): return self[0] >= other[0] class Schedule(drivers.IrcDriver): """An IrcDriver to handling scheduling of events. Events, in this case, are functions accepting no arguments. """ def __init__(self): drivers.IrcDriver.__init__(self) self.schedule = [] self.events = {} self.counter = 0 self.lock = Lock() def reset(self): with self.lock: self.events.clear() self.schedule[:] = [] # We don't reset the counter here because if someone has held an id of # one of the nuked events, we don't want them removing new events with # their old id. def name(self): return 'Schedule' def addEvent(self, f, t, name=None, args=[], kwargs={}): """Schedules an event f to run at time t. name must be hashable and not an int. """ if name is None: name = self.counter self.counter += 1 assert name not in self.events, \ 'An event with the same name has already been scheduled.' with self.lock: self.events[name] = f heapq.heappush(self.schedule, mytuple((t, name, args, kwargs))) return name def removeEvent(self, name): """Removes the event with the given name from the schedule.""" f = self.events.pop(name) # We must heapify here because the heap property may not be preserved # by the above list comprehension. We could, conceivably, just mark # the elements of the heap as removed and ignore them when we heappop, # but that would only save a constant factor (we're already linear for # the listcomp) so I'm not worried about it right now. with self.lock: self.schedule = [x for x in self.schedule if x[1] != name] heapq.heapify(self.schedule) return f def rescheduleEvent(self, name, t): f = self.removeEvent(name) self.addEvent(f, t, name=name) def addPeriodicEvent(self, f, t, name=None, now=True, args=[], kwargs={}, count=None): """Adds a periodic event that is called every t seconds.""" def wrapper(count): try: f(*args, **kwargs) finally: # Even if it raises an exception, let's schedule it. if count[0] is not None: count[0] -= 1 if count[0] is None or count[0] > 0: return self.addEvent(wrapper, time.time() + t, name) wrapper = functools.partial(wrapper, [count]) if now: return wrapper() else: return self.addEvent(wrapper, time.time() + t, name) removePeriodicEvent = removeEvent def run(self): if len(drivers._drivers) == 1 and not world.testing: log.error('Schedule is the only remaining driver, ' 'why do we continue to live?') time.sleep(1) # We're the only driver; let's pause to think. while self.schedule and self.schedule[0][0] < time.time(): with self.lock: (t, name, args, kwargs) = heapq.heappop(self.schedule) f = self.events.pop(name) try: f(*args, **kwargs) except Exception: log.exception('Uncaught exception in scheduled function:') schedule = Schedule() addEvent = schedule.addEvent removeEvent = schedule.removeEvent rescheduleEvent = schedule.rescheduleEvent addPeriodicEvent = schedule.addPeriodicEvent removePeriodicEvent = removeEvent run = schedule.run # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/registry.py0000644000175000017500000006316413233426066016342 0ustar valval00000000000000### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # 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 time import json import codecs import string import textwrap from . import utils, i18n from .utils import minisix _ = i18n.PluginInternationalization() def error(s): """Replace me with something better from another module!""" print('***', s) def exception(s): """Ditto!""" print('***', s, 'A bad exception.') class RegistryException(Exception): pass class InvalidRegistryFile(RegistryException): pass class InvalidRegistryName(RegistryException): pass class InvalidRegistryValue(RegistryException): pass class NonExistentRegistryEntry(RegistryException, AttributeError): # If we use hasattr() on a configuration group/value, Python 3 calls # __getattr__ and looks for an AttributeError, so __getattr__ has to # raise an AttributeError if a registry entry does not exist. pass ENCODING = 'string_escape' if minisix.PY2 else 'unicode_escape' decoder = codecs.getdecoder(ENCODING) encoder = codecs.getencoder(ENCODING) _cache = utils.InsensitivePreservingDict() _lastModified = 0 def open_registry(filename, clear=False): """Initializes the module by loading the registry file into memory.""" global _lastModified if clear: _cache.clear() _fd = open(filename) fd = utils.file.nonCommentNonEmptyLines(_fd) acc = '' slashEnd = re.compile(r'\\*$') for line in fd: line = line.rstrip('\r\n') # XXX There should be some way to determine whether or not we're # starting a new variable or not. As it is, if there's a backslash # at the end of every line in a variable, it won't be read, and # worse, the error will pass silently. # # If the line ends in an odd number of backslashes, then there is a # line-continutation. m = slashEnd.search(line) if m and len(m.group(0)) % 2: acc += line[:-1] continue else: acc += line try: (key, value) = re.split(r'(?', _cache[name] self.set(_cache[name]) if self._supplyDefault: for (k, v) in _cache.items(): if k.startswith(self._name): rest = k[len(self._name)+1:] # +1 is for . parts = split(rest) if len(parts) == 1 and parts[0] == name: try: self.__makeChild(name, v) except InvalidRegistryValue: # It's probably supposed to be registered later. pass def register(self, name, node=None): if not isValidRegistryName(name): raise InvalidRegistryName(name) if node is None: node = Group(private=self._private) else: node._private = node._private or self._private # We tried in any number of horrible ways to make it so that # re-registering something would work. It doesn't, plain and simple. # For the longest time, we had an "Is this right?" comment here, but # from experience, we now know that it most definitely *is* right. if name not in self._children: self._children[name] = node self._added.append(name) names = split(self._name) names.append(name) fullname = join(names) node.setName(fullname) else: # We do this in order to reload the help, if it changed. if node._help != '' and node._help != self._children[name]._help: self._children[name]._help = node._help # We do this so the return value from here is at least useful; # otherwise, we're just returning a useless, unattached node # that's simply a waste of space. node = self._children[name] return node def unregister(self, name): try: node = self._children[name] del self._children[name] # We do this because we need to remove case-insensitively. name = name.lower() for elt in reversed(self._added): if elt.lower() == name: self._added.remove(elt) if node._name in _cache: del _cache[node._name] return node except KeyError: self.__nonExistentEntry(name) def rename(self, old, new): node = self.unregister(old) self.register(new, node) def getValues(self, getChildren=False, fullNames=True): L = [] if self._orderAlphabetically: self._added.sort() for name in self._added: node = self._children[name] if hasattr(node, 'value') or hasattr(node, 'help'): if node.__class__ is not self.X: L.append((node._name, node)) if getChildren: L.extend(node.getValues(getChildren, fullNames)) if not fullNames: L = [(split(s)[-1], node) for (s, node) in L] return L class _NoValueGiven: # Special value for Value.error() pass class Value(Group): """Invalid registry value. If you're getting this message, report it, because we forgot to put a proper help string here.""" def __init__(self, default, help, setDefault=True, showDefault=True, **kwargs): self.__parent = super(Value, self) self.__parent.__init__(help, **kwargs) self._default = default self._showDefault = showDefault self._help = utils.str.normalizeWhitespace(help.strip()) self._callbacks = [] if setDefault: self.setValue(default) def error(self, value=_NoValueGiven): if hasattr(self, 'errormsg') and value is not _NoValueGiven: try: s = self.errormsg % value except TypeError: s = self.errormsg elif self.__doc__: s = self.__doc__ else: s = """%s has no docstring. If you're getting this message, report it, because we forgot to put a proper help string here."""%\ self._name e = InvalidRegistryValue(utils.str.normalizeWhitespace(s)) e.value = self raise e def setName(self, *args): if self._name == 'unset': self._lastModified = 0 self.__parent.setName(*args) self._lastModified = time.time() def set(self, s): """Override this with a function to convert a string to whatever type you want, and call self.setValue to set the value.""" self.setValue(s) def setValue(self, v): """Check conditions on the actual value type here. I.e., if you're a IntegerLessThanOneHundred (all your values must be integers less than 100) convert to an integer in set() and check that the integer is less than 100 in this method. You *must* call this parent method in your own setValue.""" self._lastModified = time.time() self.value = v if self._supplyDefault: for (name, v) in list(self._children.items()): if v.__class__ is self.X: self.unregister(name) # We call the callback once everything is clean for callback, args, kwargs in self._callbacks: callback(*args, **kwargs) def context(self, value): """Return a context manager object, which sets this variable to a temporary value, and set the previous value back when exiting the context.""" class Context: def __enter__(self2): self2._old_value = self.value self.setValue(value) def __exit__(self2, exc_type, exc_value, traceback): self.setValue(self2._old_value) return Context() def addCallback(self, callback, *args, **kwargs): """Add a callback to the list. A callback is a function that will be called when the value is changed. You can give this function as many extra arguments as you wish, they will be passed to the callback.""" self._callbacks.append((callback, args, kwargs)) def removeCallback(self, callback): """Remove all occurences of this callbacks from the callback list.""" self._callbacks = [x for x in self._callbacks if x[0] is not callback] def __str__(self): return repr(self()) def serialize(self): return encoder(str(self))[0].decode() # We tried many, *many* different syntactic methods here, and this one was # simply the best -- not very intrusive, easily overridden by subclasses, # etc. def __call__(self): if _lastModified > self._lastModified: if self._name in _cache: self.set(_cache[self._name]) return self.value class Boolean(Value): """Value must be either True or False (or On or Off).""" errormsg = _('Value must be either True or False (or On or Off), not %r.') def set(self, s): try: v = utils.str.toBool(s) except ValueError: if s.strip().lower() == 'toggle': v = not self.value else: self.error(s) self.setValue(v) def setValue(self, v): super(Boolean, self).setValue(bool(v)) class Integer(Value): """Value must be an integer.""" errormsg = _('Value must be an integer, not %r.') def set(self, s): try: self.setValue(int(s)) except ValueError: self.error(s) class NonNegativeInteger(Integer): """Value must be a non-negative integer.""" errormsg = _('Value must be a non-negative integer, not %r.') def setValue(self, v): if v < 0: self.error(v) super(NonNegativeInteger, self).setValue(v) class PositiveInteger(NonNegativeInteger): """Value must be positive (non-zero) integer.""" errormsg = _('Value must be positive (non-zero) integer, not %r.') def setValue(self, v): if not v: self.error(v) super(PositiveInteger, self).setValue(v) class Float(Value): """Value must be a floating-point number.""" errormsg = _('Value must be a floating-point number, not %r.') def set(self, s): try: self.setValue(float(s)) except ValueError: self.error(s) def setValue(self, v): try: super(Float, self).setValue(float(v)) except ValueError: self.error(v) class PositiveFloat(Float): """Value must be a floating-point number greater than zero.""" errormsg = _('Value must be a floating-point number greater than zero, ' 'not %r.') def setValue(self, v): if v <= 0: self.error(v) else: super(PositiveFloat, self).setValue(v) class Probability(Float): """Value must be a floating point number in the range [0, 1].""" errormsg = _('Value must be a floating point number in the range [0, 1], ' 'not %r.') def __init__(self, *args, **kwargs): self.__parent = super(Probability, self) self.__parent.__init__(*args, **kwargs) def setValue(self, v): if 0 <= v <= 1: self.__parent.setValue(v) else: self.error(v) class String(Value): """Value is not a valid Python string.""" errormsg = _('Value should be a valid Python string, not %r.') def set(self, s): v = s if not v: v = '""' elif v[0] != v[-1] or v[0] not in '\'"': v = repr(v) try: v = utils.safeEval(v) if not isinstance(v, minisix.string_types): raise ValueError self.setValue(v) except ValueError: # This catches utils.safeEval(s) errors too. self.error(s) _printable = string.printable[:-4] def _needsQuoting(self, s): return any([x not in self._printable for x in s]) and s.strip() != s def __str__(self): s = self.value if self._needsQuoting(s): s = repr(s) return s class OnlySomeStrings(String): validStrings = () def __init__(self, *args, **kwargs): assert self.validStrings, 'There must be some valid strings. ' \ 'This is a bug.' self.__parent = super(OnlySomeStrings, self) self.__parent.__init__(*args, **kwargs) self.__doc__ = format(_('Valid values include %L.'), list(map(repr, self.validStrings))) self.errormsg = format(_('Valid values include %L, not %%r.'), list(map(repr, self.validStrings))) def help(self): strings = [s for s in self.validStrings if s] return format('%s Valid strings: %L.', self._help, strings) def normalize(self, s): lowered = s.lower() L = list(map(str.lower, self.validStrings)) try: i = L.index(lowered) except ValueError: return s # This is handled in setValue. return self.validStrings[i] def setValue(self, s): v = self.normalize(s) if s in self.validStrings: self.__parent.setValue(v) else: self.error(v) class NormalizedString(String): def __init__(self, default, *args, **kwargs): default = self.normalize(default) self.__parent = super(NormalizedString, self) self.__parent.__init__(default, *args, **kwargs) self._showDefault = False def normalize(self, s): return utils.str.normalizeWhitespace(s.strip()) def set(self, s): s = self.normalize(s) self.__parent.set(s) def setValue(self, s): s = self.normalize(s) self.__parent.setValue(s) def serialize(self): s = self.__parent.serialize() prefixLen = len(self._name) + 2 lines = textwrap.wrap(s, width=76-prefixLen) last = len(lines)-1 for (i, line) in enumerate(lines): if i != 0: line = ' '*prefixLen + line if i != last: line += '\\' lines[i] = line ret = os.linesep.join(lines) return ret class StringSurroundedBySpaces(String): def setValue(self, v): if v and v.lstrip() == v: v= ' ' + v if v.rstrip() == v: v += ' ' super(StringSurroundedBySpaces, self).setValue(v) class StringWithSpaceOnRight(String): def setValue(self, v): if v and v.rstrip() == v: v += ' ' super(StringWithSpaceOnRight, self).setValue(v) class Regexp(Value): """Value must be a valid regular expression.""" errormsg = _('Value must be a valid regular expression, not %r.') def __init__(self, *args, **kwargs): kwargs['setDefault'] = False self.sr = '' self.value = None self.__parent = super(Regexp, self) self.__parent.__init__(*args, **kwargs) def error(self, e): s = 'Value must be a regexp of the form m/.../ or /.../. %s' % e e = InvalidRegistryValue(s) e.value = self raise e def set(self, s): try: if s: self.setValue(utils.str.perlReToPythonRe(s), sr=s) else: self.setValue(None) except ValueError as e: self.error(e) def setValue(self, v, sr=None): if v is None: self.sr = '' self.__parent.setValue(None) elif sr is not None: self.sr = sr self.__parent.setValue(v) else: raise InvalidRegistryValue('Can\'t setValue a regexp, there would be an inconsistency '\ 'between the regexp and the recorded string value.') def __str__(self): self() # Gotta update if we've been reloaded. return self.sr class SeparatedListOf(Value): List = list Value = Value sorted = False def splitter(self, s): """Override this with a function that takes a string and returns a list of strings.""" raise NotImplementedError def joiner(self, L): """Override this to join the internal list for output.""" raise NotImplementedError def set(self, s): L = self.splitter(s) for (i, s) in enumerate(L): v = self.Value(s, '') L[i] = v() self.setValue(L) def setValue(self, v): super(SeparatedListOf, self).setValue(self.List(v)) def __str__(self): values = self() if self.sorted: values = sorted(values) if values: return self.joiner(values) else: # We must return *something* here, otherwise down along the road we # can run into issues showing users the value if they've disabled # nick prefixes in any of the numerous ways possible. Since the # config parser doesn't care about this space, we'll use it :) return ' ' class SpaceSeparatedListOf(SeparatedListOf): def splitter(self, s): return s.split() joiner = ' '.join class SpaceSeparatedListOfStrings(SpaceSeparatedListOf): Value = String class SpaceSeparatedSetOfStrings(SpaceSeparatedListOfStrings): List = set class CommaSeparatedListOfStrings(SeparatedListOf): Value = String def splitter(self, s): return re.split(r'\s*,\s*', s) joiner = ', '.join class CommaSeparatedSetOfStrings(SeparatedListOf): List = set Value = String def splitter(self, s): return re.split(r'\s*,\s*', s) joiner = ', '.join class TemplatedString(String): requiredTemplates = [] def __init__(self, *args, **kwargs): assert self.requiredTemplates, \ 'There must be some templates. This is a bug.' self.__parent = super(String, self) self.__parent.__init__(*args, **kwargs) def setValue(self, v): def hasTemplate(s): return re.search(r'\$%s\b|\${%s}' % (s, s), v) is not None if utils.iter.all(hasTemplate, self.requiredTemplates): self.__parent.setValue(v) else: self.error(v) class Json(String): # Json-serializable data def set(self, v): self.setValue(json.loads(v)) def setValue(self, v): super(Json, self).setValue(json.dumps(v)) def __call__(self): return json.loads(super(Json, self).__call__()) class _Context: def __init__(self, var): self._var = var def __enter__(self): self._dict = self._var() return self._dict def __exit__(self, *args): self._var.setValue(self._dict) def editable(self): """Return an editable dict usable within a 'with' statement and committed to the configuration variable at the end.""" return self._Context(self) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/questions.py0000644000175000017500000001226213233426066016515 0ustar valval00000000000000### # 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. ### """Handles interactive questions; useful for wizards and whatnot.""" from __future__ import print_function import sys import textwrap from getpass import getpass as getPass from . import ansi, utils from .utils import minisix from supybot.i18n import PluginInternationalization _ = PluginInternationalization() useBold = False def output(s, unformatted=True, fd=sys.stdout): if unformatted: s = textwrap.fill(utils.str.normalizeWhitespace(s), width=65) print(s, file=fd) print('', file=fd) def expect(prompt, possibilities, recursed=False, default=None, acceptEmpty=False, fd=sys.stdout): """Prompt the user with prompt, allow them to choose from possibilities. If possibilities is empty, allow anything. """ prompt = utils.str.normalizeWhitespace(prompt) originalPrompt = prompt if recursed: output(_('Sorry, that response was not an option.')) if useBold: choices = '[%s%%s%s]' % (ansi.RESET, ansi.BOLD) else: choices = '[%s]' if possibilities: prompt = '%s %s' % (originalPrompt, choices % '/'.join(possibilities)) if len(prompt) > 70: prompt = '%s %s' % (originalPrompt, choices % '/ '.join(possibilities)) if default is not None: if useBold: prompt = '%s %s(default: %s)' % (prompt, ansi.RESET, default) else: prompt = '%s (default: %s)' % (prompt, default) prompt = textwrap.fill(prompt) prompt = prompt.replace('/ ', '/') prompt = prompt.strip() + ' ' if useBold: prompt += ansi.RESET print(ansi.BOLD, end=' ', file=fd) if minisix.PY3: s = input(prompt) else: s = raw_input(prompt) s = s.strip() print(file=fd) if possibilities: if s in possibilities: return s elif not s and default is not None: return default elif not s and acceptEmpty: return s else: return expect(originalPrompt, possibilities, recursed=True, default=default) else: if not s and default is not None: return default return s.strip() def anything(prompt): """Allow anything from the user.""" return expect(prompt, []) def something(prompt, default=None): """Allow anything *except* nothing from the user.""" s = expect(prompt, [], default=default) while not s: output(_('Sorry, you must enter a value.')) s = expect(prompt, [], default=default) return s def yn(prompt, default=None): """Allow only 'y' or 'n' from the user.""" if default is not None: if default: default = 'y' else: default = 'n' s = expect(prompt, ['y', 'n'], default=default) if s == 'y': return True else: return False def getpass(prompt=None, secondPrompt=None): """Prompt the user for a password.""" if prompt is None: prompt = _('Enter password: ') if secondPrompt is None: secondPrompt = _('Re-enter password: ') password = '' secondPassword = ' ' # Note that this should be different than password. assert prompt if not prompt[-1].isspace(): prompt += ' ' while True: if useBold: prompt = ansi.BOLD + prompt + ansi.RESET secondPrompt = ansi.BOLD + secondPrompt + ansi.RESET password = getPass(prompt) secondPassword = getPass(secondPrompt) if password != secondPassword: output(_('Passwords don\'t match.')) else: break print('') return password # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/plugin.py0000644000175000017500000001473213233426066015765 0ustar valval00000000000000### # 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. ### import os import sys import imp import os.path import linecache import re from . import callbacks, conf, log, registry installDir = os.path.dirname(sys.modules[__name__].__file__) _pluginsDir = os.path.join(installDir, 'plugins') class Deprecated(ImportError): pass def loadPluginModule(name, ignoreDeprecation=False): """Loads (and returns) the module for the plugin with the given name.""" files = [] pluginDirs = conf.supybot.directories.plugins()[:] pluginDirs.append(_pluginsDir) for dir in pluginDirs: try: files.extend(os.listdir(dir)) except EnvironmentError: # OSError, IOError superclass. log.warning('Invalid plugin directory: %s; removing.', dir) conf.supybot.directories.plugins().remove(dir) if name not in files: search = lambda x: re.search(r'(?i)^%s$' % (name,), x) matched_names = list(filter(search, files)) if len(matched_names) >= 1: name = matched_names[0] else: raise ImportError(name) moduleInfo = imp.find_module(name, pluginDirs) try: module = imp.load_module(name, *moduleInfo) except: sys.modules.pop(name, None) keys = list(sys.modules.keys()) for key in keys: if key.startswith(name + '.'): sys.modules.pop(key) raise if 'deprecated' in module.__dict__ and module.deprecated: if ignoreDeprecation: log.warning('Deprecated plugin loaded: %s', name) else: raise Deprecated(format('Attempted to load deprecated plugin %s', name)) if module.__name__ in sys.modules: sys.modules[module.__name__] = module linecache.checkcache() return module def renameCommand(cb, name, newName): assert not hasattr(cb, newName), 'Cannot rename over existing attributes.' assert newName == callbacks.canonicalName(newName), \ 'newName must already be normalized.' if name != newName: method = getattr(cb.__class__, name) setattr(cb.__class__, newName, method) delattr(cb.__class__, name) def registerRename(plugin, command=None, newName=None): g = conf.registerGlobalValue(conf.supybot.commands.renames, plugin, registry.SpaceSeparatedSetOfStrings([], """Determines what commands in this plugin are to be renamed.""")) if command is not None: g().add(command) v = conf.registerGlobalValue(g, command, registry.String('', '')) if newName is not None: v.setValue(newName) # In case it was already registered. return v else: return g def loadPluginClass(irc, module, register=None): """Loads the plugin Class from the given module into the given Irc.""" try: cb = module.Class(irc) except TypeError as e: s = str(e) if '2 given' in s and '__init__' in s: raise callbacks.Error('In our switch from CVS to Darcs (after 0.80.1), we ' \ 'changed the __init__ for callbacks.Privmsg* to also ' \ 'accept an irc argument. This plugin (%s) is overriding ' \ 'its __init__ method and needs to update its prototype ' \ 'to be \'def __init__(self, irc):\' as well as passing ' \ 'that irc object on to any calls to the plugin\'s ' \ 'parent\'s __init__. Another possible cause: the code in ' \ 'your __init__ raised a TypeError when calling a function ' \ 'or creating an object, which doesn\'t take 2 arguments.' %\ module.__name__) else: raise except AttributeError as e: if 'Class' in str(e): raise callbacks.Error('This plugin module doesn\'t have a "Class" ' \ 'attribute to specify which plugin should be ' \ 'instantiated. If you didn\'t write this ' \ 'plugin, but received it with Supybot, file ' \ 'a bug with us about this error.') else: raise cb.classModule = module plugin = cb.name() public = True if hasattr(cb, 'public'): public = cb.public conf.registerPlugin(plugin, register, public) assert not irc.getCallback(plugin), \ 'There is already a %r plugin registered.' % plugin try: v = registerRename(plugin) renames = conf.supybot.commands.renames.get(plugin)() if renames: for command in renames: v = registerRename(plugin, command) newName = v() assert newName renameCommand(cb, command, newName) else: conf.supybot.commands.renames.unregister(plugin) except registry.NonExistentRegistryEntry as e: pass # The plugin isn't there. irc.addCallback(cb) return cb # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/log.py0000644000175000017500000003726613233426066015257 0ustar valval00000000000000### # 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. ### import os import sys import time import types import atexit import logging import operator import textwrap import traceback from . import ansi, conf, ircutils, registry, utils from .utils import minisix deadlyExceptions = [KeyboardInterrupt, SystemExit] ### # This is for testing, of course. Mostly it just disables the firewall code # so exceptions can propagate. ### testing = False class Formatter(logging.Formatter): _fmtConf = staticmethod(lambda : conf.supybot.log.format()) def formatTime(self, record, datefmt=None): return timestamp(record.created) def formatException(self, exc_info): (E, e, tb) = exc_info for exn in deadlyExceptions: if issubclass(e.__class__, exn): raise return logging.Formatter.formatException(self, (E, e, tb)) def format(self, record): self._fmt = self._fmtConf() if hasattr(self, '_style'): # Python 3 self._style._fmt = self._fmtConf() return logging.Formatter.format(self, record) class PluginFormatter(Formatter): _fmtConf = staticmethod(lambda : conf.supybot.log.plugins.format()) class Logger(logging.Logger): def exception(self, *args): (E, e, tb) = sys.exc_info() tbinfo = traceback.extract_tb(tb) path = '[%s]' % '|'.join(map(operator.itemgetter(2), tbinfo)) eStrId = '%s:%s' % (E, path) eId = hex(hash(eStrId) & 0xFFFFF) logging.Logger.exception(self, *args) self.error('Exception id: %s', eId) self.debug('%s', utils.python.collect_extra_debug_data()) # The traceback should be sufficient if we want it. # self.error('Exception string: %s', eStrId) def _log(self, level, msg, args, exc_info=None, extra=None): msg = format(msg, *args) logging.Logger._log(self, level, msg, (), exc_info=exc_info, extra=extra) class StdoutStreamHandler(logging.StreamHandler): def format(self, record): s = logging.StreamHandler.format(self, record) if record.levelname != 'ERROR' and conf.supybot.log.stdout.wrap(): # We check for ERROR there because otherwise, tracebacks (which are # already wrapped by Python itself) wrap oddly. if not isinstance(record.levelname, minisix.string_types): print(record) print(record.levelname) print(utils.stackTrace()) prefixLen = len(record.levelname) + 1 # ' ' s = textwrap.fill(s, width=78, subsequent_indent=' '*prefixLen) s.rstrip('\r\n') return s def emit(self, record): if conf.supybot.log.stdout() and not conf.daemonized: try: logging.StreamHandler.emit(self, record) except ValueError: # Raised if sys.stdout is closed. self.disable() error('Error logging to stdout. Removing stdout handler.') exception('Uncaught exception in StdoutStreamHandler:') def disable(self): self.setLevel(sys.maxsize) # Just in case. _logger.removeHandler(self) logging._acquireLock() try: del logging._handlers[self] finally: logging._releaseLock() class BetterFileHandler(logging.FileHandler): def emit(self, record): msg = self.format(record) try: self.stream.write(msg) except (UnicodeError, TypeError): try: self.stream.write(msg.encode("utf8")) except (UnicodeError, TypeError): try: self.stream.write(msg.encode("utf8").decode('ascii', 'replace')) except (UnicodeError, TypeError): self.stream.write(repr(msg)) self.stream.write(os.linesep) try: self.flush() except OSError as e: if e.args[0] == 28: print('No space left on device, cannot flush log.') else: raise class ColorizedFormatter(Formatter): # This was necessary because these variables aren't defined until later. # The staticmethod is necessary because they get treated like methods. _fmtConf = staticmethod(lambda : conf.supybot.log.stdout.format()) def formatException(self, exc_info): (E, e, tb) = exc_info if conf.supybot.log.stdout.colorized(): return ''.join([ansi.RED, Formatter.formatException(self, (E, e, tb)), ansi.RESET]) else: return Formatter.formatException(self, (E, e, tb)) def format(self, record, *args, **kwargs): if conf.supybot.log.stdout.colorized(): color = '' if record.levelno == logging.CRITICAL: color = ansi.WHITE + ansi.BOLD elif record.levelno == logging.ERROR: color = ansi.RED elif record.levelno == logging.WARNING: color = ansi.YELLOW if color: return ''.join([color, Formatter.format(self, record, *args, **kwargs), ansi.RESET]) else: return Formatter.format(self, record, *args, **kwargs) else: return Formatter.format(self, record, *args, **kwargs) _logDir = conf.supybot.directories.log() if not os.path.exists(_logDir): os.mkdir(_logDir, 0o755) pluginLogDir = os.path.join(_logDir, 'plugins') if not os.path.exists(pluginLogDir): os.mkdir(pluginLogDir, 0o755) try: messagesLogFilename = os.path.join(_logDir, 'messages.log') _handler = BetterFileHandler(messagesLogFilename, encoding='utf8') except EnvironmentError as e: raise SystemExit('Error opening messages logfile (%s). ' \ 'Generally, this is because you are running Supybot in a directory ' \ 'you don\'t have permissions to add files in, or you\'re running ' \ 'Supybot as a different user than you normally do. The original ' \ 'error was: %s' % (messagesLogFilename, utils.gen.exnToString(e))) # These are public. formatter = Formatter('NEVER SEEN; IF YOU SEE THIS, FILE A BUG!') pluginFormatter = PluginFormatter('NEVER SEEN; IF YOU SEE THIS, FILE A BUG!') # These are not. logging.setLoggerClass(Logger) _logger = logging.getLogger('supybot') _stdoutHandler = StdoutStreamHandler(sys.stdout) class ValidLogLevel(registry.String): """Invalid log level.""" handler = None minimumLevel = -1 def set(self, s): s = s.upper() try: try: level = logging._levelNames[s] except AttributeError: level = logging._nameToLevel[s] except KeyError: try: level = int(s) except ValueError: self.error() if level < self.minimumLevel: self.error() if self.handler is not None: self.handler.setLevel(level) self.setValue(level) def __str__(self): # The str() is necessary here; apparently getLevelName returns an # integer on occasion. logging-- level = str(logging.getLevelName(self.value)) if level.startswith('Level'): level = level.split()[-1] return level class LogLevel(ValidLogLevel): """Invalid log level. Value must be either DEBUG, INFO, WARNING, ERROR, or CRITICAL.""" handler = _handler class StdoutLogLevel(ValidLogLevel): """Invalid log level. Value must be either DEBUG, INFO, WARNING, ERROR, or CRITICAL.""" handler = _stdoutHandler conf.registerGroup(conf.supybot, 'log') conf.registerGlobalValue(conf.supybot.log, 'format', registry.String('%(levelname)s %(asctime)s %(name)s %(message)s', """Determines what the bot's logging format will be. The relevant documentation on the available formattings is Python's documentation on its logging module.""")) conf.registerGlobalValue(conf.supybot.log, 'level', LogLevel(logging.INFO, """Determines what the minimum priority level logged to file will be. Do note that this value does not affect the level logged to stdout; for that, you should set the value of supybot.log.stdout.level. Valid values are DEBUG, INFO, WARNING, ERROR, and CRITICAL, in order of increasing priority.""")) conf.registerGlobalValue(conf.supybot.log, 'timestampFormat', registry.String('%Y-%m-%dT%H:%M:%S', """Determines the format string for timestamps in logfiles. Refer to the Python documentation for the time module to see what formats are accepted. If you set this variable to the empty string, times will be logged in a simple seconds-since-epoch format.""")) class BooleanRequiredFalseOnWindows(registry.Boolean): """Value cannot be true on Windows""" def set(self, s): registry.Boolean.set(self, s) if self.value and os.name == 'nt': self.error() conf.registerGlobalValue(conf.supybot.log, 'stdout', registry.Boolean(True, """Determines whether the bot will log to stdout.""")) conf.registerGlobalValue(conf.supybot.log.stdout, 'colorized', BooleanRequiredFalseOnWindows(False, """Determines whether the bot's logs to stdout (if enabled) will be colorized with ANSI color.""")) conf.registerGlobalValue(conf.supybot.log.stdout, 'wrap', registry.Boolean(False, """Determines whether the bot will wrap its logs when they're output to stdout.""")) conf.registerGlobalValue(conf.supybot.log.stdout, 'format', registry.String('%(levelname)s %(asctime)s %(message)s', """Determines what the bot's logging format will be. The relevant documentation on the available formattings is Python's documentation on its logging module.""")) conf.registerGlobalValue(conf.supybot.log.stdout, 'level', StdoutLogLevel(logging.INFO, """Determines what the minimum priority level logged will be. Valid values are DEBUG, INFO, WARNING, ERROR, and CRITICAL, in order of increasing priority.""")) conf.registerGroup(conf.supybot.log, 'plugins') conf.registerGlobalValue(conf.supybot.log.plugins, 'individualLogfiles', registry.Boolean(False, """Determines whether the bot will separate plugin logs into their own individual logfiles.""")) conf.registerGlobalValue(conf.supybot.log.plugins, 'format', registry.String('%(levelname)s %(asctime)s %(message)s', """Determines what the bot's logging format will be. The relevant documentation on the available formattings is Python's documentation on its logging module.""")) # These just make things easier. debug = _logger.debug info = _logger.info warning = _logger.warning error = _logger.error critical = _logger.critical exception = _logger.exception # These were just begging to be replaced. registry.error = error registry.exception = exception setLevel = _logger.setLevel atexit.register(logging.shutdown) # ircutils will work without this, but it's useful. ircutils.debug = debug def getPluginLogger(name): if not conf.supybot.log.plugins.individualLogfiles(): return _logger log = logging.getLogger('supybot.plugins.%s' % name) if not log.handlers: filename = os.path.join(pluginLogDir, '%s.log' % name) handler = BetterFileHandler(filename) handler.setLevel(-1) handler.setFormatter(pluginFormatter) log.addHandler(handler) if name in sys.modules: log.info('Starting log for %s.', name) return log def timestamp(when=None): if when is None: when = time.time() format = conf.supybot.log.timestampFormat() t = time.localtime(when) if format: return time.strftime(format, t) else: return str(int(time.mktime(t))) def firewall(f, errorHandler=None): def logException(self, s=None): if s is None: s = 'Uncaught exception' if hasattr(self, 'log'): logging_function = self.log.exception else: logging_function = exception logging_function('%s in %s.%s:', s, self.__class__.__name__, f.__name__) def m(self, *args, **kwargs): try: return f(self, *args, **kwargs) except Exception: if testing: raise logException(self) if errorHandler is not None: try: return errorHandler(self, *args, **kwargs) except Exception: logException(self, 'Uncaught exception in errorHandler') m = utils.python.changeFunctionName(m, f.__name__, f.__doc__) return m class MetaFirewall(type): def __new__(cls, name, bases, classdict): firewalled = {} for base in bases: if hasattr(base, '__firewalled__'): cls.updateFirewalled(firewalled, base.__firewalled__) cls.updateFirewalled(firewalled, classdict.get('__firewalled__', [])) for (attr, errorHandler) in firewalled.items(): if attr in classdict: classdict[attr] = firewall(classdict[attr], errorHandler) return super(MetaFirewall, cls).__new__(cls, name, bases, classdict) def getErrorHandler(cls, dictOrTuple, name): if isinstance(dictOrTuple, dict): return dictOrTuple[name] else: return None getErrorHandler = classmethod(getErrorHandler) def updateFirewalled(cls, firewalled, __firewalled__): for attr in __firewalled__: firewalled[attr] = cls.getErrorHandler(__firewalled__, attr) updateFirewalled = classmethod(updateFirewalled) Firewalled = MetaFirewall('Firewalled', (), {}) class PluginLogFilter(logging.Filter): def filter(self, record): if conf.supybot.log.plugins.individualLogfiles(): if record.name.startswith('supybot.plugins'): return False return True _handler.setFormatter(formatter) _handler.addFilter(PluginLogFilter()) _handler.setLevel(conf.supybot.log.level()) _logger.addHandler(_handler) _logger.setLevel(-1) _stdoutFormatter = ColorizedFormatter('IF YOU SEE THIS, FILE A BUG!') _stdoutHandler.setFormatter(_stdoutFormatter) _stdoutHandler.setLevel(conf.supybot.log.stdout.level()) if not conf.daemonized: _logger.addHandler(_stdoutHandler) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/ircutils.py0000644000175000017500000010643113233426066016323 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009,2011,2015 James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Provides a great number of useful utility functions for IRC. Things to muck around with hostmasks, set bold or color on strings, IRC-case-insensitive dicts, a nick class to handle nicks (so comparisons and hashing and whatnot work in an IRC-case-insensitive fashion), and numerous other things. """ from __future__ import division from __future__ import print_function import re import sys import time import base64 import random import string import textwrap import functools from . import utils from .utils import minisix from .version import version from .i18n import PluginInternationalization _ = PluginInternationalization() def debug(s, *args): """Prints a debug string. Most likely replaced by our logging debug.""" print('***', s % args) userHostmaskRe = re.compile(r'^\S+!\S+@\S+$') def isUserHostmask(s): """Returns whether or not the string s is a valid User hostmask.""" return userHostmaskRe.match(s) is not None def isServerHostmask(s): """s => bool Returns True if s is a valid server hostmask.""" return not isUserHostmask(s) def nickFromHostmask(hostmask): """hostmask => nick Returns the nick from a user hostmask.""" assert isUserHostmask(hostmask) return splitHostmask(hostmask)[0] def userFromHostmask(hostmask): """hostmask => user Returns the user from a user hostmask.""" assert isUserHostmask(hostmask) return splitHostmask(hostmask)[1] def hostFromHostmask(hostmask): """hostmask => host Returns the host from a user hostmask.""" assert isUserHostmask(hostmask) return splitHostmask(hostmask)[2] def splitHostmask(hostmask): """hostmask => (nick, user, host) Returns the nick, user, host of a user hostmask.""" assert isUserHostmask(hostmask) nick, rest = hostmask.rsplit('!', 1) user, host = rest.rsplit('@', 1) return (minisix.intern(nick), minisix.intern(user), minisix.intern(host)) def joinHostmask(nick, ident, host): """nick, user, host => hostmask Joins the nick, ident, host into a user hostmask.""" assert nick and ident and host return minisix.intern('%s!%s@%s' % (nick, ident, host)) _rfc1459trans = utils.str.MultipleReplacer(dict(list(zip( string.ascii_uppercase + r'\[]~', string.ascii_lowercase + r'|{}^')))) def toLower(s, casemapping=None): """s => s Returns the string s lowered according to IRC case rules.""" if casemapping is None or casemapping == 'rfc1459': return _rfc1459trans(s) elif casemapping == 'ascii': # freenode return s.lower() else: raise ValueError('Invalid casemapping: %r' % casemapping) def strEqual(nick1, nick2): """s1, s2 => bool Returns True if nick1 == nick2 according to IRC case rules.""" assert isinstance(nick1, minisix.string_types) assert isinstance(nick2, minisix.string_types) return toLower(nick1) == toLower(nick2) nickEqual = strEqual _nickchars = r'[]\`_^{|}' nickRe = re.compile(r'^[A-Za-z%s][-0-9A-Za-z%s]*$' % (re.escape(_nickchars), re.escape(_nickchars))) def isNick(s, strictRfc=True, nicklen=None): """s => bool Returns True if s is a valid IRC nick.""" if strictRfc: ret = bool(nickRe.match(s)) if ret and nicklen is not None: ret = len(s) <= nicklen return ret else: return not isChannel(s) and \ not isUserHostmask(s) and \ not ' ' in s and not '!' in s def areNicks(s, strictRfc=True, nicklen=None): """Like 'isNick(x)' but for comma-separated list.""" nick = functools.partial(isNick, strictRfc=strictRfc, nicklen=nicklen) return all(map(nick, s.split(','))) def isChannel(s, chantypes='#&+!', channellen=50): """s => bool Returns True if s is a valid IRC channel name.""" return s and \ ',' not in s and \ '\x07' not in s and \ s[0] in chantypes and \ len(s) <= channellen and \ len(s.split(None, 1)) == 1 def areChannels(s, chantypes='#&+!',channellen=50): """Like 'isChannel(x)' but for comma-separated list.""" chan = functools.partial(isChannel, chantypes=chantypes, channellen=channellen) return all(map(chan, s.split(','))) def areReceivers(s, strictRfc=True, nicklen=None, chantypes='#&+!', channellen=50): """Like 'isNick(x) or isChannel(x)' but for comma-separated list.""" nick = functools.partial(isNick, strictRfc=strictRfc, nicklen=nicklen) chan = functools.partial(isChannel, chantypes=chantypes, channellen=channellen) return all([nick(x) or chan(x) for x in s.split(',')]) _patternCache = utils.structures.CacheDict(1000) def _hostmaskPatternEqual(pattern, hostmask): try: return _patternCache[pattern](hostmask) is not None except KeyError: # We make our own regexps, rather than use fnmatch, because fnmatch's # case-insensitivity is not IRC's case-insensitity. fd = minisix.io.StringIO() for c in pattern: if c == '*': fd.write('.*') elif c == '?': fd.write('.') elif c in '[{': fd.write('[[{]') elif c in '}]': fd.write(r'[}\]]') elif c in '|\\': fd.write(r'[|\\]') elif c in '^~': fd.write('[~^]') else: fd.write(re.escape(c)) fd.write('$') f = re.compile(fd.getvalue(), re.I).match _patternCache[pattern] = f return f(hostmask) is not None _hostmaskPatternEqualCache = utils.structures.CacheDict(1000) def hostmaskPatternEqual(pattern, hostmask): """pattern, hostmask => bool Returns True if hostmask matches the hostmask pattern pattern.""" try: return _hostmaskPatternEqualCache[(pattern, hostmask)] except KeyError: b = _hostmaskPatternEqual(pattern, hostmask) _hostmaskPatternEqualCache[(pattern, hostmask)] = b return b def banmask(hostmask): """Returns a properly generic banning hostmask for a hostmask. >>> banmask('nick!user@host.domain.tld') '*!*@*.domain.tld' >>> banmask('nick!user@10.0.0.1') '*!*@10.0.0.*' """ assert isUserHostmask(hostmask) host = hostFromHostmask(hostmask) if utils.net.isIPV4(host): L = host.split('.') L[-1] = '*' return '*!*@' + '.'.join(L) elif utils.net.isIPV6(host): L = host.split(':') L[-1] = '*' return '*!*@' + ':'.join(L) else: if len(host.split('.')) > 2: # If it is a subdomain return '*!*@*%s' % host[host.find('.'):] else: return '*!*@' + host _plusRequireArguments = 'ovhblkqeI' _minusRequireArguments = 'ovhbkqeI' def separateModes(args): """Separates modelines into single mode change tuples. Basically, you should give it the .args of a MODE IrcMsg. Examples: >>> separateModes(['+ooo', 'jemfinch', 'StoneTable', 'philmes']) [('+o', 'jemfinch'), ('+o', 'StoneTable'), ('+o', 'philmes')] >>> separateModes(['+o-o', 'jemfinch', 'PeterB']) [('+o', 'jemfinch'), ('-o', 'PeterB')] >>> separateModes(['+s-o', 'test']) [('+s', None), ('-o', 'test')] >>> separateModes(['+sntl', '100']) [('+s', None), ('+n', None), ('+t', None), ('+l', 100)] """ if not args: return [] modes = args[0] assert modes[0] in '+-', 'Invalid args: %r' % args args = list(args[1:]) ret = [] for c in modes: if c in '+-': last = c else: if last == '+': requireArguments = _plusRequireArguments else: requireArguments = _minusRequireArguments if c in requireArguments: if not args: # It happens, for example with "MODE #channel +b", which # is used for getting the list of all bans. continue arg = args.pop(0) try: arg = int(arg) except ValueError: pass ret.append((last + c, arg)) else: ret.append((last + c, None)) return ret def joinModes(modes): """[(mode, targetOrNone), ...] => args Joins modes of the same form as returned by separateModes.""" args = [] modeChars = [] currentMode = '\x00' for (mode, arg) in modes: if arg is not None: args.append(arg) if not mode.startswith(currentMode): currentMode = mode[0] modeChars.append(mode[0]) modeChars.append(mode[1]) args.insert(0, ''.join(modeChars)) return args def bold(s): """Returns the string s, bolded.""" return '\x02%s\x02' % s def italic(s): """Returns the string s, italicised.""" return '\x1D%s\x1D' % s def reverse(s): """Returns the string s, reverse-videoed.""" return '\x16%s\x16' % s def underline(s): """Returns the string s, underlined.""" return '\x1F%s\x1F' % s # Definition of mircColors dictionary moved below because it became an IrcDict. def mircColor(s, fg=None, bg=None): """Returns s with the appropriate mIRC color codes applied.""" if fg is None and bg is None: return s elif bg is None: if str(fg) in mircColors: fg = mircColors[str(fg)] elif len(str(fg)) > 1: fg = mircColors[str(fg)[:-1]] else: # Should not happen pass return '\x03%s%s\x03' % (fg.zfill(2), s) elif fg is None: bg = mircColors[str(bg)] # According to the mirc color doc, a fg color MUST be specified if a # background color is specified. So, we'll specify 00 (white) if the # user doesn't specify one. return '\x0300,%s%s\x03' % (bg.zfill(2), s) else: fg = mircColors[str(fg)] bg = mircColors[str(bg)] # No need to zfill fg because the comma delimits. return '\x03%s,%s%s\x03' % (fg, bg.zfill(2), s) def canonicalColor(s, bg=False, shift=0): """Assigns an (fg, bg) canonical color pair to a string based on its hash value. This means it might change between Python versions. This pair can be used as a *parameter to mircColor. The shift parameter is how much to right-shift the hash value initially. """ h = hash(s) >> shift fg = h % 14 + 2 # The + 2 is to rule out black and white. if bg: bg = (h >> 4) & 3 # The 5th, 6th, and 7th least significant bits. if fg < 8: bg += 8 else: bg += 2 return (fg, bg) else: return (fg, None) def stripBold(s): """Returns the string s, with bold removed.""" return s.replace('\x02', '') def stripItalic(s): """Returns the string s, with italics removed.""" return s.replace('\x1d', '') _stripColorRe = re.compile(r'\x03(?:\d{1,2},\d{1,2}|\d{1,2}|,\d{1,2}|)') def stripColor(s): """Returns the string s, with color removed.""" return _stripColorRe.sub('', s) def stripReverse(s): """Returns the string s, with reverse-video removed.""" return s.replace('\x16', '') def stripUnderline(s): """Returns the string s, with underlining removed.""" return s.replace('\x1f', '') def stripFormatting(s): """Returns the string s, with all formatting removed.""" # stripColor has to go first because of some strings, check the tests. s = stripColor(s) s = stripBold(s) s = stripReverse(s) s = stripUnderline(s) s = stripItalic(s) return s.replace('\x0f', '') _containsFormattingRe = re.compile(r'[\x02\x03\x16\x1f]') def formatWhois(irc, replies, caller='', channel='', command='whois'): """Returns a string describing the target of a WHOIS command. Arguments are: * irc: the irclib.Irc object on which the replies was received * replies: a dict mapping the reply codes ('311', '312', etc.) to their corresponding ircmsg.IrcMsg * caller: an optional nick specifying who requested the whois information * channel: an optional channel specifying where the reply will be sent If provided, caller and channel will be used to avoid leaking information that the caller/channel shouldn't be privy to. """ hostmask = '@'.join(replies['311'].args[2:4]) nick = replies['318'].args[1] user = replies['311'].args[-1] START_CODE = '311' if command == 'whois' else '314' hostmask = '@'.join(replies[START_CODE].args[2:4]) user = replies[START_CODE].args[-1] if _containsFormattingRe.search(user) and user[-1] != '\x0f': # For good measure, disable any formatting user = '%s\x0f' % user if '319' in replies: channels = [] for msg in replies['319']: channels.extend(msg.args[-1].split()) ops = [] voices = [] normal = [] halfops = [] for chan in channels: origchan = chan chan = chan.lstrip('@%+~!') # UnrealIRCd uses & for user modes and disallows it as a # channel-prefix, flying in the face of the RFC. Have to # handle this specially when processing WHOIS response. testchan = chan.lstrip('&') if testchan != chan and irc.isChannel(testchan): chan = testchan diff = len(chan) - len(origchan) modes = origchan[:diff] chanState = irc.state.channels.get(chan) # The user is in a channel the bot is in, so the ircd may have # responded with otherwise private data. if chanState: # Skip channels the caller isn't in. This prevents # us from leaking information when the channel is +s or the # target is +i. if caller not in chanState.users: continue # Skip +s/+p channels the target is in only if the reply isn't # being sent to that channel. if set(('p', 's')) & set(chanState.modes.keys()) and \ not strEqual(channel or '', chan): continue if not modes: normal.append(chan) elif utils.iter.any(lambda c: c in modes,('@', '&', '~', '!')): ops.append(chan) elif utils.iter.any(lambda c: c in modes, ('%',)): halfops.append(chan) elif utils.iter.any(lambda c: c in modes, ('+',)): voices.append(chan) L = [] if ops: L.append(format(_('is an op on %L'), ops)) if halfops: L.append(format(_('is a halfop on %L'), halfops)) if voices: L.append(format(_('is voiced on %L'), voices)) if normal: if L: L.append(format(_('is also on %L'), normal)) else: L.append(format(_('is on %L'), normal)) else: if command == 'whois': L = [_('isn\'t on any publicly visible channels')] else: L = [] channels = format('%L', L) if '317' in replies: idle = utils.timeElapsed(replies['317'].args[2]) signon = utils.str.timestamp(float(replies['317'].args[3])) else: idle = '' signon = '' if '312' in replies: server = replies['312'].args[2] if len(replies['312']) > 3: signoff = replies['312'].args[3] else: server = '' if '301' in replies: away = ' %s is away: %s.' % (nick, replies['301'].args[2]) else: away = '' if '320' in replies: if replies['320'].args[2]: identify = ' identified' else: identify = '' else: identify = '' if command == 'whois': s = _('%s (%s) has been%s on server %s since %s (idle for %s). %s ' '%s.%s') % (user, hostmask, identify, server, signon, idle, nick, channels, away) else: s = _('%s (%s) has been%s on server %s and disconnected on %s.') % \ (user, hostmask, identify, server, signoff) return s class FormatContext(object): def __init__(self): self.reset() def reset(self): self.fg = None self.bg = None self.bold = False self.reverse = False self.underline = False def start(self, s): """Given a string, starts all the formatters in this context.""" if self.bold: s = '\x02' + s if self.reverse: s = '\x16' + s if self.underline: s = '\x1f' + s if self.fg is not None or self.bg is not None: s = mircColor(s, fg=self.fg, bg=self.bg)[:-1] # Remove \x03. return s def end(self, s): """Given a string, ends all the formatters in this context.""" if self.bold or self.reverse or \ self.fg or self.bg or self.underline: # Should we individually end formatters? s += '\x0f' return s class FormatParser(object): def __init__(self, s): self.fd = minisix.io.StringIO(s) self.last = None def getChar(self): if self.last is not None: c = self.last self.last = None return c else: return self.fd.read(1) def ungetChar(self, c): self.last = c def parse(self): context = FormatContext() c = self.getChar() while c: if c == '\x02': context.bold = not context.bold elif c == '\x16': context.reverse = not context.reverse elif c == '\x1f': context.underline = not context.underline elif c == '\x0f': context.reset() elif c == '\x03': self.getColor(context) c = self.getChar() return context def getInt(self): i = 0 setI = False c = self.getChar() while c.isdigit(): j = i * 10 j += int(c) if j >= 16: self.ungetChar(c) break else: setI = True i = j c = self.getChar() self.ungetChar(c) if setI: return i else: return None def getColor(self, context): context.fg = self.getInt() c = self.getChar() if c == ',': context.bg = self.getInt() else: self.ungetChar(c) def wrap(s, length, break_on_hyphens = False, break_long_words = False): processed = [] wrapper = textwrap.TextWrapper(width=length) wrapper.break_long_words = break_long_words wrapper.break_on_hyphens = break_on_hyphens chunks = wrapper.wrap(s) context = None for chunk in chunks: if context is not None: chunk = context.start(chunk) context = FormatParser(chunk).parse() processed.append(context.end(chunk)) return processed def isValidArgument(s): """Returns whether s is strictly a valid argument for an IRC message.""" return '\r' not in s and '\n' not in s and '\x00' not in s def safeArgument(s): """If s is unsafe for IRC, returns a safe version.""" if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf-8') elif (minisix.PY2 and not isinstance(s, minisix.string_types)) or \ (minisix.PY3 and not isinstance(s, str)): debug('Got a non-string in safeArgument: %r', s) s = str(s) if isValidArgument(s): return s else: return repr(s) def replyTo(msg): """Returns the appropriate target to send responses to msg.""" if isChannel(msg.args[0]): return msg.args[0] else: return msg.nick def dccIP(ip): """Converts an IP string to the DCC integer form.""" assert utils.net.isIPV4(ip), \ 'argument must be a string ip in xxx.yyy.zzz.www format.' i = 0 x = 256**3 for quad in ip.split('.'): i += int(quad)*x x //= 256 return i def unDccIP(i): """Takes an integer DCC IP and return a normal string IP.""" assert isinstance(i, minisix.integer_types), '%r is not an number.' % i L = [] while len(L) < 4: L.append(i % 256) i //= 256 L.reverse() return '.'.join(map(str, L)) class IrcString(str): """This class does case-insensitive comparison and hashing of nicks.""" def __new__(cls, s=''): x = super(IrcString, cls).__new__(cls, s) x.lowered = str(toLower(x)) return x def __eq__(self, s): try: return toLower(s) == self.lowered except: return False def __ne__(self, s): return not (self == s) def __hash__(self): return hash(self.lowered) class IrcDict(utils.InsensitivePreservingDict): """Subclass of dict to make key comparison IRC-case insensitive.""" def key(self, s): if s is not None: s = toLower(s) return s class CallableValueIrcDict(IrcDict): def __getitem__(self, k): v = super(IrcDict, self).__getitem__(k) if callable(v): v = v() return v class IrcSet(utils.NormalizingSet): """A sets.Set using IrcStrings instead of regular strings.""" def normalize(self, s): return IrcString(s) def __reduce__(self): return (self.__class__, (list(self),)) class FloodQueue(object): timeout = 0 def __init__(self, timeout=None, queues=None): if timeout is not None: self.timeout = timeout if queues is None: queues = IrcDict() self.queues = queues def __repr__(self): return 'FloodQueue(timeout=%r, queues=%s)' % (self.timeout, repr(self.queues)) def key(self, msg): # This really ought to be configurable without subclassing, but for # now, it works. # used to be msg.user + '@' + msg.host but that was too easily abused. return msg.host def getTimeout(self): if callable(self.timeout): return self.timeout() else: return self.timeout def _getQueue(self, msg, insert=True): key = self.key(msg) try: return self.queues[key] except KeyError: if insert: # python-- # instancemethod.__repr__ calls the instance.__repr__, which # means that our __repr__ calls self.queues.__repr__, which # calls structures.TimeoutQueue.__repr__, which calls # getTimeout.__repr__, which calls our __repr__, which calls... getTimeout = lambda : self.getTimeout() q = utils.structures.TimeoutQueue(getTimeout) self.queues[key] = q return q else: return None def enqueue(self, msg, what=None): if what is None: what = msg q = self._getQueue(msg) q.enqueue(what) def len(self, msg): q = self._getQueue(msg, insert=False) if q is not None: return len(q) else: return 0 def has(self, msg, what=None): q = self._getQueue(msg, insert=False) if q is not None: if what is None: what = msg for elt in q: if elt == what: return True return False mircColors = IrcDict({ 'white': '0', 'black': '1', 'blue': '2', 'green': '3', 'red': '4', 'brown': '5', 'purple': '6', 'orange': '7', 'yellow': '8', 'light green': '9', 'teal': '10', 'light blue': '11', 'dark blue': '12', 'pink': '13', 'dark grey': '14', 'light grey': '15', 'dark gray': '14', 'light gray': '15', }) # We'll map integers to their string form so mircColor is simpler. for (k, v) in list(mircColors.items()): if k is not None: # Ignore empty string for None. sv = str(v) mircColors[sv] = sv mircColors[sv.zfill(2)] = sv def standardSubstitute(irc, msg, text, env=None): """Do the standard set of substitutions on text, and return it""" def randInt(): return str(random.randint(-1000, 1000)) def randDate(): t = pow(2,30)*random.random()+time.time()/4.0 return time.ctime(t) ctime = time.strftime("%a %b %d %H:%M:%S %Y") localtime = time.localtime() gmtime = time.strftime("%a %b %d %H:%M:%S %Y", time.gmtime()) vars = CallableValueIrcDict({ 'now': ctime, 'ctime': ctime, 'utc': gmtime, 'gmt': gmtime, 'randdate': randDate, 'randomdate': randDate, 'rand': randInt, 'randint': randInt, 'randomint': randInt, 'today': time.strftime('%d %b %Y', localtime), 'year': localtime[0], 'month': localtime[1], 'monthname': time.strftime('%b', localtime), 'date': localtime[2], 'day': time.strftime('%A', localtime), 'h': localtime[3], 'hr': localtime[3], 'hour': localtime[3], 'm': localtime[4], 'min': localtime[4], 'minute': localtime[4], 's': localtime[5], 'sec': localtime[5], 'second': localtime[5], 'tz': time.strftime('%Z', localtime), 'version': 'Limnoria %s' % version, }) if irc: vars.update({ 'botnick': irc.nick, 'network': irc.network, }) if msg: vars.update({ 'who': msg.nick, 'nick': msg.nick, 'user': msg.user, 'host': msg.host, }) if msg.reply_env: vars.update(msg.reply_env) if irc and msg: if isChannel(msg.args[0]): channel = msg.args[0] else: channel = 'somewhere' def randNick(): if channel != 'somewhere': L = list(irc.state.channels[channel].users) if len(L) > 1: n = msg.nick while n == msg.nick: n = utils.iter.choice(L) return n else: return msg.nick else: return 'someone' vars.update({ 'randnick': randNick, 'randomnick': randNick, 'channel': channel, }) else: vars.update({ 'channel': 'somewhere', 'randnick': 'someone', 'randomnick': 'someone', }) if env is not None: vars.update(env) t = string.Template(text) t.idpattern = '[a-zA-Z][a-zA-Z0-9]*' return t.safe_substitute(vars) AUTHENTICATE_CHUNK_SIZE = 400 def authenticate_generator(authstring, base64ify=True): if base64ify: authstring = base64.b64encode(authstring) if minisix.PY3: authstring = authstring.decode() # +1 so we get an empty string at the end if len(authstring) is a multiple # of AUTHENTICATE_CHUNK_SIZE (including 0) for n in range(0, len(authstring)+1, AUTHENTICATE_CHUNK_SIZE): chunk = authstring[n:n+AUTHENTICATE_CHUNK_SIZE] or '+' yield chunk class AuthenticateDecoder(object): def __init__(self): self.chunks = [] self.ready = False def feed(self, msg): assert msg.command == 'AUTHENTICATE' chunk = msg.args[0] if chunk == '+' or len(chunk) != AUTHENTICATE_CHUNK_SIZE: self.ready = True if chunk != '+': if minisix.PY3: chunk = chunk.encode() self.chunks.append(chunk) def get(self): assert self.ready return base64.b64decode(b''.join(self.chunks)) numerics = { # <= 2.10 # Reply '001': 'RPL_WELCOME', '002': 'RPL_YOURHOST', '003': 'RPL_CREATED', '004': 'RPL_MYINFO', '005': 'RPL_BOUNCE', '302': 'RPL_USERHOST', '303': 'RPL_ISON', '301': 'RPL_AWAY', '305': 'RPL_UNAWAY', '306': 'RPL_NOWAWAY', '311': 'RPL_WHOISUSER', '312': 'RPL_WHOISSERVER', '313': 'RPL_WHOISOPERATOR', '317': 'RPL_WHOISIDLE', '318': 'RPL_ENDOFWHOIS', '319': 'RPL_WHOISCHANNELS', '314': 'RPL_WHOWASUSER', '369': 'RPL_ENDOFWHOWAS', '321': 'RPL_LISTSTART', '322': 'RPL_LIST', '323': 'RPL_LISTEND', '325': 'RPL_UNIQOPIS', '324': 'RPL_CHANNELMODEIS', '331': 'RPL_NOTOPIC', '332': 'RPL_TOPIC', '341': 'RPL_INVITING', '342': 'RPL_SUMMONING', '346': 'RPL_INVITELIST', '347': 'RPL_ENDOFINVITELIST', '348': 'RPL_EXCEPTLIST', '349': 'RPL_ENDOFEXCEPTLIST', '351': 'RPL_VERSION', '352': 'RPL_WHOREPLY', '352': 'RPL_WHOREPLY', '353': 'RPL_NAMREPLY', '366': 'RPL_ENDOFNAMES', '364': 'RPL_LINKS', '365': 'RPL_ENDOFLINKS', '367': 'RPL_BANLIST', '368': 'RPL_ENDOFBANLIST', '371': 'RPL_INFO', '374': 'RPL_ENDOFINFO', '372': 'RPL_MOTD', '376': 'RPL_ENDOFMOTD', '381': 'RPL_YOUREOPER', '382': 'RPL_REHASHING', '383': 'RPL_YOURESERVICE', '391': 'RPL_TIME', '392': 'RPL_USERSSTART', '393': 'RPL_USERS', '394': 'RPL_ENDOFUSERS', '395': 'RPL_NOUSERS', '200': 'RPL_TRACELINK', '201': 'RPL_TRACECONNECTING', '202': 'RPL_TRACEHANDSHAKE', '203': 'RPL_TRACEUNKNOWN', '204': 'RPL_TRACEOPERATOR', '205': 'RPL_TRACEUSER', '206': 'RPL_TRACESERVER', '207': 'RPL_TRACESERVICE', '208': 'RPL_TRACENEWTYPE', '209': 'RPL_TRACECLASS', '210': 'RPL_TRACERECONNECT', '261': 'RPL_TRACELOG', '262': 'RPL_TRACEEND', '211': 'RPL_STATSLINKINFO', '212': 'RPL_STATSCOMMANDS', '219': 'RPL_ENDOFSTATS', '242': 'RPL_STATSUPTIME', '243': 'RPL_STATSOLINE', '221': 'RPL_UMODEIS', '234': 'RPL_SERVLIST', '235': 'RPL_SERVLISTEND', '251': 'RPL_LUSERCLIENT', '252': 'RPL_LUSEROP', '253': 'RPL_LUSERUNKNOWN', '254': 'RPL_LUSERCHANNELS', '255': 'RPL_LUSERME', '256': 'RPL_ADMINME', '257': 'RPL_ADMINLOC1', '258': 'RPL_ADMINLOC2', '259': 'RPL_ADMINEMAIL', '263': 'RPL_TRYAGAIN', # Error '401': 'ERR_NOSUCHNICK', '402': 'ERR_NOSUCHSERVER', '403': 'ERR_NOSUCHCHANNEL', '404': 'ERR_CANNOTSENDTOCHAN', '405': 'ERR_TOOMANYCHANNELS', '406': 'ERR_WASNOSUCHNICK', '407': 'ERR_TOOMANYTARGETS', '408': 'ERR_NOSUCHSERVICE', '409': 'ERR_NOORIGIN', '411': 'ERR_NORECIPIENT', '412': 'ERR_NOTEXTTOSEND', '413': 'ERR_NOTOPLEVEL', '414': 'ERR_WILDTOPLEVEL', '415': 'ERR_BADMASK', '421': 'ERR_UNKNOWNCOMMAND', '422': 'ERR_NOMOTD', '423': 'ERR_NOADMININFO', '424': 'ERR_FILEERROR', '431': 'ERR_NONICKNAMEGIVEN', '432': 'ERR_ERRONEUSNICKNAME', '433': 'ERR_NICKNAMEINUSE', '436': 'ERR_NICKCOLLISION', '437': 'ERR_UNAVAILRESOURCE', '441': 'ERR_USERNOTINCHANNEL', '442': 'ERR_NOTONCHANNEL', '443': 'ERR_USERONCHANNEL', '444': 'ERR_NOLOGIN', '445': 'ERR_SUMMONDISABLED', '446': 'ERR_USERSDISABLED', '451': 'ERR_NOTREGISTERED', '461': 'ERR_NEEDMOREPARAMS', '462': 'ERR_ALREADYREGISTRED', '463': 'ERR_NOPERMFORHOST', '464': 'ERR_PASSWDMISMATCH', '465': 'ERR_YOUREBANNEDCREEP', '466': 'ERR_YOUWILLBEBANNED', '467': 'ERR_KEYSET', '471': 'ERR_CHANNELISFULL', '472': 'ERR_UNKNOWNMODE', '473': 'ERR_INVITEONLYCHAN', '474': 'ERR_BANNEDFROMCHAN', '475': 'ERR_BADCHANNELKEY', '476': 'ERR_BADCHANMASK', '477': 'ERR_NOCHANMODES', '478': 'ERR_BANLISTFULL', '481': 'ERR_NOPRIVILEGES', '482': 'ERR_CHANOPRIVSNEEDED', '483': 'ERR_CANTKILLSERVER', '484': 'ERR_RESTRICTED', '485': 'ERR_UNIQOPPRIVSNEEDED', '491': 'ERR_NOOPERHOST', '501': 'ERR_UMODEUNKNOWNFLAG', '502': 'ERR_USERSDONTMATCH', # Reserved '231': 'RPL_SERVICEINFO', '232': 'RPL_ENDOFSERVICES', '233': 'RPL_SERVICE', '300': 'RPL_NONE', '316': 'RPL_WHOISCHANOP', '361': 'RPL_KILLDONE', '362': 'RPL_CLOSING', '363': 'RPL_CLOSEEND', '373': 'RPL_INFOSTART', '384': 'RPL_MYPORTIS', '213': 'RPL_STATSCLINE', '214': 'RPL_STATSNLINE', '215': 'RPL_STATSILINE', '216': 'RPL_STATSKLINE', '217': 'RPL_STATSQLINE', '218': 'RPL_STATSYLINE', '240': 'RPL_STATSVLINE', '241': 'RPL_STATSLLINE', '244': 'RPL_STATSHLINE', '244': 'RPL_STATSSLINE', '246': 'RPL_STATSPING', '247': 'RPL_STATSBLINE', '250': 'RPL_STATSDLINE', '492': 'ERR_NOSERVICEHOST', # IRC v3.1 # SASL '900': 'RPL_LOGGEDIN', '901': 'RPL_LOGGEDOUT', '902': 'ERR_NICKLOCKED', '903': 'RPL_SASLSUCCESS', '904': 'ERR_SASLFAIL', '905': 'ERR_SASLTOOLONG', '906': 'ERR_SASLABORTED', '907': 'ERR_SASLALREADY', '908': 'RPL_SASLMECHS', # IRC v3.2 # Metadata '760': 'RPL_WHOISKEYVALUE', '761': 'RPL_KEYVALUE', '762': 'RPL_METADATAEND', '764': 'ERR_METADATALIMIT', '765': 'ERR_TARGETINVALID', '766': 'ERR_NOMATCHINGKEY', '767': 'ERR_KEYINVALID', '768': 'ERR_KEYNOTSET', '769': 'ERR_KEYNOPERMISSION', # Monitor '730': 'RPL_MONONLINE', '731': 'RPL_MONOFFLINE', '732': 'RPL_MONLIST', '733': 'RPL_ENDOFMONLIST', '734': 'ERR_MONLISTFULL', } if __name__ == '__main__': import doctest doctest.testmod(sys.modules['__main__']) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/ircmsgs.py0000644000175000017500000010432213233426066016131 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ This module provides the basic IrcMsg object used throughout the bot to represent the actual messages. It also provides several helper functions to construct such messages in an easier way than the constructor for the IrcMsg object (which, as you'll read later, is quite...full-featured :)) """ import re import time import base64 import datetime import warnings import functools from . import conf, ircutils, utils from .utils.iter import all from .utils import minisix ### # IrcMsg class -- used for representing IRC messages acquired from a network. ### class MalformedIrcMsg(ValueError): pass # http://ircv3.net/specs/core/message-tags-3.2.html#escaping-values SERVER_TAG_ESCAPE = [ ('\\', '\\\\'), # \ -> \\ (' ', r'\s'), (';', r'\:'), ('\r', r'\r'), ('\n', r'\n'), ] escape_server_tag_value = utils.str.MultipleReplacer( dict(SERVER_TAG_ESCAPE)) unescape_server_tag_value = utils.str.MultipleReplacer( dict(map(lambda x:(x[1],x[0]), SERVER_TAG_ESCAPE))) def parse_server_tags(s): server_tags = {} for tag in s.split(';'): if '=' not in tag: server_tags[tag] = None else: (key, value) = tag.split('=', 1) server_tags[key] = unescape_server_tag_value(value) return server_tags def format_server_tags(server_tags): parts = [] for (key, value) in server_tags.items(): if value is None: parts.append(key) else: parts.append('%s=%s' % (key, escape_server_tag_value(value))) return '@' + ';'.join(parts) class IrcMsg(object): """Class to represent an IRC message. As usual, ignore attributes that begin with an underscore. They simply don't exist. Instances of this class are *not* to be modified, since they are hashable. Public attributes of this class are .prefix, .command, .args, .nick, .user, and .host. The constructor for this class is pretty intricate. It's designed to take any of three major (sets of) arguments. Called with no keyword arguments, it takes a single string that is a raw IRC message (such as one taken straight from the network). Called with keyword arguments, it *requires* a command parameter. Args is optional, but with most commands will be necessary. Prefix is obviously optional, since clients aren't allowed (well, technically, they are, but only in a completely useless way) to send prefixes to the server. Since this class isn't to be modified, the constructor also accepts a 'msg' keyword argument representing a message from which to take all the attributes not provided otherwise as keyword arguments. So, for instance, if a programmer wanted to take a PRIVMSG they'd gotten and simply redirect it to a different source, they could do this: IrcMsg(prefix='', args=(newSource, otherMsg.args[1]), msg=otherMsg) """ # It's too useful to be able to tag IrcMsg objects with extra, unforeseen # data. Goodbye, __slots__. # On second thought, let's use methods for tagging. __slots__ = ('args', 'command', 'host', 'nick', 'prefix', 'user', '_hash', '_str', '_repr', '_len', 'tags', 'reply_env', 'server_tags', 'time') def __init__(self, s='', command='', args=(), prefix='', msg=None, reply_env=None): assert not (msg and s), 'IrcMsg.__init__ cannot accept both s and msg' if not s and not command and not msg: raise MalformedIrcMsg('IRC messages require a command.') self._str = None self._repr = None self._hash = None self._len = None self.reply_env = reply_env self.tags = {} if s: originalString = s try: if not s.endswith('\n'): s += '\n' self._str = s if s[0] == '@': (server_tags, s) = s.split(' ', 1) self.server_tags = parse_server_tags(server_tags[1:]) else: self.server_tags = {} if s[0] == ':': self.prefix, s = s[1:].split(None, 1) else: self.prefix = '' if ' :' in s: # Note the space: IPV6 addresses are bad w/o it. s, last = s.split(' :', 1) self.args = s.split() self.args.append(last.rstrip('\r\n')) else: self.args = s.split() self.command = self.args.pop(0) if 'time' in self.server_tags: s = self.server_tags['time'] date = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ') date = minisix.make_datetime_utc(date) self.time = minisix.datetime__timestamp(date) else: self.time = time.time() except (IndexError, ValueError): raise MalformedIrcMsg(repr(originalString)) else: if msg is not None: if prefix: self.prefix = prefix else: self.prefix = msg.prefix if command: self.command = command else: self.command = msg.command if args: self.args = args else: self.args = msg.args if reply_env: self.reply_env = reply_env elif msg.reply_env: self.reply_env = msg.reply_env.copy() else: self.reply_env = None self.tags = msg.tags.copy() self.server_tags = msg.server_tags self.time = msg.time else: self.prefix = prefix self.command = command assert all(ircutils.isValidArgument, args), args self.args = args self.time = None self.server_tags = {} self.args = tuple(self.args) if isUserHostmask(self.prefix): (self.nick,self.user,self.host)=ircutils.splitHostmask(self.prefix) else: (self.nick, self.user, self.host) = (self.prefix,)*3 def __str__(self): if self._str is not None: return self._str if self.prefix: if len(self.args) > 1: self._str = ':%s %s %s :%s\r\n' % \ (self.prefix, self.command, ' '.join(self.args[:-1]), self.args[-1]) else: if self.args: self._str = ':%s %s :%s\r\n' % \ (self.prefix, self.command, self.args[0]) else: self._str = ':%s %s\r\n' % (self.prefix, self.command) else: if len(self.args) > 1: self._str = '%s %s :%s\r\n' % \ (self.command, ' '.join(self.args[:-1]), self.args[-1]) else: if self.args: self._str = '%s :%s\r\n' % (self.command, self.args[0]) else: self._str = '%s\r\n' % self.command return self._str def __len__(self): return len(str(self)) def __eq__(self, other): return isinstance(other, self.__class__) and \ hash(self) == hash(other) and \ self.command == other.command and \ self.prefix == other.prefix and \ self.args == other.args __req__ = __eq__ # I don't know exactly what this does, but it can't hurt. def __ne__(self, other): return not (self == other) __rne__ = __ne__ # Likewise as above. def __hash__(self): if self._hash is not None: return self._hash self._hash = hash(self.command) ^ \ hash(self.prefix) ^ \ hash(repr(self.args)) return self._hash def __repr__(self): if self._repr is not None: return self._repr self._repr = format('IrcMsg(prefix=%q, command=%q, args=%r)', self.prefix, self.command, self.args) return self._repr def __reduce__(self): return (self.__class__, (str(self),)) def tag(self, tag, value=True): """Affect a key:value pair to this message.""" self.tags[tag] = value def tagged(self, tag): """Get the value affected to a tag.""" return self.tags.get(tag) # Returns None if it's not there. def __getattr__(self, attr): if attr.startswith('__'): # Since PEP 487, Python calls __set_name__ raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) if attr in self.tags: warnings.warn("msg. is deprecated. Use " "msg.tagged('') or msg.tags['']" "instead.", DeprecationWarning) return self.tags[attr] else: # TODO: make this raise AttributeError return None def isCtcp(msg): """Returns whether or not msg is a CTCP message.""" return msg.command in ('PRIVMSG', 'NOTICE') and \ msg.args[1].startswith('\x01') and \ msg.args[1].endswith('\x01') and \ len(msg.args[1]) >= 2 def isAction(msg): """A predicate returning true if the PRIVMSG in question is an ACTION""" if isCtcp(msg): s = msg.args[1] payload = s[1:-1] # Chop off \x01. command = payload.split(None, 1)[0] return command == 'ACTION' else: return False def isSplit(msg): if msg.command == 'QUIT': # It's a quit. quitmsg = msg.args[0] if not quitmsg.startswith('"') and not quitmsg.endswith('"'): # It's not a user-generated quitmsg. servers = quitmsg.split() if len(servers) == 2: # We could check if domains match, or if the hostnames actually # resolve, but we're going to put that off for now. return True return False _unactionre = re.compile(r'^\x01ACTION\s+(.*)\x01$') def unAction(msg): """Returns the payload (i.e., non-ACTION text) of an ACTION msg.""" assert isAction(msg) return _unactionre.match(msg.args[1]).group(1) def _escape(s): s = s.replace('&', '&') s = s.replace('"', '"') s = s.replace('<', '<') s = s.replace('>', '>') return s def toXml(msg, pretty=True, includeTime=True): assert msg.command == _escape(msg.command) L = [] L.append('') if pretty: L.append('\n') for arg in msg.args: if pretty: L.append(' ') L.append('%s' % _escape(arg)) if pretty: L.append('\n') L.append('\n') return ''.join(L) def prettyPrint(msg, addRecipients=False, timestampFormat=None, showNick=True): """Provides a client-friendly string form for messages. IIRC, I copied BitchX's (or was it XChat's?) format for messages. """ def nickorprefix(): return msg.nick or msg.prefix def nick(): if addRecipients: return '%s/%s' % (msg.nick, msg.args[0]) else: return msg.nick if msg.command == 'PRIVMSG': m = _unactionre.match(msg.args[1]) if m: s = '* %s %s' % (nick(), m.group(1)) else: if not showNick: s = '%s' % msg.args[1] else: s = '<%s> %s' % (nick(), msg.args[1]) elif msg.command == 'NOTICE': if not showNick: s = '%s' % msg.args[1] else: s = '-%s- %s' % (nick(), msg.args[1]) elif msg.command == 'JOIN': prefix = msg.prefix if msg.nick: prefix = '%s <%s>' % (msg.nick, prefix) s = '*** %s has joined %s' % (prefix, msg.args[0]) elif msg.command == 'PART': if len(msg.args) > 1: partmsg = ' (%s)' % msg.args[1] else: partmsg = '' s = '*** %s <%s> has parted %s%s' % (msg.nick, msg.prefix, msg.args[0], partmsg) elif msg.command == 'KICK': if len(msg.args) > 2: kickmsg = ' (%s)' % msg.args[1] else: kickmsg = '' s = '*** %s was kicked by %s%s' % (msg.args[1], msg.nick, kickmsg) elif msg.command == 'MODE': s = '*** %s sets mode: %s' % (nickorprefix(), ' '.join(msg.args)) elif msg.command == 'QUIT': if msg.args: quitmsg = ' (%s)' % msg.args[0] else: quitmsg = '' s = '*** %s <%s> has quit IRC%s' % (msg.nick, msg.prefix, quitmsg) elif msg.command == 'TOPIC': s = '*** %s changes topic to %s' % (nickorprefix(), msg.args[1]) elif msg.command == 'NICK': s = '*** %s is now known as %s' % (msg.nick, msg.args[0]) else: s = utils.str.format('--- Unknown command %q', ' '.join(msg.args)) at = msg.tagged('receivedAt') if timestampFormat and at: s = '%s %s' % (time.strftime(timestampFormat, time.localtime(at)), s) return s ### # Various IrcMsg functions ### isNick = ircutils.isNick areNicks = ircutils.areNicks isChannel = ircutils.isChannel areChannels = ircutils.areChannels areReceivers = ircutils.areReceivers isUserHostmask = ircutils.isUserHostmask def pong(payload, prefix='', msg=None): """Takes a payload and returns the proper PONG IrcMsg.""" if conf.supybot.protocols.irc.strictRfc(): assert payload, 'PONG requires a payload' if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='PONG', args=(payload,), msg=msg) def ping(payload, prefix='', msg=None): """Takes a payload and returns the proper PING IrcMsg.""" if conf.supybot.protocols.irc.strictRfc(): assert payload, 'PING requires a payload' if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='PING', args=(payload,), msg=msg) def op(channel, nick, prefix='', msg=None): """Returns a MODE to op nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '+o', nick), msg=msg) def ops(channel, nicks, prefix='', msg=None): """Returns a MODE to op each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert nicks, 'Nicks must not be empty.' assert all(isNick, nicks), nicks if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '+' + ('o'*len(nicks))) + tuple(nicks), msg=msg) def deop(channel, nick, prefix='', msg=None): """Returns a MODE to deop nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '-o', nick), msg=msg) def deops(channel, nicks, prefix='', msg=None): """Returns a MODE to deop each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert nicks, 'Nicks must not be empty.' assert all(isNick, nicks), nicks if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', msg=msg, args=(channel, '-' + ('o'*len(nicks))) + tuple(nicks)) def halfop(channel, nick, prefix='', msg=None): """Returns a MODE to halfop nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '+h', nick), msg=msg) def halfops(channel, nicks, prefix='', msg=None): """Returns a MODE to halfop each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert nicks, 'Nicks must not be empty.' assert all(isNick, nicks), nicks if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', msg=msg, args=(channel, '+' + ('h'*len(nicks))) + tuple(nicks)) def dehalfop(channel, nick, prefix='', msg=None): """Returns a MODE to dehalfop nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '-h', nick), msg=msg) def dehalfops(channel, nicks, prefix='', msg=None): """Returns a MODE to dehalfop each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert nicks, 'Nicks must not be empty.' assert all(isNick, nicks), nicks if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', msg=msg, args=(channel, '-' + ('h'*len(nicks))) + tuple(nicks)) def voice(channel, nick, prefix='', msg=None): """Returns a MODE to voice nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '+v', nick), msg=msg) def voices(channel, nicks, prefix='', msg=None): """Returns a MODE to voice each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert nicks, 'Nicks must not be empty.' assert all(isNick, nicks) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', msg=msg, args=(channel, '+' + ('v'*len(nicks))) + tuple(nicks)) def devoice(channel, nick, prefix='', msg=None): """Returns a MODE to devoice nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '-v', nick), msg=msg) def devoices(channel, nicks, prefix='', msg=None): """Returns a MODE to devoice each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert nicks, 'Nicks must not be empty.' assert all(isNick, nicks), nicks if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', msg=msg, args=(channel, '-' + ('v'*len(nicks))) + tuple(nicks)) def ban(channel, hostmask, exception='', prefix='', msg=None): """Returns a MODE to ban nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isUserHostmask(hostmask), repr(hostmask) modes = [('+b', hostmask)] if exception: modes.append(('+e', exception)) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=[channel] + ircutils.joinModes(modes), msg=msg) def bans(channel, hostmasks, exceptions=(), prefix='', msg=None): """Returns a MODE to ban each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert all(isUserHostmask, hostmasks), hostmasks modes = [('+b', s) for s in hostmasks] + [('+e', s) for s in exceptions] if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=[channel] + ircutils.joinModes(modes), msg=msg) def unban(channel, hostmask, prefix='', msg=None): """Returns a MODE to unban nick on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isUserHostmask(hostmask), repr(hostmask) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=(channel, '-b', hostmask), msg=msg) def unbans(channel, hostmasks, prefix='', msg=None): """Returns a MODE to unban each of nicks on channel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert all(isUserHostmask, hostmasks), hostmasks modes = [('-b', s) for s in hostmasks] if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=[channel] + ircutils.joinModes(modes), msg=msg) def kick(channel, nick, s='', prefix='', msg=None): """Returns a KICK to kick nick from channel with the message msg.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8') assert isinstance(s, str) if s: return IrcMsg(prefix=prefix, command='KICK', args=(channel, nick, s), msg=msg) else: return IrcMsg(prefix=prefix, command='KICK', args=(channel, nick), msg=msg) def kicks(channels, nicks, s='', prefix='', msg=None): """Returns a KICK to kick each of nicks from channel with the message msg. """ if isinstance(channels, str): # Backward compatibility channels = [channels] if conf.supybot.protocols.irc.strictRfc(): assert areChannels(channels), repr(channels) assert areNicks(nicks), repr(nicks) if msg and not prefix: prefix = msg.prefix if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8') assert isinstance(s, str) if s: for channel in channels: return IrcMsg(prefix=prefix, command='KICK', args=(channel, ','.join(nicks), s), msg=msg) else: for channel in channels: return IrcMsg(prefix=prefix, command='KICK', args=(channel, ','.join(nicks)), msg=msg) def privmsg(recipient, s, prefix='', msg=None): """Returns a PRIVMSG to recipient with the message msg.""" if conf.supybot.protocols.irc.strictRfc(): assert (areReceivers(recipient)), repr(recipient) assert s, 's must not be empty.' if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8') assert isinstance(s, str) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='PRIVMSG', args=(recipient, s), msg=msg) def dcc(recipient, kind, *args, **kwargs): # Stupid Python won't allow (recipient, kind, *args, prefix=''), so we have # to use the **kwargs form. Blech. assert isNick(recipient), 'Can\'t DCC a channel.' kind = kind.upper() assert kind in ('SEND', 'CHAT', 'RESUME', 'ACCEPT'), 'Invalid DCC command.' args = (kind,) + args return IrcMsg(prefix=kwargs.get('prefix', ''), command='PRIVMSG', args=(recipient, ' '.join(args))) def action(recipient, s, prefix='', msg=None): """Returns a PRIVMSG ACTION to recipient with the message msg.""" if conf.supybot.protocols.irc.strictRfc(): assert (isChannel(recipient) or isNick(recipient)), repr(recipient) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='PRIVMSG', args=(recipient, '\x01ACTION %s\x01' % s), msg=msg) def notice(recipient, s, prefix='', msg=None): """Returns a NOTICE to recipient with the message msg.""" if conf.supybot.protocols.irc.strictRfc(): assert areReceivers(recipient), repr(recipient) assert s, 'msg must not be empty.' if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8') assert isinstance(s, str) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='NOTICE', args=(recipient, s), msg=msg) def join(channel, key=None, prefix='', msg=None): """Returns a JOIN to a channel""" if conf.supybot.protocols.irc.strictRfc(): assert areChannels(channel), repr(channel) if msg and not prefix: prefix = msg.prefix if key is None: return IrcMsg(prefix=prefix, command='JOIN', args=(channel,), msg=msg) else: if conf.supybot.protocols.irc.strictRfc(): chars = '\x00\r\n\f\t\v ' assert not any([(ord(x) >= 128 or x in chars) for x in key]) return IrcMsg(prefix=prefix, command='JOIN', args=(channel, key), msg=msg) def joins(channels, keys=None, prefix='', msg=None): """Returns a JOIN to each of channels.""" if conf.supybot.protocols.irc.strictRfc(): assert all(isChannel, channels), channels if msg and not prefix: prefix = msg.prefix if keys is None: keys = [] assert len(keys) <= len(channels), 'Got more keys than channels.' if not keys: return IrcMsg(prefix=prefix, command='JOIN', args=(','.join(channels),), msg=msg) else: if conf.supybot.protocols.irc.strictRfc(): chars = '\x00\r\n\f\t\v ' for key in keys: assert not any([(ord(x) >= 128 or x in chars) for x in key]) return IrcMsg(prefix=prefix, command='JOIN', args=(','.join(channels), ','.join(keys)), msg=msg) def part(channel, s='', prefix='', msg=None): """Returns a PART from channel with the message msg.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) if msg and not prefix: prefix = msg.prefix if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8') assert isinstance(s, str) if s: return IrcMsg(prefix=prefix, command='PART', args=(channel, s), msg=msg) else: return IrcMsg(prefix=prefix, command='PART', args=(channel,), msg=msg) def parts(channels, s='', prefix='', msg=None): """Returns a PART from each of channels with the message msg.""" if conf.supybot.protocols.irc.strictRfc(): assert all(isChannel, channels), channels if msg and not prefix: prefix = msg.prefix if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8') assert isinstance(s, str) if s: return IrcMsg(prefix=prefix, command='PART', args=(','.join(channels), s), msg=msg) else: return IrcMsg(prefix=prefix, command='PART', args=(','.join(channels),), msg=msg) def quit(s='', prefix='', msg=None): """Returns a QUIT with the message msg.""" if msg and not prefix: prefix = msg.prefix if s: return IrcMsg(prefix=prefix, command='QUIT', args=(s,), msg=msg) else: return IrcMsg(prefix=prefix, command='QUIT', msg=msg) def topic(channel, topic=None, prefix='', msg=None): """Returns a TOPIC for channel with the topic topic.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) if msg and not prefix: prefix = msg.prefix if topic is None: return IrcMsg(prefix=prefix, command='TOPIC', args=(channel,), msg=msg) else: if minisix.PY2 and isinstance(topic, unicode): topic = topic.encode('utf8') assert isinstance(topic, str) return IrcMsg(prefix=prefix, command='TOPIC', args=(channel, topic), msg=msg) def nick(nick, prefix='', msg=None): """Returns a NICK with nick nick.""" if conf.supybot.protocols.irc.strictRfc(): assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='NICK', args=(nick,), msg=msg) def user(ident, user, prefix='', msg=None): """Returns a USER with ident ident and user user.""" if conf.supybot.protocols.irc.strictRfc(): assert '\x00' not in ident and \ '\r' not in ident and \ '\n' not in ident and \ ' ' not in ident and \ '@' not in ident if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='USER', args=(ident, '0', '*', user), msg=msg) def who(hostmaskOrChannel, prefix='', msg=None, args=()): """Returns a WHO for the hostmask or channel hostmaskOrChannel.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(hostmaskOrChannel) or \ isUserHostmask(hostmaskOrChannel), repr(hostmaskOrChannel) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='WHO', args=(hostmaskOrChannel,) + args, msg=msg) def _whois(COMMAND, nick, mask='', prefix='', msg=None): """Returns a WHOIS for nick.""" if conf.supybot.protocols.irc.strictRfc(): assert areNicks(nick), repr(nick) if msg and not prefix: prefix = msg.prefix args = (nick,) if mask: args = (nick, mask) return IrcMsg(prefix=prefix, command=COMMAND, args=args, msg=msg) whois = functools.partial(_whois, 'WHOIS') whowas = functools.partial(_whois, 'WHOWAS') def names(channel=None, prefix='', msg=None): if conf.supybot.protocols.irc.strictRfc(): assert areChannels(channel) if msg and not prefix: prefix = msg.prefix if channel is not None: return IrcMsg(prefix=prefix, command='NAMES', args=(channel,), msg=msg) else: return IrcMsg(prefix=prefix, command='NAMES', msg=msg) def mode(channel, args=(), prefix='', msg=None): if msg and not prefix: prefix = msg.prefix if isinstance(args, minisix.string_types): args = (args,) else: args = tuple(map(str, args)) return IrcMsg(prefix=prefix, command='MODE', args=(channel,)+args, msg=msg) def modes(channel, args=(), prefix='', msg=None): """Returns a MODE message for the channel for all the (mode, targetOrNone) 2-tuples in 'args'.""" if conf.supybot.protocols.irc.strictRfc(): assert isChannel(channel), repr(channel) modes = args if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='MODE', args=[channel] + ircutils.joinModes(modes), msg=msg) def limit(channel, limit, prefix='', msg=None): return mode(channel, ['+l', limit], prefix=prefix, msg=msg) def unlimit(channel, limit, prefix='', msg=None): return mode(channel, ['-l', limit], prefix=prefix, msg=msg) def invite(nick, channel, prefix='', msg=None): """Returns an INVITE for nick.""" if conf.supybot.protocols.irc.strictRfc(): assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='INVITE', args=(nick, channel), msg=msg) def password(password, prefix='', msg=None): """Returns a PASS command for accessing a server.""" if conf.supybot.protocols.irc.strictRfc(): assert password, 'password must not be empty.' if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='PASS', args=(password,), msg=msg) def ison(nick, prefix='', msg=None): if conf.supybot.protocols.irc.strictRfc(): assert isNick(nick), repr(nick) if msg and not prefix: prefix = msg.prefix return IrcMsg(prefix=prefix, command='ISON', args=(nick,), msg=msg) def monitor(subcommand, nicks=None, prefix='', msg=None): if conf.supybot.protocols.irc.strictRfc(): for nick in nicks: assert isNick(nick), repr(nick) assert subcommand in '+-CLS' if subcommand in 'CLS': assert nicks is None if msg and not prefix: prefix = msg.prefix if not isinstance(nicks, str): nicks = ','.join(nicks) return IrcMsg(prefix=prefix, command='MONITOR', args=(subcommand, nicks), msg=msg) def error(s, msg=None): return IrcMsg(command='ERROR', args=(s,), msg=msg) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/irclib.py0000644000175000017500000016352513233426066015740 0ustar valval00000000000000### # 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. ### import re import copy import time import random import base64 import collections try: import ecdsa except ImportError: ecdsa = None try: import pyxmpp2_scram as scram except ImportError: scram = None from . import conf, ircdb, ircmsgs, ircutils, log, utils, world from .utils.str import rsplit from .utils.iter import chain from .utils.structures import smallqueue, RingBuffer ### # The base class for a callback to be registered with an Irc object. Shows # the required interface for callbacks -- name(), # inFilter(irc, msg), outFilter(irc, msg), and __call__(irc, msg) [used so as # to make functions used as callbacks conceivable, and so if refactoring ever # changes the nature of the callbacks from classes to functions, syntactical # changes elsewhere won't be required.] ### class IrcCommandDispatcher(object): """Base class for classes that must dispatch on a command.""" def dispatchCommand(self, command): """Given a string 'command', dispatches to doCommand.""" return getattr(self, 'do' + command.capitalize(), None) class IrcCallback(IrcCommandDispatcher, log.Firewalled): """Base class for standard callbacks. Callbacks derived from this class should have methods of the form "doCommand" -- doPrivmsg, doNick, do433, etc. These will be called on matching messages. """ callAfter = () callBefore = () __firewalled__ = {'die': None, 'reset': None, '__call__': None, 'inFilter': lambda self, irc, msg: msg, 'outFilter': lambda self, irc, msg: msg, 'name': lambda self: self.__class__.__name__, 'callPrecedence': lambda self, irc: ([], []), } def __init__(self, *args, **kwargs): #object doesn't take any args, so the buck stops here. #super(IrcCallback, self).__init__(*args, **kwargs) pass def __repr__(self): return '<%s %s %s>' % \ (self.__class__.__name__, self.name(), object.__repr__(self)) def name(self): """Returns the name of the callback.""" return self.__class__.__name__ def callPrecedence(self, irc): """Returns a pair of (callbacks to call before me, callbacks to call after me)""" after = [] before = [] for name in self.callBefore: cb = irc.getCallback(name) if cb is not None: after.append(cb) for name in self.callAfter: cb = irc.getCallback(name) if cb is not None: before.append(cb) assert self not in after, '%s was in its own after.' % self.name() assert self not in before, '%s was in its own before.' % self.name() return (before, after) def inFilter(self, irc, msg): """Used for filtering/modifying messages as they're entering. ircmsgs.IrcMsg objects are immutable, so this method is expected to return another ircmsgs.IrcMsg object. Obviously the same IrcMsg can be returned. """ return msg def outFilter(self, irc, msg): """Used for filtering/modifying messages as they're leaving. As with inFilter, an IrcMsg is returned. """ return msg def __call__(self, irc, msg): """Used for handling each message.""" method = self.dispatchCommand(msg.command) if method is not None: method(irc, msg) def reset(self): """Resets the callback. Called when reconnecting to the server.""" pass def die(self): """Makes the callback die. Called when the parent Irc object dies.""" pass ### # Basic queue for IRC messages. It doesn't presently (but should at some # later point) reorder messages based on priority or penalty calculations. ### _high = frozenset(['MODE', 'KICK', 'PONG', 'NICK', 'PASS', 'CAPAB', 'REMOVE']) _low = frozenset(['PRIVMSG', 'PING', 'WHO', 'NOTICE', 'JOIN']) class IrcMsgQueue(object): """Class for a queue of IrcMsgs. Eventually, it should be smart. Probably smarter than it is now, though it's gotten quite a bit smarter than it originally was. A method to "score" methods, and a heapq to maintain a priority queue of the messages would be the ideal way to do intelligent queuing. As it stands, however, we simply keep track of 'high priority' messages, 'low priority' messages, and normal messages, and just make sure to return the 'high priority' ones before the normal ones before the 'low priority' ones. """ __slots__ = ('msgs', 'highpriority', 'normal', 'lowpriority', 'lastJoin') def __init__(self, iterable=()): self.reset() for msg in iterable: self.enqueue(msg) def reset(self): """Clears the queue.""" self.lastJoin = 0 self.highpriority = smallqueue() self.normal = smallqueue() self.lowpriority = smallqueue() def enqueue(self, msg): """Enqueues a given message.""" if msg in self and \ conf.supybot.protocols.irc.queuing.duplicates(): s = str(msg).strip() log.info('Not adding message %q to queue, already added.', s) return False else: if msg.command in _high: self.highpriority.enqueue(msg) elif msg.command in _low: self.lowpriority.enqueue(msg) else: self.normal.enqueue(msg) return True def dequeue(self): """Dequeues a given message.""" msg = None if self.highpriority: msg = self.highpriority.dequeue() elif self.normal: msg = self.normal.dequeue() elif self.lowpriority: msg = self.lowpriority.dequeue() if msg.command == 'JOIN': limit = conf.supybot.protocols.irc.queuing.rateLimit.join() now = time.time() if self.lastJoin + limit <= now: self.lastJoin = now else: self.lowpriority.enqueue(msg) msg = None return msg def __contains__(self, msg): return msg in self.normal or \ msg in self.lowpriority or \ msg in self.highpriority def __bool__(self): return bool(self.highpriority or self.normal or self.lowpriority) __nonzero__ = __bool__ def __len__(self): return len(self.highpriority)+len(self.lowpriority)+len(self.normal) def __repr__(self): name = self.__class__.__name__ return '%s(%r)' % (name, list(chain(self.highpriority, self.normal, self.lowpriority))) __str__ = __repr__ ### # Maintains the state of IRC connection -- the most recent messages, the # status of various modes (especially ops/halfops/voices) in channels, etc. ### class ChannelState(utils.python.Object): __slots__ = ('users', 'ops', 'halfops', 'bans', 'voices', 'topic', 'modes', 'created') def __init__(self): self.topic = '' self.created = 0 self.ops = ircutils.IrcSet() self.bans = ircutils.IrcSet() self.users = ircutils.IrcSet() self.voices = ircutils.IrcSet() self.halfops = ircutils.IrcSet() self.modes = {} def isOp(self, nick): return nick in self.ops def isOpPlus(self, nick): return nick in self.ops def isVoice(self, nick): return nick in self.voices def isVoicePlus(self, nick): return nick in self.voices or nick in self.halfops or nick in self.ops def isHalfop(self, nick): return nick in self.halfops def isHalfopPlus(self, nick): return nick in self.halfops or nick in self.ops def addUser(self, user): "Adds a given user to the ChannelState. Power prefixes are handled." nick = user.lstrip('@%+&~!') if not nick: return # & is used to denote protected users in UnrealIRCd # ~ is used to denote channel owner in UnrealIRCd # ! is used to denote protected users in UltimateIRCd while user and user[0] in '@%+&~!': (marker, user) = (user[0], user[1:]) assert user, 'Looks like my caller is passing chars, not nicks.' if marker in '@&~!': self.ops.add(nick) elif marker == '%': self.halfops.add(nick) elif marker == '+': self.voices.add(nick) self.users.add(nick) def replaceUser(self, oldNick, newNick): """Changes the user oldNick to newNick; used for NICK changes.""" # Note that this doesn't have to have the sigil (@%+) that users # have to have for addUser; it just changes the name of the user # without changing any of their categories. for s in (self.users, self.ops, self.halfops, self.voices): if oldNick in s: s.remove(oldNick) s.add(newNick) def removeUser(self, user): """Removes a given user from the channel.""" self.users.discard(user) self.ops.discard(user) self.halfops.discard(user) self.voices.discard(user) def setMode(self, mode, value=None): assert mode not in 'ovhbeq' self.modes[mode] = value def unsetMode(self, mode): assert mode not in 'ovhbeq' if mode in self.modes: del self.modes[mode] def doMode(self, msg): def getSet(c): if c == 'o': Set = self.ops elif c == 'v': Set = self.voices elif c == 'h': Set = self.halfops elif c == 'b': Set = self.bans else: # We don't care yet, so we'll just return an empty set. Set = set() return Set for (mode, value) in ircutils.separateModes(msg.args[1:]): (action, modeChar) = mode if modeChar in 'ovhbeq': # We don't handle e or q yet. Set = getSet(modeChar) if action == '-': Set.discard(value) elif action == '+': Set.add(value) else: if action == '+': self.setMode(modeChar, value) else: assert action == '-' self.unsetMode(modeChar) def __getstate__(self): return [getattr(self, name) for name in self.__slots__] def __setstate__(self, t): for (name, value) in zip(self.__slots__, t): setattr(self, name, value) def __eq__(self, other): ret = True for name in self.__slots__: ret = ret and getattr(self, name) == getattr(other, name) return ret Batch = collections.namedtuple('Batch', 'type arguments messages') class IrcState(IrcCommandDispatcher, log.Firewalled): """Maintains state of the Irc connection. Should also become smarter. """ __firewalled__ = {'addMsg': None} def __init__(self, history=None, supported=None, nicksToHostmasks=None, channels=None, capabilities_ack=None, capabilities_nak=None, capabilities_ls=None): if history is None: history = RingBuffer(conf.supybot.protocols.irc.maxHistoryLength()) if supported is None: supported = utils.InsensitivePreservingDict() if nicksToHostmasks is None: nicksToHostmasks = ircutils.IrcDict() if channels is None: channels = ircutils.IrcDict() self.capabilities_ack = capabilities_ack or set() self.capabilities_nak = capabilities_nak or set() self.capabilities_ls = capabilities_ls or {} self.ircd = None self.supported = supported self.history = history self.channels = channels self.nicksToHostmasks = nicksToHostmasks self.batches = {} def reset(self): """Resets the state to normal, unconnected state.""" self.history.reset() self.channels.clear() self.supported.clear() self.nicksToHostmasks.clear() self.history.resize(conf.supybot.protocols.irc.maxHistoryLength()) self.batches = {} def __reduce__(self): return (self.__class__, (self.history, self.supported, self.nicksToHostmasks, self.channels)) def __eq__(self, other): return self.history == other.history and \ self.channels == other.channels and \ self.supported == other.supported and \ self.nicksToHostmasks == other.nicksToHostmasks and \ self.batches == other.batches def __ne__(self, other): return not self == other def copy(self): ret = self.__class__() ret.history = copy.deepcopy(self.history) ret.nicksToHostmasks = copy.deepcopy(self.nicksToHostmasks) ret.channels = copy.deepcopy(self.channels) ret.batches = copy.deepcopy(self.batches) return ret def addMsg(self, irc, msg): """Updates the state based on the irc object and the message.""" self.history.append(msg) if ircutils.isUserHostmask(msg.prefix) and not msg.command == 'NICK': self.nicksToHostmasks[msg.nick] = msg.prefix if 'batch' in msg.server_tags: batch = msg.server_tags['batch'] assert batch in self.batches, \ 'Server references undeclared batch %s' % batch self.batches[batch].messages.append(msg) method = self.dispatchCommand(msg.command) if method is not None: method(irc, msg) def getTopic(self, channel): """Returns the topic for a given channel.""" return self.channels[channel].topic def nickToHostmask(self, nick): """Returns the hostmask for a given nick.""" return self.nicksToHostmasks[nick] def do004(self, irc, msg): """Handles parsing the 004 reply Supported user and channel modes are cached""" # msg.args = [nick, server, ircd-version, umodes, modes, # modes that require arguments? (non-standard)] self.ircd = msg.args[2] if len(msg.args) >= 3 else msg.args[1] self.supported['umodes'] = frozenset(msg.args[3]) self.supported['chanmodes'] = frozenset(msg.args[4]) _005converters = utils.InsensitivePreservingDict({ 'modes': int, 'keylen': int, 'nicklen': int, 'userlen': int, 'hostlen': int, 'kicklen': int, 'awaylen': int, 'silence': int, 'topiclen': int, 'channellen': int, 'maxtargets': int, 'maxnicklen': int, 'maxchannels': int, 'watch': int, # DynastyNet, EnterTheGame }) def _prefixParser(s): if ')' in s: (left, right) = s.split(')') assert left[0] == '(', 'Odd PREFIX in 005: %s' % s left = left[1:] assert len(left) == len(right), 'Odd PREFIX in 005: %s' % s return dict(list(zip(left, right))) else: return dict(list(zip('ovh', s))) _005converters['prefix'] = _prefixParser del _prefixParser def _maxlistParser(s): modes = '' limits = [] pairs = s.split(',') for pair in pairs: (mode, limit) = pair.split(':', 1) modes += mode limits += (int(limit),) * len(mode) return dict(list(zip(modes, limits))) _005converters['maxlist'] = _maxlistParser del _maxlistParser def _maxbansParser(s): # IRCd using a MAXLIST style string (IRCNet) if ':' in s: modes = '' limits = [] pairs = s.split(',') for pair in pairs: (mode, limit) = pair.split(':', 1) modes += mode limits += (int(limit),) * len(mode) d = dict(list(zip(modes, limits))) assert 'b' in d return d['b'] else: return int(s) _005converters['maxbans'] = _maxbansParser del _maxbansParser def do005(self, irc, msg): for arg in msg.args[1:-1]: # 0 is nick, -1 is "are supported" if '=' in arg: (name, value) = arg.split('=', 1) converter = self._005converters.get(name, lambda x: x) try: self.supported[name] = converter(value) except Exception: log.exception('Uncaught exception in 005 converter:') log.error('Name: %s, Converter: %s', name, converter) else: self.supported[arg] = None def do352(self, irc, msg): # WHO reply. (nick, user, host) = (msg.args[5], msg.args[2], msg.args[3]) hostmask = '%s!%s@%s' % (nick, user, host) self.nicksToHostmasks[nick] = hostmask def do354(self, irc, msg): # WHOX reply. if len(msg.args) != 9 or msg.args[1] != '1': return # irc.nick 1 user ip host nick status account gecos (n, t, user, ip, host, nick, status, account, gecos) = msg.args hostmask = '%s!%s@%s' % (nick, user, host) self.nicksToHostmasks[nick] = hostmask def do353(self, irc, msg): # NAMES reply. (__, type, channel, items) = msg.args if channel not in self.channels: self.channels[channel] = ChannelState() c = self.channels[channel] for item in items.split(): if ircutils.isUserHostmask(item): name = ircutils.nickFromHostmask(item) self.nicksToHostmasks[name] = name else: name = item c.addUser(name) if type == '@': c.modes['s'] = None def doChghost(self, irc, msg): (user, host) = msg.args nick = msg.nick hostmask = '%s!%s@%s' % (nick, user, host) self.nicksToHostmasks[nick] = hostmask def doJoin(self, irc, msg): for channel in msg.args[0].split(','): if channel in self.channels: self.channels[channel].addUser(msg.nick) elif msg.nick: # It must be us. chan = ChannelState() chan.addUser(msg.nick) self.channels[channel] = chan # I don't know why this assert was here. #assert msg.nick == irc.nick, msg def do367(self, irc, msg): # Example: # :server 367 user #chan some!random@user evil!channel@op 1356276459 try: state = self.channels[msg.args[1]] except KeyError: # We have been kicked of the channel before the server replied to # the MODE +b command. pass else: state.bans.add(msg.args[2]) def doMode(self, irc, msg): channel = msg.args[0] if ircutils.isChannel(channel): # There can be user modes, as well. try: chan = self.channels[channel] except KeyError: chan = ChannelState() self.channels[channel] = chan chan.doMode(msg) def do324(self, irc, msg): channel = msg.args[1] try: chan = self.channels[channel] except KeyError: chan = ChannelState() self.channels[channel] = chan for (mode, value) in ircutils.separateModes(msg.args[2:]): modeChar = mode[1] if mode[0] == '+' and mode[1] not in 'ovh': chan.setMode(modeChar, value) elif mode[0] == '-' and mode[1] not in 'ovh': chan.unsetMode(modeChar) def do329(self, irc, msg): # This is the last part of an empty mode. channel = msg.args[1] try: chan = self.channels[channel] except KeyError: chan = ChannelState() self.channels[channel] = chan chan.created = int(msg.args[2]) def doPart(self, irc, msg): for channel in msg.args[0].split(','): try: chan = self.channels[channel] except KeyError: continue if ircutils.strEqual(msg.nick, irc.nick): del self.channels[channel] else: chan.removeUser(msg.nick) def doKick(self, irc, msg): (channel, users) = msg.args[:2] chan = self.channels[channel] for user in users.split(','): if ircutils.strEqual(user, irc.nick): del self.channels[channel] return else: chan.removeUser(user) def doQuit(self, irc, msg): channel_names = ircutils.IrcSet() for (name, channel) in self.channels.items(): if msg.nick in channel.users: channel_names.add(name) channel.removeUser(msg.nick) # Remember which channels the user was on msg.tag('channels', channel_names) if msg.nick in self.nicksToHostmasks: # If we're quitting, it may not be. del self.nicksToHostmasks[msg.nick] def doTopic(self, irc, msg): if len(msg.args) == 1: return # Empty TOPIC for information. Does not affect state. try: chan = self.channels[msg.args[0]] chan.topic = msg.args[1] except KeyError: pass # We don't have to be in a channel to send a TOPIC. def do332(self, irc, msg): chan = self.channels[msg.args[1]] chan.topic = msg.args[2] def doNick(self, irc, msg): newNick = msg.args[0] oldNick = msg.nick try: if msg.user and msg.host: # Nick messages being handed out from the bot itself won't # have the necessary prefix to make a hostmask. newHostmask = ircutils.joinHostmask(newNick,msg.user,msg.host) self.nicksToHostmasks[newNick] = newHostmask del self.nicksToHostmasks[oldNick] except KeyError: pass channel_names = ircutils.IrcSet() for (name, channel) in self.channels.items(): if msg.nick in channel.users: channel_names.add(name) channel.replaceUser(oldNick, newNick) msg.tag('channels', channel_names) def doBatch(self, irc, msg): batch_name = msg.args[0][1:] if msg.args[0].startswith('+'): batch_type = msg.args[1] batch_arguments = tuple(msg.args[2:]) self.batches[batch_name] = Batch(type=batch_type, arguments=batch_arguments, messages=[]) elif msg.args[0].startswith('-'): batch = self.batches.pop(batch_name) msg.tag('batch', batch) else: assert False, msg.args[0] def doAway(self, irc, msg): channel_names = ircutils.IrcSet() for (name, channel) in self.channels.items(): if msg.nick in channel.users: channel_names.add(name) msg.tag('channels', channel_names) ### # The basic class for handling a connection to an IRC server. Accepts # callbacks of the IrcCallback interface. Public attributes include 'driver', # 'queue', and 'state', in addition to the standard nick/user/ident attributes. ### _callbacks = [] class Irc(IrcCommandDispatcher, log.Firewalled): """The base class for an IRC connection. Handles PING commands already. """ __firewalled__ = {'die': None, 'feedMsg': None, 'takeMsg': None,} _nickSetters = set(['001', '002', '003', '004', '250', '251', '252', '254', '255', '265', '266', '372', '375', '376', '333', '353', '332', '366', '005']) # We specifically want these callbacks to be common between all Ircs, # that's why we don't do the normal None default with a check. def __init__(self, network, callbacks=_callbacks): self.zombie = False world.ircs.append(self) self.network = network self.startedAt = time.time() self.callbacks = callbacks self.state = IrcState() self.queue = IrcMsgQueue() self.fastqueue = smallqueue() self.driver = None # The driver should set this later. self._setNonResettingVariables() self._queueConnectMessages() self.startedSync = ircutils.IrcDict() self.monitoring = ircutils.IrcDict() def isChannel(self, s): """Helper function to check whether a given string is a channel on the network this Irc object is connected to.""" kw = {} if 'chantypes' in self.state.supported: kw['chantypes'] = self.state.supported['chantypes'] if 'channellen' in self.state.supported: kw['channellen'] = self.state.supported['channellen'] return ircutils.isChannel(s, **kw) def isNick(self, s): kw = {} if 'nicklen' in self.state.supported: kw['nicklen'] = self.state.supported['nicklen'] return ircutils.isNick(s, **kw) # This *isn't* threadsafe! def addCallback(self, callback): """Adds a callback to the callbacks list. :param callback: A callback object :type callback: supybot.irclib.IrcCallback """ assert not self.getCallback(callback.name()) self.callbacks.append(callback) # This is the new list we're building, which will be tsorted. cbs = [] # The vertices are self.callbacks itself. Now we make the edges. edges = set() for cb in self.callbacks: (before, after) = cb.callPrecedence(self) assert cb not in after, 'cb was in its own after.' assert cb not in before, 'cb was in its own before.' for otherCb in before: edges.add((otherCb, cb)) for otherCb in after: edges.add((cb, otherCb)) def getFirsts(): firsts = set(self.callbacks) - set(cbs) for (before, after) in edges: firsts.discard(after) return firsts firsts = getFirsts() while firsts: # Then we add these to our list of cbs, and remove all edges that # originate with these cbs. for cb in firsts: cbs.append(cb) edgesToRemove = [] for edge in edges: if edge[0] is cb: edgesToRemove.append(edge) for edge in edgesToRemove: edges.remove(edge) firsts = getFirsts() assert len(cbs) == len(self.callbacks), \ 'cbs: %s, self.callbacks: %s' % (cbs, self.callbacks) self.callbacks[:] = cbs def getCallback(self, name): """Gets a given callback by name.""" name = name.lower() for callback in self.callbacks: if callback.name().lower() == name: return callback else: return None def removeCallback(self, name): """Removes a callback from the callback list.""" name = name.lower() def nameMatches(cb): return cb.name().lower() == name (bad, good) = utils.iter.partition(nameMatches, self.callbacks) self.callbacks[:] = good return bad def queueMsg(self, msg): """Queues a message to be sent to the server.""" if not self.zombie: return self.queue.enqueue(msg) else: log.warning('Refusing to queue %r; %s is a zombie.', msg, self) return False def sendMsg(self, msg): """Queues a message to be sent to the server *immediately*""" if not self.zombie: self.fastqueue.enqueue(msg) else: log.warning('Refusing to send %r; %s is a zombie.', msg, self) def takeMsg(self): """Called by the IrcDriver; takes a message to be sent.""" if not self.callbacks: log.critical('No callbacks in %s.', self) now = time.time() msg = None if self.fastqueue: msg = self.fastqueue.dequeue() elif self.queue: if now-self.lastTake <= conf.supybot.protocols.irc.throttleTime(): log.debug('Irc.takeMsg throttling.') else: self.lastTake = now msg = self.queue.dequeue() elif self.afterConnect and \ conf.supybot.protocols.irc.ping() and \ now > self.lastping + conf.supybot.protocols.irc.ping.interval(): if self.outstandingPing: s = 'Ping sent at %s not replied to.' % \ log.timestamp(self.lastping) log.warning(s) self.feedMsg(ircmsgs.error(s)) self.driver.reconnect() elif not self.zombie: self.lastping = now now = str(int(now)) self.outstandingPing = True self.queueMsg(ircmsgs.ping(now)) if msg: for callback in reversed(self.callbacks): msg = callback.outFilter(self, msg) if msg is None: log.debug('%s.outFilter returned None.', callback.name()) return self.takeMsg() world.debugFlush() if len(str(msg)) > 512: # Yes, this violates the contract, but at this point it doesn't # matter. That's why we gotta go munging in private attributes # # I'm changing this to a log.debug to fix a possible loop in # the LogToIrc plugin. Since users can't do anything about # this issue, there's no fundamental reason to make it a # warning. log.debug('Truncating %r, message is too long.', msg) msg._str = msg._str[:500] + '\r\n' msg._len = len(str(msg)) # I don't think we should do this. Why should it matter? If it's # something important, then the server will send it back to us, # and if it's just a privmsg/notice/etc., we don't care. # On second thought, we need this for testing. if world.testing: self.state.addMsg(self, msg) log.debug('Outgoing message (%s): %s', self.network, str(msg).rstrip('\r\n')) return msg elif self.zombie: # We kill the driver here so it doesn't continue to try to # take messages from us. self.driver.die() self._reallyDie() else: return None _numericErrorCommandRe = re.compile(r'^[45][0-9][0-9]$') def feedMsg(self, msg): """Called by the IrcDriver; feeds a message received.""" msg.tag('receivedBy', self) msg.tag('receivedOn', self.network) msg.tag('receivedAt', time.time()) if msg.args and self.isChannel(msg.args[0]): channel = msg.args[0] else: channel = None preInFilter = str(msg).rstrip('\r\n') log.debug('Incoming message (%s): %s', self.network, preInFilter) # Yeah, so this is odd. Some networks (oftc) seem to give us certain # messages with our nick instead of our prefix. We'll fix that here. if msg.prefix == self.nick: log.debug('Got one of those odd nick-instead-of-prefix msgs.') msg = ircmsgs.IrcMsg(prefix=self.prefix, msg=msg) # This catches cases where we know our own nick (from sending it to the # server) but we don't yet know our prefix. if msg.nick == self.nick and self.prefix != msg.prefix: self.prefix = msg.prefix # This keeps our nick and server attributes updated. if msg.command in self._nickSetters: if msg.args[0] != self.nick: self.nick = msg.args[0] log.debug('Updating nick attribute to %s.', self.nick) if msg.prefix != self.server: self.server = msg.prefix log.debug('Updating server attribute to %s.', self.server) # Dispatch to specific handlers for commands. method = self.dispatchCommand(msg.command) if method is not None: method(msg) elif self._numericErrorCommandRe.search(msg.command): log.error('Unhandled error message from server: %r' % msg) # Now update the IrcState object. try: self.state.addMsg(self, msg) except: log.exception('Exception in update of IrcState object:') # Now call the callbacks. world.debugFlush() for callback in self.callbacks: try: m = callback.inFilter(self, msg) if not m: log.debug('%s.inFilter returned None', callback.name()) return msg = m except: log.exception('Uncaught exception in inFilter:') world.debugFlush() postInFilter = str(msg).rstrip('\r\n') if postInFilter != preInFilter: log.debug('Incoming message (post-inFilter): %s', postInFilter) for callback in self.callbacks: try: if callback is not None: callback(self, msg) except: log.exception('Uncaught exception in callback:') world.debugFlush() def die(self): """Makes the Irc object *promise* to die -- but it won't die (of its own volition) until all its queues are clear. Isn't that cool?""" self.zombie = True if not self.afterConnect: self._reallyDie() # This is useless because it's in world.ircs, so it won't be deleted until # the program exits. Just figured you might want to know. #def __del__(self): # self._reallyDie() def reset(self): """Resets the Irc object. Called when the driver reconnects.""" self._setNonResettingVariables() self.state.reset() self.queue.reset() self.fastqueue.reset() self.startedSync.clear() for callback in self.callbacks: callback.reset() self._queueConnectMessages() def _setNonResettingVariables(self): # Configuration stuff. network_config = conf.supybot.networks.get(self.network) def get_value(name): return getattr(network_config, name)() or \ getattr(conf.supybot, name)() self.nick = get_value('nick') # Expand variables like $version in realname. self.user = ircutils.standardSubstitute(self, None, get_value('user')) self.ident = get_value('ident') self.alternateNicks = conf.supybot.nick.alternates()[:] self.password = network_config.password() self.prefix = '%s!%s@%s' % (self.nick, self.ident, 'unset.domain') # The rest. self.lastTake = 0 self.server = 'unset' self.afterConnect = False self.startedAt = time.time() self.lastping = time.time() self.outstandingPing = False self.capNegociationEnded = False self.requireStarttls = not network_config.ssl() and \ network_config.requireStarttls() if self.requireStarttls: log.error(('STARTTLS is no longer supported. Set ' 'supybot.networks.%s.requireStarttls to False ' 'to disable it, and use supybot.networks.%s.ssl ' 'instead.') % (self.network, self.network)) self.driver.die() self._reallyDie() return self.resetSasl() def resetSasl(self): network_config = conf.supybot.networks.get(self.network) self.sasl_authenticated = False self.sasl_username = network_config.sasl.username() self.sasl_password = network_config.sasl.password() self.sasl_ecdsa_key = network_config.sasl.ecdsa_key() self.sasl_scram_state = {'step': 'uninitialized'} self.authenticate_decoder = None self.sasl_next_mechanisms = [] self.sasl_current_mechanism = None for mechanism in network_config.sasl.mechanisms(): if mechanism == 'ecdsa-nist256p-challenge' and \ ecdsa and self.sasl_username and self.sasl_ecdsa_key: self.sasl_next_mechanisms.append(mechanism) elif mechanism == 'external' and ( network_config.certfile() or conf.supybot.protocols.irc.certfile()): self.sasl_next_mechanisms.append(mechanism) elif mechanism.startswith('scram-') and scram and \ self.sasl_username and self.sasl_password: self.sasl_next_mechanisms.append(mechanism) elif mechanism == 'plain' and \ self.sasl_username and self.sasl_password: self.sasl_next_mechanisms.append(mechanism) if self.sasl_next_mechanisms: self.REQUEST_CAPABILITIES.add('sasl') REQUEST_CAPABILITIES = set(['account-notify', 'extended-join', 'multi-prefix', 'metadata-notify', 'account-tag', 'userhost-in-names', 'invite-notify', 'server-time', 'chghost', 'batch', 'away-notify']) def _queueConnectMessages(self): if self.zombie: self.driver.die() self._reallyDie() return self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('LS', '302'))) self.sendAuthenticationMessages() def sendAuthenticationMessages(self): # Notes: # * using sendMsg instead of queueMsg because these messages cannot # be throttled. if self.password: log.info('%s: Queuing PASS command, not logging the password.', self.network) self.sendMsg(ircmsgs.password(self.password)) log.debug('%s: Sending NICK command, nick is %s.', self.network, self.nick) self.sendMsg(ircmsgs.nick(self.nick)) log.debug('%s: Sending USER command, ident is %s, user is %s.', self.network, self.ident, self.user) self.sendMsg(ircmsgs.user(self.ident, self.user)) def endCapabilityNegociation(self): if not self.capNegociationEnded: self.capNegociationEnded = True self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('END',))) def sendSaslString(self, string): for chunk in ircutils.authenticate_generator(string): self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=(chunk,))) def tryNextSaslMechanism(self): if self.sasl_next_mechanisms: self.sasl_current_mechanism = self.sasl_next_mechanisms.pop(0) self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=(self.sasl_current_mechanism.upper(),))) else: self.sasl_current_mechanism = None self.endCapabilityNegociation() def filterSaslMechanisms(self, available): available = set(map(str.lower, available)) self.sasl_next_mechanisms = [ x for x in self.sasl_next_mechanisms if x.lower() in available] def doAuthenticate(self, msg): if not self.authenticate_decoder: self.authenticate_decoder = ircutils.AuthenticateDecoder() self.authenticate_decoder.feed(msg) if not self.authenticate_decoder.ready: return # Waiting for other messages string = self.authenticate_decoder.get() self.authenticate_decoder = None mechanism = self.sasl_current_mechanism if mechanism == 'ecdsa-nist256p-challenge': self.doAuthenticateEcdsa(string) elif mechanism == 'external': self.sendSaslString(b'') elif mechanism.startswith('scram-'): step = self.sasl_scram_state['step'] try: if step == 'uninitialized': log.debug('%s: starting SCRAM.', self.network) self.doAuthenticateScramFirst(mechanism) elif step == 'first-sent': log.debug('%s: received SCRAM challenge.', self.network) self.doAuthenticateScramChallenge(string) elif step == 'final-sent': log.debug('%s: finishing SCRAM.', self.network) self.doAuthenticateScramFinish(string) else: assert False except scram.ScramException: self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) self.tryNextSaslMechanism() elif mechanism == 'plain': authstring = b'\0'.join([ self.sasl_username.encode('utf-8'), self.sasl_username.encode('utf-8'), self.sasl_password.encode('utf-8'), ]) self.sendSaslString(authstring) def doAuthenticateEcdsa(self, string): if string == b'': self.sendSaslString(self.sasl_username.encode('utf-8')) return try: with open(self.sasl_ecdsa_key) as fd: private_key = ecdsa.SigningKey.from_pem(fd.read()) authstring = private_key.sign(string) self.sendSaslString(authstring) except (ecdsa.BadDigestError, OSError, ValueError): self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) self.tryNextSaslMechanism() def doAuthenticateScramFirst(self, mechanism): """Handle sending the client-first message of SCRAM auth.""" hash_name = mechanism[len('scram-'):] if hash_name.endswith('-plus'): hash_name = hash_name[:-len('-plus')] hash_name = hash_name.upper() if hash_name not in scram.HASH_FACTORIES: log.debug('%s: SCRAM hash %r not supported, aborting.', self.network, hash_name) self.tryNextSaslMechanism() return authenticator = scram.SCRAMClientAuthenticator(hash_name, channel_binding=False) self.sasl_scram_state['authenticator'] = authenticator client_first = authenticator.start({ 'username': self.sasl_username, 'password': self.sasl_password, }) self.sendSaslString(client_first) self.sasl_scram_state['step'] = 'first-sent' def doAuthenticateScramChallenge(self, challenge): client_final = self.sasl_scram_state['authenticator'] \ .challenge(challenge) self.sendSaslString(client_final) self.sasl_scram_state['step'] = 'final-sent' def doAuthenticateScramFinish(self, data): try: res = self.sasl_scram_state['authenticator'] \ .finish(data) except scram.BadSuccessException as e: log.warning('%s: SASL authentication failed with SCRAM error: %e', self.network, e) self.tryNextSaslMechanism() else: self.sendSaslString(b'') self.sasl_scram_state['step'] = 'authenticated' def do903(self, msg): log.info('%s: SASL authentication successful', self.network) self.sasl_authenticated = True self.endCapabilityNegociation() def do904(self, msg): log.warning('%s: SASL authentication failed', self.network) self.tryNextSaslMechanism() def do905(self, msg): log.warning('%s: SASL authentication failed because the username or ' 'password is too long.', self.network) self.tryNextSaslMechanism() def do906(self, msg): log.warning('%s: SASL authentication aborted', self.network) self.tryNextSaslMechanism() def do907(self, msg): log.warning('%s: Attempted SASL authentication when we were already ' 'authenticated.', self.network) self.tryNextSaslMechanism() def do908(self, msg): log.info('%s: Supported SASL mechanisms: %s', self.network, msg.args[1]) self.filterSaslMechanisms(set(msg.args[1].split(','))) def doCap(self, msg): subcommand = msg.args[1] if subcommand == 'ACK': self.doCapAck(msg) elif subcommand == 'NAK': self.doCapNak(msg) elif subcommand == 'LS': self.doCapLs(msg) elif subcommand == 'DEL': self.doCapDel(msg) elif subcommand == 'NEW': self.doCapNew(msg) def doCapAck(self, msg): if len(msg.args) != 3: log.warning('Bad CAP ACK from server: %r', msg) return caps = msg.args[2].split() assert caps, 'Empty list of capabilities' log.debug('%s: Server acknowledged capabilities: %L', self.network, caps) self.state.capabilities_ack.update(caps) if 'sasl' in caps: self.tryNextSaslMechanism() else: self.endCapabilityNegociation() def doCapNak(self, msg): if len(msg.args) != 3: log.warning('Bad CAP NAK from server: %r', msg) return caps = msg.args[2].split() assert caps, 'Empty list of capabilities' self.state.capabilities_nak.update(caps) log.warning('%s: Server refused capabilities: %L', self.network, caps) self.endCapabilityNegociation() def _addCapabilities(self, capstring): for item in capstring.split(): while item.startswith(('=', '~')): item = item[1:] if '=' in item: (cap, value) = item.split('=', 1) self.state.capabilities_ls[cap] = value else: self.state.capabilities_ls[item] = None def doCapLs(self, msg): if len(msg.args) == 4: # Multi-line LS if msg.args[2] != '*': log.warning('Bad CAP LS from server: %r', msg) return self._addCapabilities(msg.args[3]) elif len(msg.args) == 3: # End of LS self._addCapabilities(msg.args[2]) common_supported_capabilities = set(self.state.capabilities_ls) & \ self.REQUEST_CAPABILITIES if 'sasl' in self.state.capabilities_ls: s = self.state.capabilities_ls['sasl'] if s is not None: self.filterSaslMechanisms(set(s.split(','))) # NOTE: Capabilities are requested in alphabetic order, because # sets are unordered, and their "order" is nondeterministic. # This is needed for the tests. if common_supported_capabilities: caps = ' '.join(sorted(common_supported_capabilities)) self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('REQ', caps))) else: self.endCapabilityNegociation() else: log.warning('Bad CAP LS from server: %r', msg) return def doCapDel(self, msg): if len(msg.args) != 3: log.warning('Bad CAP DEL from server: %r', msg) return caps = msg.args[2].split() assert caps, 'Empty list of capabilities' for cap in caps: # The spec says "If capability negotiation 3.2 was used, extensions # listed MAY contain values." for CAP NEW and CAP DEL cap = cap.split('=')[0] try: del self.state.capabilities_ls[cap] except KeyError: pass try: self.state.capabilities_ack.remove(cap) except KeyError: pass def doCapNew(self, msg): if len(msg.args) != 3: log.warning('Bad CAP NEW from server: %r', msg) return caps = msg.args[2].split() assert caps, 'Empty list of capabilities' self._addCapabilities(msg.args[2]) if not self.sasl_authenticated and 'sasl' in self.state.capabilities_ls: self.resetSasl() s = self.state.capabilities_ls['sasl'] if s is not None: self.filterSaslMechanisms(set(s.split(','))) common_supported_unrequested_capabilities = ( set(self.state.capabilities_ls) & self.REQUEST_CAPABILITIES - self.state.capabilities_ack) if common_supported_unrequested_capabilities: caps = ' '.join(sorted(common_supported_unrequested_capabilities)) self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('REQ', caps))) def monitor(self, targets): """Increment a counter of how many callbacks monitor each target; and send a MONITOR + to the server if the target is not yet monitored.""" if isinstance(targets, str): targets = [targets] not_yet_monitored = set() for target in targets: if target in self.monitoring: self.monitoring[target] += 1 else: not_yet_monitored.add(target) self.monitoring[target] = 1 if not_yet_monitored: self.queueMsg(ircmsgs.monitor('+', not_yet_monitored)) return not_yet_monitored def unmonitor(self, targets): """Decrements a counter of how many callbacks monitor each target; and send a MONITOR - to the server if the counter drops to 0.""" if isinstance(targets, str): targets = [targets] should_be_unmonitored = set() for target in targets: self.monitoring[target] -= 1 if self.monitoring[target] == 0: del self.monitoring[target] should_be_unmonitored.add(target) if should_be_unmonitored: self.queueMsg(ircmsgs.monitor('-', should_be_unmonitored)) return should_be_unmonitored def _getNextNick(self): if self.alternateNicks: nick = self.alternateNicks.pop(0) if '%s' in nick: network_nick = conf.supybot.networks.get(self.network).nick() if network_nick == '': nick %= conf.supybot.nick() else: nick %= network_nick return nick else: nick = conf.supybot.nick() network_nick = conf.supybot.networks.get(self.network).nick() if network_nick != '': nick = network_nick ret = nick L = list(nick) while len(L) <= 3: L.append('`') while ircutils.strEqual(ret, nick): L[random.randrange(len(L))] = utils.iter.choice('0123456789') ret = ''.join(L) return ret def do002(self, msg): """Logs the ircd version.""" (beginning, version) = rsplit(msg.args[-1], maxsplit=1) log.info('Server %s has version %s', self.server, version) def doPing(self, msg): """Handles PING messages.""" self.sendMsg(ircmsgs.pong(msg.args[0])) def doPong(self, msg): """Handles PONG messages.""" self.outstandingPing = False def do376(self, msg): log.info('Got end of MOTD from %s', self.server) self.afterConnect = True # Let's reset nicks in case we had to use a weird one. self.alternateNicks = conf.supybot.nick.alternates()[:] umodes = conf.supybot.networks.get(self.network).umodes() if umodes == '': umodes = conf.supybot.protocols.irc.umodes() supported = self.state.supported.get('umodes') if supported: acceptedchars = supported.union('+-') umodes = ''.join([m for m in umodes if m in acceptedchars]) if umodes: log.info('Sending user modes to %s: %s', self.network, umodes) self.sendMsg(ircmsgs.mode(self.nick, umodes)) do377 = do422 = do376 def do43x(self, msg, problem): if not self.afterConnect: newNick = self._getNextNick() assert newNick != self.nick log.info('Got %s: %s %s. Trying %s.', msg.command, self.nick, problem, newNick) self.sendMsg(ircmsgs.nick(newNick)) def do437(self, msg): self.do43x(msg, 'is temporarily unavailable') def do433(self, msg): self.do43x(msg, 'is in use') def do432(self, msg): self.do43x(msg, 'is not a valid nickname') def doJoin(self, msg): if msg.nick == self.nick: channel = msg.args[0] self.queueMsg(ircmsgs.who(channel, args=('%tuhnairf,1',))) # Ends with 315. self.queueMsg(ircmsgs.mode(channel)) # Ends with 329. for channel in msg.args[0].split(','): self.queueMsg(ircmsgs.mode(channel, '+b')) self.startedSync[channel] = time.time() def do315(self, msg): channel = msg.args[1] if channel in self.startedSync: now = time.time() started = self.startedSync.pop(channel) elapsed = now - started log.info('Join to %s on %s synced in %.2f seconds.', channel, self.network, elapsed) def doError(self, msg): """Handles ERROR messages.""" log.warning('Error message from %s: %s', self.network, msg.args[0]) if not self.zombie: if msg.args[0].lower().startswith('closing link'): self.driver.reconnect() elif 'too fast' in msg.args[0]: # Connecting too fast. self.driver.reconnect(wait=True) def doNick(self, msg): """Handles NICK messages.""" if msg.nick == self.nick: newNick = msg.args[0] self.nick = newNick (nick, user, domain) = ircutils.splitHostmask(msg.prefix) self.prefix = ircutils.joinHostmask(self.nick, user, domain) elif conf.supybot.followIdentificationThroughNickChanges(): # We use elif here because this means it's someone else's nick # change, not our own. try: id = ircdb.users.getUserId(msg.prefix) u = ircdb.users.getUser(id) except KeyError: return if u.auth: (_, user, host) = ircutils.splitHostmask(msg.prefix) newhostmask = ircutils.joinHostmask(msg.args[0], user, host) for (i, (when, authmask)) in enumerate(u.auth[:]): if ircutils.strEqual(msg.prefix, authmask): log.info('Following identification for %s: %s -> %s', u.name, authmask, newhostmask) u.auth[i] = (u.auth[i][0], newhostmask) ircdb.users.setUser(u) def _reallyDie(self): """Makes the Irc object die. Dead.""" log.info('Irc object for %s dying.', self.network) # XXX This hasattr should be removed, I'm just putting it here because # we're so close to a release. After 0.80.0 we should remove this # and fix whatever AttributeErrors arise in the drivers themselves. if self.driver is not None and hasattr(self.driver, 'die'): self.driver.die() if self in world.ircs: world.ircs.remove(self) # Only kill the callbacks if we're the last Irc. if not world.ircs: for cb in self.callbacks: cb.die() # If we shared our list of callbacks, this ensures that # cb.die() is only called once for each callback. It's # not really necessary since we already check to make sure # we're the only Irc object, but a little robustitude never # hurt anybody. log.debug('Last Irc, clearing callbacks.') self.callbacks[:] = [] else: log.warning('Irc object killed twice: %s', utils.stackTrace()) def __hash__(self): return id(self) def __eq__(self, other): # We check isinstance here, so that if some proxy object (like those # defined in callbacks.py) has overridden __eq__, it takes precedence. if isinstance(other, self.__class__): return id(self) == id(other) else: return other.__eq__(self) def __ne__(self, other): return not (self == other) def __str__(self): return 'Irc object for %s' % self.network def __repr__(self): return '' % self.network # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/ircdb.py0000644000175000017500000013111113233426066015541 0ustar valval00000000000000### # Copyright (c) 2002-2009, Jeremiah Fincher # Copyright (c) 2011, Valentin Lorentz # Copyright (c) 2009,2013, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import time import operator from . import conf, ircutils, log, registry, unpreserve, utils, world from .utils import minisix def isCapability(capability): return len(capability.split(None, 1)) == 1 def fromChannelCapability(capability): """Returns a (channel, capability) tuple from a channel capability.""" assert isChannelCapability(capability), 'got %s' % capability return capability.split(',', 1) def isChannelCapability(capability): """Returns True if capability is a channel capability; False otherwise.""" if ',' in capability: (channel, capability) = capability.split(',', 1) return ircutils.isChannel(channel) and isCapability(capability) else: return False def makeChannelCapability(channel, capability): """Makes a channel capability given a channel and a capability.""" assert isCapability(capability), 'got %s' % capability assert ircutils.isChannel(channel), 'got %s' % channel return '%s,%s' % (channel, capability) def isAntiCapability(capability): """Returns True if capability is an anticapability; False otherwise.""" if isChannelCapability(capability): (_, capability) = fromChannelCapability(capability) return isCapability(capability) and capability[0] == '-' def makeAntiCapability(capability): """Returns the anticapability of a given capability.""" assert isCapability(capability), 'got %s' % capability assert not isAntiCapability(capability), \ 'makeAntiCapability does not work on anticapabilities. ' \ 'You probably want invertCapability; got %s.' % capability if isChannelCapability(capability): (channel, capability) = fromChannelCapability(capability) return makeChannelCapability(channel, '-' + capability) else: return '-' + capability def unAntiCapability(capability): """Takes an anticapability and returns the non-anti form.""" assert isCapability(capability), 'got %s' % capability if not isAntiCapability(capability): raise ValueError('%s is not an anti capability' % capability) if isChannelCapability(capability): (channel, capability) = fromChannelCapability(capability) return ','.join((channel, capability[1:])) else: return capability[1:] def invertCapability(capability): """Make a capability into an anticapability and vice versa.""" assert isCapability(capability), 'got %s' % capability if isAntiCapability(capability): return unAntiCapability(capability) else: return makeAntiCapability(capability) def canonicalCapability(capability): if callable(capability): capability = capability() assert isCapability(capability), 'got %s' % capability return capability.lower() _unwildcard_remover = utils.str.MultipleRemover('!@*?') def unWildcardHostmask(hostmask): return _unwildcard_remover(hostmask) _invert = invertCapability class CapabilitySet(set): """A subclass of set handling basic capability stuff.""" def __init__(self, capabilities=()): self.__parent = super(CapabilitySet, self) self.__parent.__init__() for capability in capabilities: self.add(capability) def add(self, capability): """Adds a capability to the set.""" capability = ircutils.toLower(capability) inverted = _invert(capability) if self.__parent.__contains__(inverted): self.__parent.remove(inverted) self.__parent.add(capability) def remove(self, capability): """Removes a capability from the set.""" capability = ircutils.toLower(capability) self.__parent.remove(capability) def __contains__(self, capability): capability = ircutils.toLower(capability) if self.__parent.__contains__(capability): return True if self.__parent.__contains__(_invert(capability)): return True else: return False def check(self, capability, ignoreOwner=False): """Returns the appropriate boolean for whether a given capability is 'allowed' given its (or its anticapability's) presence in the set. """ capability = ircutils.toLower(capability) if self.__parent.__contains__(capability): return True elif self.__parent.__contains__(_invert(capability)): return False else: raise KeyError def __repr__(self): return '%s([%s])' % (self.__class__.__name__, ', '.join(map(repr, self))) antiOwner = makeAntiCapability('owner') class UserCapabilitySet(CapabilitySet): """A subclass of CapabilitySet to handle the owner capability correctly.""" def __init__(self, *args, **kwargs): self.__parent = super(UserCapabilitySet, self) self.__parent.__init__(*args, **kwargs) def __contains__(self, capability, ignoreOwner=False): capability = ircutils.toLower(capability) if not ignoreOwner and capability == 'owner' or capability == antiOwner: return True elif not ignoreOwner and self.__parent.__contains__('owner'): return True else: return self.__parent.__contains__(capability) def check(self, capability, ignoreOwner=False): """Returns the appropriate boolean for whether a given capability is 'allowed' given its (or its anticapability's) presence in the set. Differs from CapabilitySet in that it handles the 'owner' capability appropriately. """ capability = ircutils.toLower(capability) if capability == 'owner' or capability == antiOwner: if self.__parent.__contains__('owner'): return not isAntiCapability(capability) else: return isAntiCapability(capability) elif not ignoreOwner and self.__parent.__contains__('owner'): if isAntiCapability(capability): return False else: return True else: return self.__parent.check(capability) def add(self, capability): """Adds a capability to the set. Just make sure it's not -owner.""" capability = ircutils.toLower(capability) assert capability != '-owner', '"-owner" disallowed.' self.__parent.add(capability) class IrcUser(object): """This class holds the capabilities and authentications for a user.""" def __init__(self, ignore=False, password='', name='', capabilities=(), hostmasks=None, nicks=None, secure=False, hashed=False): self.id = None self.auth = [] # The (time, hostmask) list of auth crap. self.name = name # The name of the user. self.ignore = ignore # A boolean deciding if the person is ignored. self.secure = secure # A boolean describing if hostmasks *must* match. self.hashed = hashed # True if the password is hashed on disk. self.password = password # password (plaintext? hashed?) self.capabilities = UserCapabilitySet() for capability in capabilities: self.capabilities.add(capability) if hostmasks is None: self.hostmasks = ircutils.IrcSet() # hostmasks used for recognition else: self.hostmasks = hostmasks if nicks is None: self.nicks = {} # {'network1': ['foo', 'bar'], 'network': ['baz']} else: self.nicks = nicks self.gpgkeys = [] # GPG key ids def __repr__(self): return format('%s(id=%s, ignore=%s, password="", name=%q, hashed=%r, ' 'capabilities=%r, hostmasks=[], secure=%r)\n', self.__class__.__name__, self.id, self.ignore, self.name, self.hashed, self.capabilities, self.secure) def __hash__(self): return hash(self.id) def addCapability(self, capability): """Gives the user the given capability.""" self.capabilities.add(capability) def removeCapability(self, capability): """Takes from the user the given capability.""" self.capabilities.remove(capability) def _checkCapability(self, capability, ignoreOwner=False): """Checks the user for a given capability.""" if self.ignore: if isAntiCapability(capability): return True else: return False else: return self.capabilities.check(capability, ignoreOwner=ignoreOwner) def setPassword(self, password, hashed=False): """Sets the user's password.""" if hashed or self.hashed: self.hashed = True self.password = utils.saltHash(password) else: self.password = password def checkPassword(self, password): """Checks the user's password.""" if password is None: return False if self.hashed: (salt, _) = self.password.split('|') return (self.password == utils.saltHash(password, salt=salt)) else: return (self.password == password) def checkHostmask(self, hostmask, useAuth=True): """Checks a given hostmask against the user's hostmasks or current authentication. If useAuth is False, only checks against the user's hostmasks. """ if useAuth: timeout = conf.supybot.databases.users.timeoutIdentification() removals = [] try: for (when, authmask) in self.auth: if timeout and when+timeout < time.time(): removals.append((when, authmask)) elif hostmask == authmask: return True finally: while removals: self.auth.remove(removals.pop()) for pat in self.hostmasks: if ircutils.hostmaskPatternEqual(pat, hostmask): return pat return False def addHostmask(self, hostmask): """Adds a hostmask to the user's hostmasks.""" assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask if len(unWildcardHostmask(hostmask)) < 3: raise ValueError('Hostmask must contain at least 3 non-wildcard characters.') self.hostmasks.add(hostmask) def removeHostmask(self, hostmask): """Removes a hostmask from the user's hostmasks.""" self.hostmasks.remove(hostmask) def checkNick(self, network, nick): """Checks a given nick against the user's nicks.""" return nick in self.nicks[network] def addNick(self, network, nick): """Adds a nick to the user's registered nicks on the network.""" global users assert isinstance(network, minisix.string_types) assert ircutils.isNick(nick), 'got %s' % nick if users.getUserFromNick(network, nick) is not None: raise KeyError if network not in self.nicks: self.nicks[network] = [] if nick not in self.nicks[network]: self.nicks[network].append(nick) def removeNick(self, network, nick): """Removes a nick from the user's registered nicks on the network.""" assert isinstance(network, minisix.string_types) if nick not in self.nicks[network]: raise KeyError self.nicks[network].remove(nick) def addAuth(self, hostmask): """Sets a user's authenticated hostmask. This times out according to conf.supybot.timeoutIdentification. If hostmask exactly matches an existing, known hostmask, the previous entry is removed.""" if self.checkHostmask(hostmask, useAuth=False) or not self.secure: self.auth.append((time.time(), hostmask)) knownHostmasks = set() def uniqueHostmask(auth): (_, mask) = auth if mask not in knownHostmasks: knownHostmasks.add(mask) return True return False uniqued = list(filter(uniqueHostmask, reversed(self.auth))) self.auth = list(reversed(uniqued)) else: raise ValueError('secure flag set, unmatched hostmask') def clearAuth(self): """Unsets a user's authenticated hostmask.""" for (when, hostmask) in self.auth: users.invalidateCache(hostmask=hostmask) self.auth = [] def preserve(self, fd, indent=''): def write(s): fd.write(indent) fd.write(s) fd.write(os.linesep) write('name %s' % self.name) write('ignore %s' % self.ignore) write('secure %s' % self.secure) if self.password: write('hashed %s' % self.hashed) write('password %s' % self.password) for capability in self.capabilities: write('capability %s' % capability) for hostmask in self.hostmasks: write('hostmask %s' % hostmask) for network, nicks in self.nicks.items(): write('nicks %s %s' % (network, ' '.join(nicks))) for key in self.gpgkeys: write('gpgkey %s' % key) fd.write(os.linesep) class IrcChannel(object): """This class holds the capabilities, bans, and ignores of a channel.""" defaultOff = ('op', 'halfop', 'voice', 'protected') def __init__(self, bans=None, silences=None, exceptions=None, ignores=None, capabilities=None, lobotomized=False, defaultAllow=True): self.defaultAllow = defaultAllow self.expiredBans = [] self.bans = bans or {} self.ignores = ignores or {} self.silences = silences or [] self.exceptions = exceptions or [] self.capabilities = capabilities or CapabilitySet() for capability in self.defaultOff: if capability not in self.capabilities: self.capabilities.add(makeAntiCapability(capability)) self.lobotomized = lobotomized def __repr__(self): return '%s(bans=%r, ignores=%r, capabilities=%r, ' \ 'lobotomized=%r, defaultAllow=%s, ' \ 'silences=%r, exceptions=%r)\n' % \ (self.__class__.__name__, self.bans, self.ignores, self.capabilities, self.lobotomized, self.defaultAllow, self.silences, self.exceptions) def addBan(self, hostmask, expiration=0): """Adds a ban to the channel banlist.""" assert not conf.supybot.protocols.irc.strictRfc() or \ ircutils.isUserHostmask(hostmask), 'got %s' % hostmask self.bans[hostmask] = int(expiration) def removeBan(self, hostmask): """Removes a ban from the channel banlist.""" assert not conf.supybot.protocols.irc.strictRfc() or \ ircutils.isUserHostmask(hostmask), 'got %s' % hostmask return self.bans.pop(hostmask) def checkBan(self, hostmask): """Checks whether a given hostmask is banned by the channel banlist.""" assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask now = time.time() for (pattern, expiration) in list(self.bans.items()): if now < expiration or not expiration: if ircutils.hostmaskPatternEqual(pattern, hostmask): return True else: self.expiredBans.append((pattern, expiration)) del self.bans[pattern] return False def addIgnore(self, hostmask, expiration=0): """Adds an ignore to the channel ignore list.""" assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask self.ignores[hostmask] = int(expiration) def removeIgnore(self, hostmask): """Removes an ignore from the channel ignore list.""" assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask return self.ignores.pop(hostmask) def addCapability(self, capability): """Adds a capability to the channel's default capabilities.""" assert isCapability(capability), 'got %s' % capability self.capabilities.add(capability) def removeCapability(self, capability): """Removes a capability from the channel's default capabilities.""" assert isCapability(capability), 'got %s' % capability self.capabilities.remove(capability) def setDefaultCapability(self, b): """Sets the default capability in the channel.""" self.defaultAllow = b def _checkCapability(self, capability, ignoreOwner=False): """Checks whether a certain capability is allowed by the channel.""" assert isCapability(capability), 'got %s' % capability if capability in self.capabilities: return self.capabilities.check(capability, ignoreOwner=ignoreOwner) else: if isAntiCapability(capability): return not self.defaultAllow else: return self.defaultAllow def checkIgnored(self, hostmask): """Checks whether a given hostmask is to be ignored by the channel.""" if self.lobotomized: return True if world.testing: return False assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask if self.checkBan(hostmask): return True now = time.time() for (pattern, expiration) in list(self.ignores.items()): if now < expiration or not expiration: if ircutils.hostmaskPatternEqual(pattern, hostmask): return True else: del self.ignores[pattern] # Later we may wish to keep expiredIgnores, but not now. return False def preserve(self, fd, indent=''): def write(s): fd.write(indent) fd.write(s) fd.write(os.linesep) write('lobotomized %s' % self.lobotomized) write('defaultAllow %s' % self.defaultAllow) for capability in self.capabilities: write('capability ' + capability) bans = list(self.bans.items()) utils.sortBy(operator.itemgetter(1), bans) for (ban, expiration) in bans: write('ban %s %d' % (ban, expiration)) ignores = list(self.ignores.items()) utils.sortBy(operator.itemgetter(1), ignores) for (ignore, expiration) in ignores: write('ignore %s %d' % (ignore, expiration)) fd.write(os.linesep) class Creator(object): def badCommand(self, command, rest, lineno): raise ValueError('Invalid command on line %s: %s' % (lineno, command)) class IrcUserCreator(Creator): u = None def __init__(self, users): if self.u is None: IrcUserCreator.u = IrcUser() self.users = users def user(self, rest, lineno): if self.u.id is not None: raise ValueError('Unexpected user command on line %s.' % lineno) self.u.id = int(rest) def _checkId(self): if self.u.id is None: raise ValueError('Unexpected user description without user.') def name(self, rest, lineno): self._checkId() self.u.name = rest def ignore(self, rest, lineno): self._checkId() self.u.ignore = bool(utils.gen.safeEval(rest)) def secure(self, rest, lineno): self._checkId() self.u.secure = bool(utils.gen.safeEval(rest)) def hashed(self, rest, lineno): self._checkId() self.u.hashed = bool(utils.gen.safeEval(rest)) def password(self, rest, lineno): self._checkId() self.u.password = rest def hostmask(self, rest, lineno): self._checkId() self.u.hostmasks.add(rest) def nicks(self, rest, lineno): self._checkId() network, nicks = rest.split(' ', 1) self.u.nicks[network] = nicks.split(' ') def capability(self, rest, lineno): self._checkId() self.u.capabilities.add(rest) def gpgkey(self, rest, lineno): self._checkId() self.u.gpgkeys.append(rest) def finish(self): if self.u.name: try: self.users.setUser(self.u) except DuplicateHostmask: log.error('Hostmasks for %s collided with another user\'s. ' 'Resetting hostmasks for %s.', self.u.name, self.u.name) # Some might argue that this is arbitrary, and perhaps it is. # But we've got to do *something*, so we'll show some deference # to our lower-numbered users. self.u.hostmasks.clear() self.users.setUser(self.u) IrcUserCreator.u = None class IrcChannelCreator(Creator): name = None def __init__(self, channels): self.c = IrcChannel() self.channels = channels self.hadChannel = bool(self.name) def channel(self, rest, lineno): if self.name is not None: raise ValueError('Unexpected channel command on line %s' % lineno) IrcChannelCreator.name = rest def _checkId(self): if self.name is None: raise ValueError('Unexpected channel description without channel.') def lobotomized(self, rest, lineno): self._checkId() self.c.lobotomized = bool(utils.gen.safeEval(rest)) def defaultallow(self, rest, lineno): self._checkId() self.c.defaultAllow = bool(utils.gen.safeEval(rest)) def capability(self, rest, lineno): self._checkId() self.c.capabilities.add(rest) def ban(self, rest, lineno): self._checkId() (pattern, expiration) = rest.split() self.c.bans[pattern] = int(float(expiration)) def ignore(self, rest, lineno): self._checkId() (pattern, expiration) = rest.split() self.c.ignores[pattern] = int(float(expiration)) def finish(self): if self.hadChannel: self.channels.setChannel(self.name, self.c) IrcChannelCreator.name = None class DuplicateHostmask(ValueError): pass class UsersDictionary(utils.IterableMap): """A simple serialized-to-file User Database.""" def __init__(self): self.noFlush = False self.filename = None self.users = {} self.nextId = 0 self._nameCache = utils.structures.CacheDict(1000) self._hostmaskCache = utils.structures.CacheDict(1000) # This is separate because the Creator has to access our instance. def open(self, filename): self.filename = filename reader = unpreserve.Reader(IrcUserCreator, self) try: self.noFlush = True try: reader.readFile(filename) self.noFlush = False self.flush() except EnvironmentError as e: log.error('Invalid user dictionary file, resetting to empty.') log.error('Exact error: %s', utils.exnToString(e)) except Exception as e: log.exception('Exact error:') finally: self.noFlush = False def reload(self): """Reloads the database from its file.""" self.nextId = 0 self.users.clear() self._nameCache.clear() self._hostmaskCache.clear() if self.filename is not None: try: self.open(self.filename) except EnvironmentError as e: log.warning('UsersDictionary.reload failed: %s', e) else: log.error('UsersDictionary.reload called with no filename.') def flush(self): """Flushes the database to its file.""" if not self.noFlush: if self.filename is not None: L = list(self.users.items()) L.sort() fd = utils.file.AtomicFile(self.filename) for (id, u) in L: fd.write('user %s' % id) fd.write(os.linesep) u.preserve(fd, indent=' ') fd.close() else: log.error('UsersDictionary.flush called with no filename.') else: log.debug('Not flushing UsersDictionary because of noFlush.') def close(self): self.flush() if self.flush in world.flushers: world.flushers.remove(self.flush) self.users.clear() def items(self): return self.users.items() def getUserId(self, s): """Returns the user ID of a given name or hostmask.""" if ircutils.isUserHostmask(s): try: return self._hostmaskCache[s] except KeyError: ids = {} for (id, user) in self.users.items(): x = user.checkHostmask(s) if x: ids[id] = x if len(ids) == 1: id = list(ids.keys())[0] self._hostmaskCache[s] = id try: self._hostmaskCache[id].add(s) except KeyError: self._hostmaskCache[id] = set([s]) return id elif len(ids) == 0: raise KeyError(s) else: log.error('Multiple matches found in user database. ' 'Removing the offending hostmasks.') for (id, hostmask) in ids.items(): log.error('Removing %q from user %s.', hostmask, id) self.users[id].removeHostmask(hostmask) raise DuplicateHostmask('Ids %r matched.' % ids) else: # Not a hostmask, must be a name. s = s.lower() try: return self._nameCache[s] except KeyError: for (id, user) in self.users.items(): if s == user.name.lower(): self._nameCache[s] = id self._nameCache[id] = s return id else: raise KeyError(s) def getUser(self, id): """Returns a user given its id, name, or hostmask.""" if not isinstance(id, int): # Must be a string. Get the UserId first. id = self.getUserId(id) u = self.users[id] while isinstance(u, int): id = u u = self.users[id] u.id = id return u def getUserFromNick(self, network, nick): """Return a user given its nick.""" for user in self.users.values(): try: if nick in user.nicks[network]: return user except KeyError: pass return None def hasUser(self, id): """Returns the database has a user given its id, name, or hostmask.""" try: self.getUser(id) return True except KeyError: return False def numUsers(self): return len(self.users) def invalidateCache(self, id=None, hostmask=None, name=None): if hostmask is not None: if hostmask in self._hostmaskCache: id = self._hostmaskCache.pop(hostmask) self._hostmaskCache[id].remove(hostmask) if not self._hostmaskCache[id]: del self._hostmaskCache[id] if name is not None: del self._nameCache[self._nameCache[id]] del self._nameCache[id] if id is not None: if id in self._nameCache: del self._nameCache[self._nameCache[id]] del self._nameCache[id] if id in self._hostmaskCache: for hostmask in self._hostmaskCache[id]: del self._hostmaskCache[hostmask] del self._hostmaskCache[id] def setUser(self, user, flush=True): """Sets a user (given its id) to the IrcUser given it.""" self.nextId = max(self.nextId, user.id) try: if self.getUserId(user.name) != user.id: raise DuplicateHostmask(user.name, user.name) except KeyError: pass for hostmask in user.hostmasks: for (i, u) in self.items(): if i == user.id: continue elif u.checkHostmask(hostmask): # We used to remove the hostmask here, but it's not # appropriate for us both to remove the hostmask and to # raise an exception. So instead, we'll raise an # exception, but be nice and give the offending hostmask # back at the same time. raise DuplicateHostmask(u.name, hostmask) for otherHostmask in u.hostmasks: if ircutils.hostmaskPatternEqual(hostmask, otherHostmask): raise DuplicateHostmask(u.name, hostmask) self.invalidateCache(user.id) self.users[user.id] = user if flush: self.flush() def delUser(self, id): """Removes a user from the database.""" del self.users[id] if id in self._nameCache: del self._nameCache[self._nameCache[id]] del self._nameCache[id] if id in self._hostmaskCache: for hostmask in list(self._hostmaskCache[id]): del self._hostmaskCache[hostmask] del self._hostmaskCache[id] self.flush() def newUser(self): """Allocates a new user in the database and returns it and its id.""" user = IrcUser(hashed=True) self.nextId += 1 id = self.nextId self.users[id] = user self.flush() user.id = id return user class ChannelsDictionary(utils.IterableMap): def __init__(self): self.noFlush = False self.filename = None self.channels = ircutils.IrcDict() def open(self, filename): self.noFlush = True try: self.filename = filename reader = unpreserve.Reader(IrcChannelCreator, self) try: reader.readFile(filename) self.noFlush = False self.flush() except EnvironmentError as e: log.error('Invalid channel database, resetting to empty.') log.error('Exact error: %s', utils.exnToString(e)) except Exception as e: log.error('Invalid channel database, resetting to empty.') log.exception('Exact error:') finally: self.noFlush = False def flush(self): """Flushes the channel database to its file.""" if not self.noFlush: if self.filename is not None: fd = utils.file.AtomicFile(self.filename) for (channel, c) in self.channels.items(): fd.write('channel %s' % channel) fd.write(os.linesep) c.preserve(fd, indent=' ') fd.close() else: log.warning('ChannelsDictionary.flush without self.filename.') else: log.debug('Not flushing ChannelsDictionary because of noFlush.') def close(self): self.flush() if self.flush in world.flushers: world.flushers.remove(self.flush) self.channels.clear() def reload(self): """Reloads the channel database from its file.""" if self.filename is not None: self.channels.clear() try: self.open(self.filename) except EnvironmentError as e: log.warning('ChannelsDictionary.reload failed: %s', e) else: log.warning('ChannelsDictionary.reload without self.filename.') def getChannel(self, channel): """Returns an IrcChannel object for the given channel.""" channel = channel.lower() if channel in self.channels: return self.channels[channel] else: c = IrcChannel() self.channels[channel] = c return c def setChannel(self, channel, ircChannel): """Sets a given channel to the IrcChannel object given.""" channel = channel.lower() self.channels[channel] = ircChannel self.flush() def items(self): return self.channels.items() class IgnoresDB(object): def __init__(self): self.filename = None self.hostmasks = {} def open(self, filename): self.filename = filename fd = open(self.filename) for line in utils.file.nonCommentNonEmptyLines(fd): try: line = line.rstrip('\r\n') L = line.split() hostmask = L.pop(0) if L: expiration = int(float(L.pop(0))) else: expiration = 0 self.add(hostmask, expiration) except Exception: log.error('Invalid line in ignores database: %q', line) fd.close() def flush(self): if self.filename is not None: fd = utils.file.AtomicFile(self.filename) now = time.time() for (hostmask, expiration) in self.hostmasks.items(): if now < expiration or not expiration: fd.write('%s %s' % (hostmask, expiration)) fd.write(os.linesep) fd.close() else: log.warning('IgnoresDB.flush called without self.filename.') def close(self): if self.flush in world.flushers: world.flushers.remove(self.flush) self.flush() self.hostmasks.clear() def reload(self): if self.filename is not None: oldhostmasks = self.hostmasks.copy() self.hostmasks.clear() try: self.open(self.filename) except EnvironmentError as e: log.warning('IgnoresDB.reload failed: %s', e) # Let's be somewhat transactional. self.hostmasks.update(oldhostmasks) else: log.warning('IgnoresDB.reload called without self.filename.') def checkIgnored(self, prefix): now = time.time() for (hostmask, expiration) in list(self.hostmasks.items()): if expiration and now > expiration: del self.hostmasks[hostmask] else: if ircutils.hostmaskPatternEqual(hostmask, prefix): return True return False def add(self, hostmask, expiration=0): assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask self.hostmasks[hostmask] = expiration def remove(self, hostmask): del self.hostmasks[hostmask] confDir = conf.supybot.directories.conf() try: userFile = os.path.join(confDir, conf.supybot.databases.users.filename()) users = UsersDictionary() users.open(userFile) except EnvironmentError as e: log.warning('Couldn\'t open user database: %s', e) try: channelFile = os.path.join(confDir, conf.supybot.databases.channels.filename()) channels = ChannelsDictionary() channels.open(channelFile) except EnvironmentError as e: log.warning('Couldn\'t open channel database: %s', e) try: ignoreFile = os.path.join(confDir, conf.supybot.databases.ignores.filename()) ignores = IgnoresDB() ignores.open(ignoreFile) except EnvironmentError as e: log.warning('Couldn\'t open ignore database: %s', e) world.flushers.append(users.flush) world.flushers.append(ignores.flush) world.flushers.append(channels.flush) ### # Useful functions for checking credentials. ### def checkIgnored(hostmask, recipient='', users=users, channels=channels): """checkIgnored(hostmask, recipient='') -> True/False Checks if the user is ignored by the recipient of the message. """ try: id = users.getUserId(hostmask) user = users.getUser(id) if user._checkCapability('owner'): # Owners shouldn't ever be ignored. return False elif user.ignore: log.debug('Ignoring %s due to their IrcUser ignore flag.', hostmask) return True except KeyError: # If there's no user... if conf.supybot.defaultIgnore(): log.debug('Ignoring %s due to conf.supybot.defaultIgnore', hostmask) return True if ignores.checkIgnored(hostmask): log.debug('Ignoring %s due to ignore database.', hostmask) return True if ircutils.isChannel(recipient): channel = channels.getChannel(recipient) if channel.checkIgnored(hostmask): log.debug('Ignoring %s due to the channel ignores.', hostmask) return True return False def _x(capability, ret): if isAntiCapability(capability): return not ret else: return ret def _checkCapabilityForUnknownUser(capability, users=users, channels=channels, ignoreDefaultAllow=False): if isChannelCapability(capability): (channel, capability) = fromChannelCapability(capability) try: c = channels.getChannel(channel) if capability in c.capabilities: return c._checkCapability(capability) else: return _x(capability, (not ignoreDefaultAllow) and c.defaultAllow) except KeyError: pass defaultCapabilities = conf.supybot.capabilities() if capability in defaultCapabilities: return defaultCapabilities.check(capability) elif ignoreDefaultAllow: return _x(capability, False) else: return _x(capability, conf.supybot.capabilities.default()) def checkCapability(hostmask, capability, users=users, channels=channels, ignoreOwner=False, ignoreChannelOp=False, ignoreDefaultAllow=False): """Checks that the user specified by name/hostmask has the capability given. ``users`` and ``channels`` default to ``ircdb.users`` and ``ircdb.channels``. ``ignoreOwner``, ``ignoreChannelOp``, and ``ignoreDefaultAllow`` are used to override default behavior of the capability system in special cases (actually, in the AutoMode plugin): * ``ignoreOwner`` disables the behavior "owners have all capabilites" * ``ignoreChannelOp`` disables the behavior "channel ops have all channel capabilities" * ``ignoreDefaultAllow`` disables the behavior "if a user does not have a capability or the associated anticapability, then they have the capability" """ if world.testing and (not isinstance(hostmask, str) or '@' not in hostmask or '__no_testcap__' not in hostmask.split('@')[1]): return _x(capability, True) try: u = users.getUser(hostmask) if u.secure and not u.checkHostmask(hostmask, useAuth=False): raise KeyError except KeyError: # Raised when no hostmasks match. return _checkCapabilityForUnknownUser(capability, users=users, channels=channels, ignoreDefaultAllow=ignoreDefaultAllow) except ValueError as e: # Raised when multiple hostmasks match. log.warning('%s: %s', hostmask, e) return _checkCapabilityForUnknownUser(capability, users=users, channels=channels, ignoreDefaultAllow=ignoreDefaultAllow) if capability in u.capabilities: try: return u._checkCapability(capability, ignoreOwner) except KeyError: pass if isChannelCapability(capability): (channel, capability) = fromChannelCapability(capability) if not ignoreChannelOp: try: chanop = makeChannelCapability(channel, 'op') if u._checkCapability(chanop): return _x(capability, True) except KeyError: pass c = channels.getChannel(channel) if capability in c.capabilities: return c._checkCapability(capability) elif not ignoreDefaultAllow: return _x(capability, c.defaultAllow) else: return False defaultCapabilities = conf.supybot.capabilities() defaultCapabilitiesRegistered = conf.supybot.capabilities.registeredUsers() if capability in defaultCapabilities: return defaultCapabilities.check(capability) elif capability in defaultCapabilitiesRegistered: return defaultCapabilitiesRegistered.check(capability) elif ignoreDefaultAllow: return _x(capability, False) else: return _x(capability, conf.supybot.capabilities.default()) def checkCapabilities(hostmask, capabilities, requireAll=False): """Checks that a user has capabilities in a list. requireAll is True if *all* capabilities in the list must be had, False if *any* of the capabilities in the list must be had. """ for capability in capabilities: if requireAll: if not checkCapability(hostmask, capability): return False else: if checkCapability(hostmask, capability): return True return requireAll ### # supybot.capabilities ### class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedListOfStrings): List = CapabilitySet class DefaultCapabilities(SpaceSeparatedListOfCapabilities): # We use a keyword argument trick here to prevent eval'ing of code that # changes allowDefaultOwner from affecting this. It's not perfect, but # it's still an improvement, raising the bar for potential crackers. def setValue(self, v, allowDefaultOwner=conf.allowDefaultOwner): registry.SpaceSeparatedListOfStrings.setValue(self, v) if '-owner' not in self.value and not allowDefaultOwner: print('*** You must run supybot with the --allow-default-owner') print('*** option in order to allow a default capability of owner.') print('*** Don\'t do that, it\'s dumb.') self.value.add('-owner') conf.registerGlobalValue(conf.supybot, 'capabilities', DefaultCapabilities(['-owner', '-admin', '-trusted'], """These are the capabilities that are given to everyone by default. If they are normal capabilities, then the user will have to have the appropriate anti-capability if you want to override these capabilities; if they are anti-capabilities, then the user will have to have the actual capability to override these capabilities. See docs/CAPABILITIES if you don't understand why these default to what they do.""")) conf.registerGlobalValue(conf.supybot.capabilities, 'registeredUsers', SpaceSeparatedListOfCapabilities([], """These are the capabilities that are given to every authenticated user by default. You probably want to use supybot.capabilities instead, to give these capabilities both to registered and non-registered users.""")) conf.registerGlobalValue(conf.supybot.capabilities, 'default', registry.Boolean(True, """Determines whether the bot by default will allow users to have a capability. If this is disabled, a user must explicitly have the capability for whatever command they wish to run.""")) conf.registerGlobalValue(conf.supybot.capabilities, 'private', registry.SpaceSeparatedListOfStrings([], """Determines what capabilities the bot will never tell to a non-admin whether or not a user has them.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/i18n.py0000644000175000017500000003306513233426066015246 0ustar valval00000000000000### # 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 internationalisation and localisation managment. """ __all__ = ['PluginInternationalization', 'internationalizeDocstring'] import os import sys import weakref conf = None # Don't import conf here ; because conf needs this module WAITING_FOR_MSGID = 1 IN_MSGID = 2 WAITING_FOR_MSGSTR = 3 IN_MSGSTR = 4 MSGID = 'msgid "' MSGSTR = 'msgstr "' currentLocale = 'en' class PluginNotFound(Exception): pass def getLocaleFromRegistryFilename(filename): """Called by the 'supybot' script. Gets the locale name before conf is loaded.""" global currentLocale with open(filename, 'r') as fd: for line in fd: if line.startswith('supybot.language: '): currentLocale = line[len('supybot.language: '):] def import_conf(): """Imports the conf into this module""" global conf conf = __import__('supybot.conf').conf conf.registerGlobalValue(conf.supybot, 'language', conf.registry.String(currentLocale, """Determines the bot's default language if translations exist. Currently supported are 'de', 'en', 'es', 'fi', 'fr' and 'it'.""")) conf.supybot.language.addCallback(reloadLocalesIfRequired) def getPluginDir(plugin_name): """Gets the directory of the given plugin""" filename = None try: filename = sys.modules[plugin_name].__file__ except KeyError: # It sometimes happens with Owner pass if filename == None: try: filename = sys.modules['supybot.plugins.' + plugin_name].__file__ except: # In the case where the plugin is not loaded by Supybot try: filename = sys.modules['plugin'].__file__ except: filename = sys.modules['__main__'].__file__ if filename.endswith(".pyc"): filename = filename[0:-1] allowed_files = ['__init__.py', 'config.py', 'plugin.py', 'test.py'] for allowed_file in allowed_files: if filename.endswith(allowed_file): return filename[0:-len(allowed_file)] raise PluginNotFound() def getLocalePath(name, localeName, extension): """Gets the path of the locale file of the given plugin ('supybot' stands for the core).""" if name != 'supybot': base = getPluginDir(name) else: from . import ansi # Any Supybot plugin could fit base = ansi.__file__[0:-len('ansi.pyc')] directory = os.path.join(base, 'locales') return '%s/%s.%s' % (directory, localeName, extension) i18nClasses = weakref.WeakValueDictionary() internationalizedCommands = weakref.WeakValueDictionary() internationalizedFunctions = [] # No need to know their name def reloadLocalesIfRequired(): global currentLocale if conf is None: return if currentLocale != conf.supybot.language(): currentLocale = conf.supybot.language() reloadLocales() def reloadLocales(): for pluginClass in i18nClasses.values(): pluginClass.loadLocale() for command in list(internationalizedCommands.values()): internationalizeDocstring(command) for function in internationalizedFunctions: function.loadLocale() def normalize(string, removeNewline=False): import supybot.utils as utils string = string.replace('\\n\\n', '\n\n') string = string.replace('\\n', ' ') string = string.replace('\\"', '"') string = string.replace("\'", "'") string = utils.str.normalizeWhitespace(string, removeNewline) string = string.strip('\n') string = string.strip('\t') return string def parse(translationFile): step = WAITING_FOR_MSGID translations = set() for line in translationFile: line = line[0:-1] # Remove the ending \n line = line if line.startswith(MSGID): # Don't check if step is WAITING_FOR_MSGID untranslated = '' translated = '' data = line[len(MSGID):-1] if len(data) == 0: # Multiline mode step = IN_MSGID else: untranslated += data step = WAITING_FOR_MSGSTR elif step is IN_MSGID and line.startswith('"') and \ line.endswith('"'): untranslated += line[1:-1] elif step is IN_MSGID and untranslated == '': # Empty MSGID step = WAITING_FOR_MSGID elif step is IN_MSGID: # the MSGID is finished step = WAITING_FOR_MSGSTR if step is WAITING_FOR_MSGSTR and line.startswith(MSGSTR): data = line[len(MSGSTR):-1] if len(data) == 0: # Multiline mode step = IN_MSGSTR else: translations |= set([(untranslated, data)]) step = WAITING_FOR_MSGID elif step is IN_MSGSTR and line.startswith('"') and \ line.endswith('"'): translated += line[1:-1] elif step is IN_MSGSTR: # the MSGSTR is finished step = WAITING_FOR_MSGID if translated == '': translated = untranslated translations |= set([(untranslated, translated)]) if step is IN_MSGSTR: if translated == '': translated = untranslated translations |= set([(untranslated, translated)]) return translations i18nSupybot = None def PluginInternationalization(name='supybot'): # This is a proxy that prevents having several objects for the same plugin if name in i18nClasses: return i18nClasses[name] else: return _PluginInternationalization(name) class _PluginInternationalization: """Internationalization managment for a plugin.""" def __init__(self, name='supybot'): self.name = name self.translations = {} self.currentLocaleName = None i18nClasses.update({name: self}) self.loadLocale() def loadLocale(self, localeName=None): """(Re)loads the locale used by this class.""" self.translations = {} if localeName is None: localeName = currentLocale self.currentLocaleName = localeName self._loadL10nCode() try: try: translationFile = open(getLocalePath(self.name, localeName, 'po'), 'ru') except ValueError: # We are using Windows translationFile = open(getLocalePath(self.name, localeName, 'po'), 'r') self._parse(translationFile) except (IOError, PluginNotFound): # The translation is unavailable pass finally: if 'translationFile' in locals(): translationFile.close() def _parse(self, translationFile): """A .po files parser. Give it a file object.""" self.translations = {} for translation in parse(translationFile): self._addToDatabase(*translation) def _addToDatabase(self, untranslated, translated): untranslated = normalize(untranslated, True) translated = normalize(translated) if translated: self.translations.update({untranslated: translated}) def __call__(self, untranslated): """Main function. This is the function which is called when a plugin runs _()""" normalizedUntranslated = normalize(untranslated, True) try: string = self._translate(normalizedUntranslated) return self._addTracker(string, untranslated) except KeyError: pass if untranslated.__class__ is InternationalizedString: return untranslated._original else: return untranslated def _translate(self, string): """Translate the string. C the string internationalizer if any; else, use the local database""" if string.__class__ == InternationalizedString: return string._internationalizer(string.untranslated) else: return self.translations[string] def _addTracker(self, string, untranslated): """Add a kind of 'tracker' on the string, in order to keep the untranslated string (used when changing the locale)""" if string.__class__ == InternationalizedString: return string else: string = InternationalizedString(string) string._original = untranslated string._internationalizer = self return string def _loadL10nCode(self): """Open the file containing the code specific to this locale, and load its functions.""" if self.name != 'supybot': return path = self._getL10nCodePath() try: with open(path) as fd: exec(compile(fd.read(), path, 'exec')) except IOError: # File doesn't exist pass functions = locals() functions.pop('self') self._l10nFunctions = functions # Remove old functions and come back to the native language def _getL10nCodePath(self): """Returns the path to the code localization file. It contains functions that needs to by fully (code + strings) localized""" if self.name != 'supybot': return return getLocalePath('supybot', self.currentLocaleName, 'py') def localizeFunction(self, name): """Returns the localized version of the function. Should be used only by the InternationalizedFunction class""" if self.name != 'supybot': return if hasattr(self, '_l10nFunctions') and \ name in self._l10nFunctions: return self._l10nFunctions[name] def internationalizeFunction(self, name): """Decorates functions and internationalize their code. Only useful for Supybot core functions""" if self.name != 'supybot': return class FunctionInternationalizer: def __init__(self, parent, name): self._parent = parent self._name = name def __call__(self, obj): obj = InternationalizedFunction(self._parent, self._name, obj) obj.loadLocale() return obj return FunctionInternationalizer(self, name) class InternationalizedFunction: """Proxy for functions that need to be fully localized. The localization code is in locales/LOCALE.py""" def __init__(self, internationalizer, name, function): self._internationalizer = internationalizer self._name = name self._origin = function internationalizedFunctions.append(self) def loadLocale(self): self.__call__ = self._internationalizer.localizeFunction(self._name) if self.__call__ == None: self.restore() def restore(self): self.__call__ = self._origin def __call__(self, *args, **kwargs): return self._origin(*args, **kwargs) try: class InternationalizedString(str): """Simple subclass to str, that allow to add attributes. Also used to know if a string is already localized""" __slots__ = ('_original', '_internationalizer') except TypeError: # Fallback for CPython 2.x: # TypeError: Error when calling the metaclass bases # nonempty __slots__ not supported for subtype of 'str' class InternationalizedString(str): """Simple subclass to str, that allow to add attributes. Also used to know if a string is already localized""" pass def internationalizeDocstring(obj): """Decorates functions and internationalize their docstring. Only useful for commands (commands' docstring is displayed on IRC)""" if obj.__doc__ == None: return obj plugin_module = sys.modules[obj.__module__] if '_' in plugin_module.__dict__: internationalizedCommands.update({hash(obj): obj}) try: obj.__doc__ = plugin_module._.__call__(obj.__doc__) # We use _.__call__() instead of _() because of a pygettext warning. except AttributeError: # attribute '__doc__' of 'type' objects is not writable pass return obj limnoria-2018.01.25/src/httpserver.py0000644000175000017500000003676713233426066016711 0ustar valval00000000000000### # Copyright (c) 2011, 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. ### """ An embedded and centralized HTTP server for Supybot's plugins. """ import os import cgi import socket from threading import Thread import supybot.log as log import supybot.conf as conf import supybot.world as world import supybot.utils.minisix as minisix from supybot.i18n import PluginInternationalization _ = PluginInternationalization() if minisix.PY2: from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler else: from http.server import HTTPServer, BaseHTTPRequestHandler configGroup = conf.supybot.servers.http class RequestNotHandled(Exception): pass DEFAULT_TEMPLATES = { 'index.html': """\ """ + _('Supybot Web server index') + """

Supybot web server index

""" + _('Here is a list of the plugins that have a Web interface:') +\ """

%(list)s """, 'generic/error.html': """\ %(title)s

Error

%(error)s

""", 'default.css': """\ body { background-color: #F0F0F0; } /************************************ * Classes that plugins should use. * ************************************/ /* Error pages */ body.error { text-align: center; } body.error p { background-color: #FFE0E0; border: 1px #FFA0A0 solid; } /* Pages that only contain a list. */ .purelisting { text-align: center; } .purelisting ul { margin: 0; padding: 0; } .purelisting ul li { margin: 0; padding: 0; list-style-type: none; } /* Pages that only contain a table. */ .puretable { text-align: center; } .puretable table { width: 100%; border-collapse: collapse; text-align: center; } .puretable table th { /*color: #039;*/ padding: 10px 8px; border-bottom: 2px solid #6678b1; } .puretable table td { padding: 9px 8px 0px 8px; border-bottom: 1px solid #ccc; } """, 'robots.txt': """""", } def set_default_templates(defaults): for filename, content in defaults.items(): path = conf.supybot.directories.data.web.dirize(filename) if os.path.isfile(path + '.example'): os.unlink(path + '.example') if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) with open(path + '.example', 'a') as fd: fd.write(content) set_default_templates(DEFAULT_TEMPLATES) def get_template(filename): path = conf.supybot.directories.data.web.dirize(filename) if os.path.isfile(path): with open(path, 'r') as fd: return fd.read() else: assert os.path.isfile(path + '.example'), path + '.example' with open(path + '.example', 'r') as fd: return fd.read() class RealSupyHTTPServer(HTTPServer): # TODO: make this configurable timeout = 0.5 running = False def __init__(self, address, protocol, callback): self.protocol = protocol if protocol == 4: self.address_family = socket.AF_INET elif protocol == 6: self.address_family = socket.AF_INET6 else: raise AssertionError(protocol) HTTPServer.__init__(self, address, callback) self.callbacks = {} def server_bind(self): if self.protocol == 6: v = conf.supybot.servers.http.singleStack() self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, v) HTTPServer.server_bind(self) def hook(self, subdir, callback): if subdir in self.callbacks: log.warning(('The HTTP subdirectory `%s` was already hooked but ' 'has been claimed by another plugin (or maybe you ' 'reloaded the plugin and it didn\'t properly unhook. ' 'Forced unhook.') % subdir) self.callbacks[subdir] = callback callback.doHook(self, subdir) def unhook(self, subdir): callback = self.callbacks.pop(subdir) # May raise a KeyError. We don't care. callback.doUnhook(self) return callback def __str__(self): return 'server at %s %i' % self.server_address[0:2] class TestSupyHTTPServer(RealSupyHTTPServer): def __init__(self, *args, **kwargs): self.callbacks = {} def serve_forever(self, *args, **kwargs): pass def shutdown(self, *args, **kwargs): pass if world.testing: SupyHTTPServer = TestSupyHTTPServer else: SupyHTTPServer = RealSupyHTTPServer class SupyHTTPRequestHandler(BaseHTTPRequestHandler): def do_X(self, callbackMethod, *args, **kwargs): if self.path == '/': callback = SupyIndex() elif self.path in ('/robots.txt',): callback = Static('text/plain; charset=utf-8') elif self.path in ('/default.css',): callback = Static('text/css') elif self.path == '/favicon.ico': callback = Favicon() else: subdir = self.path.split('/')[1] try: callback = self.server.callbacks[subdir] except KeyError: callback = Supy404() # Some shortcuts for name in ('send_response', 'send_header', 'end_headers', 'rfile', 'wfile', 'headers'): setattr(callback, name, getattr(self, name)) # We call doX, because this is more supybotic than do_X. path = self.path if not callback.fullpath: path = '/' + path.split('/', 2)[-1] getattr(callback, callbackMethod)(self, path, *args, **kwargs) def do_GET(self): self.do_X('doGet') def do_POST(self): if 'Content-Type' not in self.headers: self.headers['Content-Type'] = 'application/x-www-form-urlencoded' if self.headers['Content-Type'] == 'application/x-www-form-urlencoded': form = cgi.FieldStorage( fp=self.rfile, headers=self.headers, environ={'REQUEST_METHOD':'POST', 'CONTENT_TYPE':self.headers['Content-Type'], }) else: content_length = int(self.headers.get('Content-Length', '0')) form = self.rfile.read(content_length) self.do_X('doPost', form=form) def do_HEAD(self): self.do_X('doHead') def address_string(self): s = BaseHTTPRequestHandler.address_string(self) # Strip IPv4-mapped IPv6 addresses such as ::ffff:127.0.0.1 prefix = '::ffff:' if s.startswith(prefix): s = s[len(prefix):] return s def log_message(self, format, *args): log.info('HTTP request: %s - %s' % (self.address_string(), format % args)) class SupyHTTPServerCallback(log.Firewalled): """This is a base class that should be overriden by any plugin that want to have a Web interface.""" __firewalled__ = {'doGet': None, 'doPost': None, 'doHead': None, 'doPut': None, 'doDelete': None, } fullpath = False name = "Unnamed plugin" defaultResponse = _(""" This is a default response of the Supybot HTTP server. If you see this message, it probably means you are developing a plugin, and you have neither overriden this message or defined an handler for this query.""") if minisix.PY3: def write(self, b): if isinstance(b, str): b = b.encode() self.wfile.write(b) else: def write(self, s): self.wfile.write(s) def doGetOrHead(self, handler, path, write_content): response = self.defaultResponse.encode() handler.send_response(405) self.send_header('Content-Type', 'text/plain; charset=utf-8; charset=utf-8') self.send_header('Content-Length', len(response)) self.end_headers() if write_content: self.wfile.write(response) def doGet(self, handler, path): self.doGetOrHead(handler, path, write_content=True) def doHead(self, handler, path): self.doGetOrHead(handler, path, write_content=False) doPost = doGet def doHook(self, handler, subdir): """Method called when hooking this callback.""" pass def doUnhook(self, handler): """Method called when unhooking this callback.""" pass class Supy404(SupyHTTPServerCallback): """A 404 Not Found error.""" name = "Error 404" fullpath = True response = _(""" I am a pretty clever IRC bot, but I suck at serving Web pages, particulary if I don't know what to serve. What I'm saying is you just triggered a 404 Not Found, and I am not trained to help you in such a case.""") def doGetOrHead(self, handler, path, write_content): response = self.response if minisix.PY3: response = response.encode() handler.send_response(404) self.send_header('Content-Type', 'text/plain; charset=utf-8; charset=utf-8') self.send_header('Content-Length', len(self.response)) self.end_headers() if write_content: self.wfile.write(response) class SupyIndex(SupyHTTPServerCallback): """Displays the index of available plugins.""" name = "index" fullpath = True defaultResponse = _("Request not handled.") def doGetOrHead(self, handler, path, write_content): plugins = [x for x in handler.server.callbacks.items()] if plugins == []: plugins = _('No plugins available.') else: plugins = '
  • %s
' % '
  • '.join( ['%s' % (x,y.name) for x,y in plugins]) response = get_template('index.html') % {'list': plugins} if minisix.PY3: response = response.encode() handler.send_response(200) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Length', len(response)) self.end_headers() if write_content: self.wfile.write(response) class Static(SupyHTTPServerCallback): """Serves static files.""" fullpath = True name = 'static' defaultResponse = _('Request not handled') def __init__(self, mimetype='text/plain; charset=utf-8'): super(Static, self).__init__() self._mimetype = mimetype def doGetOrHead(self, handler, path, write_content): response = get_template(path) if minisix.PY3: response = response.encode() handler.send_response(200) self.send_header('Content-type', self._mimetype) self.send_header('Content-Length', len(response)) self.end_headers() if write_content: self.wfile.write(response) class Favicon(SupyHTTPServerCallback): """Services the favicon.ico file to browsers.""" name = 'favicon' defaultResponse = _('Request not handled') def doGetOrHead(self, handler, path, write_content): response = None file_path = conf.supybot.servers.http.favicon() if file_path: try: icon = open(file_path, 'rb') response = icon.read() except IOError: pass finally: icon.close() if response is not None: # I have no idea why, but this headers are already sent. # filename = file_path.rsplit(os.sep, 1)[1] # if '.' in filename: # ext = filename.rsplit('.', 1)[1] # else: # ext = 'ico' # self.send_header('Content-Length', len(response)) # self.send_header('Content-type', 'image/' + ext) # self.end_headers() if write_content: self.wfile.write(response) else: response = _('No favicon set.') if minisix.PY3: response = response.encode() handler.send_response(404) self.send_header('Content-type', 'text/plain; charset=utf-8') self.send_header('Content-Length', len(response)) self.end_headers() if write_content: self.wfile.write(response) http_servers = [] def startServer(): """Starts the HTTP server. Shouldn't be called from other modules. The callback should be an instance of a child of SupyHTTPServerCallback.""" global http_servers addresses4 = [(4, (x, configGroup.port())) for x in configGroup.hosts4() if x != ''] addresses6 = [(6, (x, configGroup.port())) for x in configGroup.hosts6() if x != ''] http_servers = [] for protocol, address in (addresses4 + addresses6): server = SupyHTTPServer(address, protocol, SupyHTTPRequestHandler) Thread(target=server.serve_forever, name='HTTP Server').start() http_servers.append(server) log.info('Starting HTTP server: %s' % str(server)) def stopServer(): """Stops the HTTP server. Should be run only from this module or from when the bot is dying (ie. from supybot.world)""" global http_servers for server in http_servers: log.info('Stopping HTTP server: %s' % str(server)) server.shutdown() server = None if configGroup.keepAlive(): startServer() def hook(subdir, callback): """Sets a callback for a given subdir.""" if not http_servers: startServer() assert isinstance(http_servers, list) for server in http_servers: server.hook(subdir, callback) def unhook(subdir): """Unsets the callback assigned to the given subdir, and return it.""" global http_servers assert isinstance(http_servers, list) for server in http_servers: server.unhook(subdir) if len(server.callbacks) <= 0 and not configGroup.keepAlive(): server.shutdown() http_servers.remove(server) limnoria-2018.01.25/src/gpg.py0000644000175000017500000000566013233426066015244 0ustar valval00000000000000### # Copyright (c) 2012, 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 os import supybot.log as log import supybot.conf as conf found_gnupg_lib = False found_gnupg_bin = False try: import gnupg except ImportError: # As we do not want Supybot to depend on GnuPG, we will use it only if # it is available. Otherwise, we just don't allow user auth through GPG. log.debug('Cannot import gnupg, disabling GPG support.') gnupg = None try: if gnupg: gnupg.GPG(gnupghome=None) found_gnupg_lib = found_gnupg_bin = True except TypeError: # This is the 'gnupg' library, not 'python-gnupg'. gnupg = None log.error('Cannot use GPG. gnupg (a Python package) is installed, ' 'but python-gnupg (an other Python package) should be ' 'installed instead.') except OSError: gnupg = None found_gnupg_lib = True log.error('Cannot use GPG. python-gnupg is installed but cannot ' 'find the gnupg executable.') available = (gnupg is not None) def loadKeyring(): if not available: return global keyring path = os.path.abspath(conf.supybot.directories.data.dirize('GPGkeyring')) if not os.path.isdir(path): log.info('Creating directory %s' % path) os.mkdir(path, 0o700) assert os.path.isdir(path) keyring = gnupg.GPG(gnupghome=path) loadKeyring() # Reload the keyring if path changed conf.supybot.directories.data.addCallback(loadKeyring) limnoria-2018.01.25/src/dynamicScope.py0000644000175000017500000000440213233426066017076 0ustar valval00000000000000### # 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 sys class DynamicScope(object): def _getLocals(self, name): f = sys._getframe().f_back.f_back # _getLocals <- __[gs]etattr__ <- ... while f: if name in f.f_locals: return f.f_locals f = f.f_back raise NameError(name) def __getattr__(self, name): try: return self._getLocals(name)[name] except (NameError, KeyError): return None def __setattr__(self, name, value): self._getLocals(name)[name] = value (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['dynamic'] = DynamicScope() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/dbi.py0000644000175000017500000003151613233426066015224 0ustar valval00000000000000### # 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. ### """ Module for some slight database-independence for simple databases. """ import os import csv import math from . import cdb, utils from .utils import minisix from .utils.iter import ilen class Error(Exception): """General error for this module.""" class NoRecordError(KeyError): pass class InvalidDBError(Exception): pass class MappingInterface(object): """This is a class to represent the underlying representation of a map from integer keys to strings.""" def __init__(self, filename, **kwargs): """Feel free to ignore the filename.""" raise NotImplementedError def get(id): """Gets the record matching id. Raises NoRecordError otherwise.""" raise NotImplementedError def set(id, s): """Sets the record matching id to s.""" raise NotImplementedError def add(self, s): """Adds a new record, returning a new id for it.""" raise NotImplementedError def remove(self, id): "Returns and removes the record with the given id from the database." raise NotImplementedError def __iter__(self): "Return an iterator over (id, s) pairs. Not required to be ordered." raise NotImplementedError def flush(self): """Flushes current state to disk.""" raise NotImplementedError def close(self): """Flushes current state to disk and invalidates the Mapping.""" raise NotImplementedError def vacuum(self): "Cleans up in the database, if possible. Not required to do anything." pass class DirMapping(MappingInterface): def __init__(self, filename, **kwargs): self.dirname = filename if not os.path.exists(self.dirname): os.mkdir(self.dirname) if not os.path.exists(os.path.join(self.dirname, 'max')): self._setMax(1) def _setMax(self, id): fd = open(os.path.join(self.dirname, 'max'), 'w') try: fd.write(str(id)) finally: fd.close() def _getMax(self): fd = open(os.path.join(self.dirname, 'max')) try: i = int(fd.read()) return i finally: fd.close() def _makeFilename(self, id): return os.path.join(self.dirname, str(id)) def get(self, id): try: fd = open(self._makeFilename(id)) return fd.read() except EnvironmentError as e: exn = NoRecordError(id) exn.realException = e raise exn finally: fd.close() def set(self, id, s): fd = open(self._makeFilename(id), 'w') fd.write(s) fd.close() def add(self, s): id = self._getMax() fd = open(self._makeFilename(id), 'w') try: fd.write(s) return id finally: fd.close() def remove(self, id): try: os.remove(self._makeFilename(id)) except EnvironmentError: raise NoRecordError(id) class FlatfileMapping(MappingInterface): def __init__(self, filename, maxSize=10**6): self.filename = filename try: fd = open(self.filename) strId = fd.readline().rstrip() self.maxSize = len(strId) try: self.currentId = int(strId) except ValueError: raise Error('Invalid file for FlatfileMapping: %s' % filename) except EnvironmentError as e: # File couldn't be opened. self.maxSize = int(math.log10(maxSize)) self.currentId = 0 self._incrementCurrentId() finally: if 'fd' in locals(): fd.close() def _canonicalId(self, id): if id is not None: return str(id).zfill(self.maxSize) else: return '-'*self.maxSize def _incrementCurrentId(self, fd=None): fdWasNone = fd is None if fdWasNone: fd = open(self.filename, 'a') fd.seek(0) self.currentId += 1 fd.write(self._canonicalId(self.currentId)) fd.write('\n') if fdWasNone: fd.close() def _splitLine(self, line): line = line.rstrip('\r\n') (id, s) = line.split(':', 1) return (id, s) def _joinLine(self, id, s): return '%s:%s\n' % (self._canonicalId(id), s) def add(self, s): line = self._joinLine(self.currentId, s) fd = open(self.filename, 'r+') try: fd.seek(0, 2) # End. fd.write(line) return self.currentId finally: self._incrementCurrentId(fd) fd.close() def get(self, id): strId = self._canonicalId(id) try: fd = open(self.filename) fd.readline() # First line, nextId. for line in fd: (lineId, s) = self._splitLine(line) if lineId == strId: return s raise NoRecordError(id) finally: fd.close() # XXX This assumes it's not been given out. We should make sure that our # maximum id remains accurate if this is some value we've never given # out -- i.e., self.maxid = max(self.maxid, id) or something. def set(self, id, s): strLine = self._joinLine(id, s) try: fd = open(self.filename, 'r+') self.remove(id, fd) fd.seek(0, 2) # End. fd.write(strLine) finally: fd.close() def remove(self, id, fd=None): fdWasNone = fd is None strId = self._canonicalId(id) try: if fdWasNone: fd = open(self.filename, 'r+') fd.seek(0) fd.readline() # First line, nextId pos = fd.tell() line = fd.readline() while line: (lineId, _) = self._splitLine(line) if lineId == strId: fd.seek(pos) fd.write(self._canonicalId(None)) fd.seek(pos) fd.readline() # Same line we just rewrote the id for. pos = fd.tell() line = fd.readline() # We should be at the end. finally: if fdWasNone: fd.close() def __iter__(self): fd = open(self.filename) fd.readline() # First line, nextId. for line in fd: (id, s) = self._splitLine(line) if not id.startswith('-'): yield (int(id), s) fd.close() def vacuum(self): infd = open(self.filename) outfd = utils.file.AtomicFile(self.filename,makeBackupIfSmaller=False) outfd.write(infd.readline()) # First line, nextId. for line in infd: if not line.startswith('-'): outfd.write(line) infd.close() outfd.close() def flush(self): pass # No-op, we maintain no open files. def close(self): self.vacuum() # Should we do this? It should be fine. class CdbMapping(MappingInterface): def __init__(self, filename, **kwargs): self.filename = filename self._openCdb() # So it can be overridden later. if 'nextId' not in self.db: self.db['nextId'] = '1' def _openCdb(self, *args, **kwargs): self.db = cdb.open_db(self.filename, 'c', **kwargs) def _getNextId(self): i = int(self.db['nextId']) self.db['nextId'] = str(i+1) return i def get(self, id): try: return self.db[str(id)] except KeyError: raise NoRecordError(id) # XXX Same as above. def set(self, id, s): self.db[str(id)] = s def add(self, s): id = self._getNextId() self.set(id, s) return id def remove(self, id): del self.db[str(id)] def __iter__(self): for (id, s) in self.db.items(): if id != 'nextId': yield (int(id), s) def flush(self): self.db.flush() def close(self): self.db.close() class DB(object): Mapping = 'flat' # This is a good, sane default. Record = None def __init__(self, filename, Mapping=None, Record=None): if Record is not None: self.Record = Record if Mapping is not None: self.Mapping = Mapping if isinstance(self.Mapping, minisix.string_types): self.Mapping = Mappings[self.Mapping] self.map = self.Mapping(filename) def _newRecord(self, id, s): record = self.Record(id=id) record.deserialize(s) return record def get(self, id): s = self.map.get(id) return self._newRecord(id, s) def set(self, id, record): s = record.serialize() self.map.set(id, s) def add(self, record): s = record.serialize() id = self.map.add(s) record.id = id return id def remove(self, id): self.map.remove(id) def __iter__(self): for (id, s) in self.map: # We don't need to yield the id because it's in the record. yield self._newRecord(id, s) def select(self, p): for record in self: if p(record): yield record def random(self): try: return self._newRecord(*utils.iter.choice(self.map)) except IndexError: return None def size(self): return ilen(self.map) def flush(self): self.map.flush() def vacuum(self): self.map.vacuum() def close(self): self.map.close() Mappings = { 'cdb': CdbMapping, 'flat': FlatfileMapping, } class Record(object): def __init__(self, id=None, **kwargs): if id is not None: assert isinstance(id, int), 'id must be an integer.' self.id = id self.fields = [] self.defaults = {} self.converters = {} for name in self.__fields__: if isinstance(name, tuple): (name, spec) = name else: spec = utils.safeEval assert name != 'id' self.fields.append(name) if isinstance(spec, tuple): (converter, default) = spec else: converter = spec default = None self.defaults[name] = default self.converters[name] = converter seen = set() for (name, value) in kwargs.items(): assert name in self.fields, 'name must be a record value.' seen.add(name) setattr(self, name, value) for name in self.fields: if name not in seen: default = self.defaults[name] if callable(default): default = default() setattr(self, name, default) def serialize(self): return csv.join([repr(getattr(self, name)) for name in self.fields]) def deserialize(self, s): unseenRecords = set(self.fields) for (name, strValue) in zip(self.fields, csv.split(s)): setattr(self, name, self.converters[name](strValue)) unseenRecords.remove(name) for name in unseenRecords: setattr(self, name, self.defaults[name]) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/conf.py0000644000175000017500000016432613233426066015421 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008-2009,2011, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import sys import time import socket from . import ircutils, registry, utils from .utils import minisix from .utils.net import isSocketAddress from .version import version from .i18n import PluginInternationalization _ = PluginInternationalization() if minisix.PY2: from urllib2 import build_opener, install_opener, ProxyHandler else: from urllib.request import build_opener, install_opener, ProxyHandler ### # *** The following variables are affected by command-line options. They are # not registry variables for a specific reason. Do *not* change these to # registry variables without first consulting people smarter than yourself. ### ### # daemonized: This determines whether or not the bot has been daemonized # (i.e., set to run in the background). Obviously, this defaults # to False. A command-line option for obvious reasons. ### daemonized = False ### # allowDefaultOwner: True if supybot.capabilities is allowed not to include # '-owner' -- that is, if all users should be automatically # recognized as owners. That would suck, hence we require a # command-line option to allow this stupidity. ### allowDefaultOwner = False ### # Here we replace values in other modules as appropriate. ### utils.web.defaultHeaders['User-agent'] = \ 'Mozilla/5.0 (Compatible; Supybot %s)' % version ### # The standard registry. ### supybot = registry.Group() supybot.setName('supybot') def registerGroup(Group, name, group=None, **kwargs): if kwargs: group = registry.Group(**kwargs) return Group.register(name, group) def registerGlobalValue(group, name, value): value.channelValue = False return group.register(name, value) def registerChannelValue(group, name, value): value._supplyDefault = True value.channelValue = True g = group.register(name, value) gname = g._name.lower() for name in registry._cache.keys(): if name.lower().startswith(gname) and len(gname) < len(name): name = name[len(gname)+1:] # +1 for . parts = registry.split(name) if len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]): # This gets the channel values so they always persist. g.get(parts[0])() def registerPlugin(name, currentValue=None, public=True): group = registerGlobalValue(supybot.plugins, name, registry.Boolean(False, _("""Determines whether this plugin is loaded by default."""), showDefault=False)) supybot.plugins().add(name) registerGlobalValue(group, 'public', registry.Boolean(public, _("""Determines whether this plugin is publicly visible."""))) if currentValue is not None: supybot.plugins.get(name).setValue(currentValue) registerGroup(users.plugins, name) return group def get(group, channel=None): if group.channelValue and \ channel is not None and ircutils.isChannel(channel): return group.get(channel)() else: return group() ### # The user info registry. ### users = registry.Group() users.setName('users') registerGroup(users, 'plugins', orderAlphabetically=True) def registerUserValue(group, name, value): assert group._name.startswith('users') value._supplyDefault = True group.register(name, value) class ValidNick(registry.String): """Value must be a valid IRC nick.""" def setValue(self, v): if not ircutils.isNick(v): self.error() else: registry.String.setValue(self, v) class ValidNickOrEmpty(ValidNick): """Value must be a valid IRC nick or empty.""" def setValue(self, v): if v != '' and not ircutils.isNick(v): self.error() else: registry.String.setValue(self, v) class ValidNicks(registry.SpaceSeparatedListOf): Value = ValidNick class ValidNickAllowingPercentS(ValidNick): """Value must be a valid IRC nick, with the possible exception of a %s in it.""" def setValue(self, v): # If this works, it's a valid nick, aside from the %s. try: ValidNick.setValue(self, v.replace('%s', '')) # It's valid aside from the %s, we'll let it through. registry.String.setValue(self, v) except registry.InvalidRegistryValue: self.error() class ValidNicksAllowingPercentS(ValidNicks): Value = ValidNickAllowingPercentS class ValidChannel(registry.String): """Value must be a valid IRC channel name.""" def setValue(self, v): self.channel = v if ',' in v: # To prevent stupid users from: a) trying to add a channel key # with a comma in it, b) trying to add channels separated by # commas instead of spaces try: (channel, _) = v.split(',') except ValueError: self.error() else: channel = v if not ircutils.isChannel(channel): self.error() else: registry.String.setValue(self, v) def error(self): try: super(ValidChannel, self).error() except registry.InvalidRegistryValue as e: e.channel = self.channel raise e class ValidHostmask(registry.String): """Value must be a valid user hostmask.""" def setValue(self, v): if not ircutils.isUserHostmask(v): self.error() super(ValidHostmask, self).setValue(v) registerGlobalValue(supybot, 'nick', ValidNick('supybot', _("""Determines the bot's default nick."""))) registerGlobalValue(supybot.nick, 'alternates', ValidNicksAllowingPercentS(['%s`', '%s_'], _("""Determines what alternative nicks will be used if the primary nick (supybot.nick) isn't available. A %s in this nick is replaced by the value of supybot.nick when used. If no alternates are given, or if all are used, the supybot.nick will be perturbed appropriately until an unused nick is found."""))) registerGlobalValue(supybot, 'ident', ValidNick('limnoria', _("""Determines the bot's ident string, if the server doesn't provide one by default."""))) # Although empty version strings are theoretically allowed by the RFC, # popular IRCds do not. # So, we keep replacing the empty string by the current version for # bots which are migrated from Supybot or an old version of Limnoria # (whose default value of supybot.user is the empty string). class VersionIfEmpty(registry.String): def __call__(self): ret = registry.String.__call__(self) if not ret: ret = 'Limnoria $version' return ret registerGlobalValue(supybot, 'user', VersionIfEmpty('Limnoria $version', _("""Determines the real name which the bot sends to the server. A standard real name using the current version of the bot will be generated if this is left empty."""))) class Networks(registry.SpaceSeparatedSetOfStrings): List = ircutils.IrcSet registerGlobalValue(supybot, 'networks', Networks([], _("""Determines what networks the bot will connect to."""), orderAlphabetically=True)) class Servers(registry.SpaceSeparatedListOfStrings): def normalize(self, s): if ':' not in s: s += ':6667' return s def convert(self, s): s = self.normalize(s) (server, port) = s.rsplit(':', 1) port = int(port) return (server, port) def __call__(self): L = registry.SpaceSeparatedListOfStrings.__call__(self) return list(map(self.convert, L)) def __str__(self): return ' '.join(registry.SpaceSeparatedListOfStrings.__call__(self)) def append(self, s): L = registry.SpaceSeparatedListOfStrings.__call__(self) L.append(s) class SocksProxy(registry.String): """Value must be a valid hostname:port string.""" def setValue(self, v): # TODO: improve checks if ':' not in v: self.error() try: int(v.rsplit(':', 1)[1]) except ValueError: self.error() super(SocksProxy, self).setValue(v) class SpaceSeparatedSetOfChannels(registry.SpaceSeparatedListOf): sorted = True List = ircutils.IrcSet Value = ValidChannel def join(self, channel): from . import ircmsgs # Don't put this globally! It's recursive. key = self.key.get(channel)() if key: return ircmsgs.join(channel, key) else: return ircmsgs.join(channel) def joins(self): from . import ircmsgs # Don't put this globally! It's recursive. channels = [] channels_with_key = [] keys = [] old = None msgs = [] msg = None for channel in self(): key = self.key.get(channel)() if key: keys.append(key) channels_with_key.append(channel) else: channels.append(channel) msg = ircmsgs.joins(channels_with_key + channels, keys) if len(str(msg)) > 512: msgs.append(old) keys = [] channels_with_key = [] channels = [] old = msg if msg: msgs.append(msg) return msgs else: # Let's be explicit about it return None class ValidSaslMechanism(registry.OnlySomeStrings): validStrings = ('ecdsa-nist256p-challenge', 'external', 'plain', 'scram-sha-256') class SpaceSeparatedListOfSaslMechanisms(registry.SpaceSeparatedListOf): Value = ValidSaslMechanism def registerNetwork(name, password='', ssl=True, sasl_username='', sasl_password=''): network = registerGroup(supybot.networks, name) registerGlobalValue(network, 'password', registry.String(password, _("""Determines what password will be used on %s. Yes, we know that technically passwords are server-specific and not network-specific, but this is the best we can do right now.""") % name, private=True)) registerGlobalValue(network, 'servers', Servers([], _("""Space-separated list of servers the bot will connect to for %s. Each will be tried in order, wrapping back to the first when the cycle is completed.""") % name)) registerGlobalValue(network, 'channels', SpaceSeparatedSetOfChannels([], _("""Space-separated list of channels the bot will join only on %s.""") % name, private=True)) registerGlobalValue(network, 'ssl', registry.Boolean(ssl, _("""Determines whether the bot will attempt to connect with SSL sockets to %s.""") % name)) registerGlobalValue(network.ssl, 'serverFingerprints', registry.SpaceSeparatedSetOfStrings([], format(_("""Space-separated list of fingerprints of trusted certificates for this network. Supported hash algorithms are: %L. If non-empty, Certification Authority signatures will not be used to verify certificates."""), utils.net.FINGERPRINT_ALGORITHMS))) registerGlobalValue(network.ssl, 'authorityCertificate', registry.String('', _("""A certificate that is trusted to verify certificates of this network (aka. Certificate Authority)."""))) registerGlobalValue(network, 'requireStarttls', registry.Boolean(False, _("""Deprecated config value, keep it to False."""))) registerGlobalValue(network, 'certfile', registry.String('', _("""Determines what certificate file (if any) the bot will use to connect with SSL sockets to %s.""") % name)) registerChannelValue(network.channels, 'key', registry.String('', _("""Determines what key (if any) will be used to join the channel."""), private=True)) registerGlobalValue(network, 'nick', ValidNickOrEmpty('', _("""Determines what nick the bot will use on this network. If empty, defaults to supybot.nick."""))) registerGlobalValue(network, 'ident', ValidNickOrEmpty('', _("""Determines the bot's ident string, if the server doesn't provide one by default. If empty, defaults to supybot.ident."""))) registerGlobalValue(network, 'user', registry.String('', _("""Determines the real name which the bot sends to the server. If empty, defaults to supybot.user"""))) registerGlobalValue(network, 'umodes', registry.String('', _("""Determines what user modes the bot will request from the server when it first connects. If empty, defaults to supybot.protocols.irc.umodes"""))) sasl = registerGroup(network, 'sasl') registerGlobalValue(sasl, 'username', registry.String(sasl_username, _("""Determines what SASL username will be used on %s. This should be the bot's account name. Due to the way SASL works, you can't use any grouped nick.""") % name, private=False)) registerGlobalValue(sasl, 'password', registry.String(sasl_password, _("""Determines what SASL password will be used on %s.""") \ % name, private=True)) registerGlobalValue(sasl, 'ecdsa_key', registry.String('', _("""Determines what SASL ECDSA key (if any) will be used on %s. The public key must be registered with NickServ for SASL ECDSA-NIST256P-CHALLENGE to work.""") % name, private=False)) registerGlobalValue(sasl, 'mechanisms', SpaceSeparatedListOfSaslMechanisms( ['ecdsa-nist256p-challenge', 'external', 'plain'], _("""Determines what SASL mechanisms will be tried and in which order."""))) registerGlobalValue(network, 'socksproxy', registry.String('', _("""If not empty, determines the hostname of the socks proxy that will be used to connect to this network."""))) return network # Let's fill our networks. for (name, s) in registry._cache.items(): if name.startswith('supybot.networks.'): parts = name.split('.') name = parts[2] if name != 'default': registerNetwork(name) ### # Reply/error tweaking. ### registerGroup(supybot, 'reply') registerGroup(supybot.reply, 'format') registerChannelValue(supybot.reply.format, 'url', registry.String('<%s>', _("""Determines how urls should be formatted."""))) def url(s): if s: return supybot.reply.format.url() % s else: return '' utils.str.url = url registerChannelValue(supybot.reply.format, 'time', registry.String('%Y-%m-%dT%H:%M:%S%z', _("""Determines how timestamps printed for human reading should be formatted. Refer to the Python documentation for the time module to see valid formatting characters for time formats."""))) def timestamp(t): if t is None: t = time.time() if isinstance(t, float) or isinstance(t, int): t = time.localtime(t) format = get(supybot.reply.format.time, dynamic.channel) return time.strftime(format, t) utils.str.timestamp = timestamp registerGroup(supybot.reply.format.time, 'elapsed') registerChannelValue(supybot.reply.format.time.elapsed, 'short', registry.Boolean(False, _("""Determines whether elapsed times will be given as "1 day, 2 hours, 3 minutes, and 15 seconds" or as "1d 2h 3m 15s"."""))) originalTimeElapsed = utils.timeElapsed def timeElapsed(*args, **kwargs): kwargs['short'] = supybot.reply.format.time.elapsed.short() return originalTimeElapsed(*args, **kwargs) utils.timeElapsed = timeElapsed registerGlobalValue(supybot.reply, 'maximumLength', registry.Integer(512*256, _("""Determines the absolute maximum length of the bot's reply -- no reply will be passed through the bot with a length greater than this."""))) registerChannelValue(supybot.reply, 'mores', registry.Boolean(True, _("""Determines whether the bot will break up long messages into chunks and allow users to use the 'more' command to get the remaining chunks."""))) registerChannelValue(supybot.reply.mores, 'maximum', registry.PositiveInteger(50, _("""Determines what the maximum number of chunks (for use with the 'more' command) will be."""))) registerChannelValue(supybot.reply.mores, 'length', registry.NonNegativeInteger(0, _("""Determines how long individual chunks will be. If set to 0, uses our super-tweaked, get-the-most-out-of-an-individual-message default."""))) registerChannelValue(supybot.reply.mores, 'instant', registry.PositiveInteger(1, _("""Determines how many mores will be sent instantly (i.e., without the use of the more command, immediately when they are formed). Defaults to 1, which means that a more command will be required for all but the first chunk."""))) registerChannelValue(supybot.reply, 'oneToOne', registry.Boolean(True, _("""Determines whether the bot will send multi-message replies in a single message. This defaults to True in order to prevent the bot from flooding. If this is set to False the bot will send multi-message replies on multiple lines."""))) registerChannelValue(supybot.reply, 'whenNotCommand', registry.Boolean(True, _("""Determines whether the bot will reply with an error message when it is addressed but not given a valid command. If this value is False, the bot will remain silent, as long as no other plugins override the normal behavior."""))) registerGroup(supybot.reply, 'error') registerGlobalValue(supybot.reply.error, 'detailed', registry.Boolean(False, _("""Determines whether error messages that result from bugs in the bot will show a detailed error message (the uncaught exception) or a generic error message."""))) registerChannelValue(supybot.reply.error, 'inPrivate', registry.Boolean(False, _("""Determines whether the bot will send error messages to users in private. You might want to do this in order to keep channel traffic to minimum. This can be used in combination with supybot.reply.error.withNotice."""))) registerChannelValue(supybot.reply.error, 'withNotice', registry.Boolean(False, _("""Determines whether the bot will send error messages to users via NOTICE instead of PRIVMSG. You might want to do this so users can ignore NOTICEs from the bot and not have to see error messages; or you might want to use it in combination with supybot.reply.errorInPrivate so private errors don't open a query window in most IRC clients."""))) registerChannelValue(supybot.reply.error, 'noCapability', registry.Boolean(False, _("""Determines whether the bot will *not* provide details in the error message to users who attempt to call a command for which they do not have the necessary capability. You may wish to make this True if you don't want users to understand the underlying security system preventing them from running certain commands."""))) registerChannelValue(supybot.reply, 'inPrivate', registry.Boolean(False, _("""Determines whether the bot will reply privately when replying in a channel, rather than replying to the whole channel."""))) registerChannelValue(supybot.reply, 'withNotice', registry.Boolean(False, _("""Determines whether the bot will reply with a notice when replying in a channel, rather than replying with a privmsg as normal."""))) # XXX: User value. registerGlobalValue(supybot.reply, 'withNoticeWhenPrivate', registry.Boolean(True, _("""Determines whether the bot will reply with a notice when it is sending a private message, in order not to open a /query window in clients."""))) registerChannelValue(supybot.reply, 'withNickPrefix', registry.Boolean(True, _("""Determines whether the bot will always prefix the user's nick to its reply to that user's command."""))) registerChannelValue(supybot.reply, 'whenNotAddressed', registry.Boolean(False, _("""Determines whether the bot should attempt to reply to all messages even if they don't address it (either via its nick or a prefix character). If you set this to True, you almost certainly want to set supybot.reply.whenNotCommand to False."""))) registerChannelValue(supybot.reply, 'requireChannelCommandsToBeSentInChannel', registry.Boolean(False, _("""Determines whether the bot will allow you to send channel-related commands outside of that channel. Sometimes people find it confusing if a channel-related command (like Filter.outfilter) changes the behavior of the channel but was sent outside the channel itself."""))) registerGlobalValue(supybot, 'followIdentificationThroughNickChanges', registry.Boolean(False, _("""Determines whether the bot will unidentify someone when that person changes their nick. Setting this to True will cause the bot to track such changes. It defaults to False for a little greater security."""))) registerChannelValue(supybot, 'alwaysJoinOnInvite', registry.Boolean(False, _("""Determines whether the bot will always join a channel when it's invited. If this value is False, the bot will only join a channel if the user inviting it has the 'admin' capability (or if it's explicitly told to join the channel using the Admin.join command)."""))) registerChannelValue(supybot.reply, 'showSimpleSyntax', registry.Boolean(False, _("""Supybot normally replies with the full help whenever a user misuses a command. If this value is set to True, the bot will only reply with the syntax of the command (the first line of the help) rather than the full help."""))) class ValidPrefixChars(registry.String): """Value must contain only ~!@#$%^&*()_-+=[{}]\\|'\";:,<.>/?""" def setValue(self, v): if any([x not in '`~!@#$%^&*()_-+=[{}]\\|\'";:,<.>/?' for x in v]): self.error() registry.String.setValue(self, v) registerGroup(supybot.reply, 'whenAddressedBy') registerChannelValue(supybot.reply.whenAddressedBy, 'chars', ValidPrefixChars('', _("""Determines what prefix characters the bot will reply to. A prefix character is a single character that the bot will use to determine what messages are addressed to it; when there are no prefix characters set, it just uses its nick. Each character in this string is interpreted individually; you can have multiple prefix chars simultaneously, and if any one of them is used as a prefix the bot will assume it is being addressed."""))) registerChannelValue(supybot.reply.whenAddressedBy, 'strings', registry.SpaceSeparatedSetOfStrings([], _("""Determines what strings the bot will reply to when they are at the beginning of the message. Whereas prefix.chars can only be one character (although there can be many of them), this variable is a space-separated list of strings, so you can set something like '@@ ??' and the bot will reply when a message is prefixed by either @@ or ??."""))) registerChannelValue(supybot.reply.whenAddressedBy, 'nick', registry.Boolean(True, _("""Determines whether the bot will reply when people address it by its nick, rather than with a prefix character."""))) registerChannelValue(supybot.reply.whenAddressedBy.nick, 'atEnd', registry.Boolean(False, _("""Determines whether the bot will reply when people address it by its nick at the end of the message, rather than at the beginning."""))) registerChannelValue(supybot.reply.whenAddressedBy, 'nicks', registry.SpaceSeparatedSetOfStrings([], _("""Determines what extra nicks the bot will always respond to when addressed by, even if its current nick is something else."""))) ### # Replies ### registerGroup(supybot, 'replies') registerChannelValue(supybot.replies, 'success', registry.NormalizedString(_("""The operation succeeded."""), _("""Determines what message the bot replies with when a command succeeded. If this configuration variable is empty, no success message will be sent."""))) registerChannelValue(supybot.replies, 'error', registry.NormalizedString(_("""An error has occurred and has been logged. Please contact this bot's administrator for more information."""), _(""" Determines what error message the bot gives when it wants to be ambiguous."""))) registerChannelValue(supybot.replies, 'errorOwner', registry.NormalizedString(_("""An error has occurred and has been logged. Check the logs for more information."""), _("""Determines what error message the bot gives to the owner when it wants to be ambiguous."""))) registerChannelValue(supybot.replies, 'incorrectAuthentication', registry.NormalizedString(_("""Your hostmask doesn't match or your password is wrong."""), _("""Determines what message the bot replies with when someone tries to use a command that requires being identified or having a password and neither credential is correct."""))) # XXX: This should eventually check that there's one and only one %s here. registerChannelValue(supybot.replies, 'noUser', registry.NormalizedString(_("""I can't find %s in my user database. If you didn't give a user name, then I might not know what your user is, and you'll need to identify before this command might work."""), _("""Determines what error message the bot replies with when someone tries to accessing some information on a user the bot doesn't know about."""))) registerChannelValue(supybot.replies, 'notRegistered', registry.NormalizedString(_("""You must be registered to use this command. If you are already registered, you must either identify (using the identify command) or add a hostmask matching your current hostmask (using the "hostmask add" command)."""), _("""Determines what error message the bot replies with when someone tries to do something that requires them to be registered but they're not currently recognized."""))) registerChannelValue(supybot.replies, 'noCapability', registry.NormalizedString(_("""You don't have the %s capability. If you think that you should have this capability, be sure that you are identified before trying again. The 'whoami' command can tell you if you're identified."""), _("""Determines what error message is given when the bot is telling someone they aren't cool enough to use the command they tried to use."""))) registerChannelValue(supybot.replies, 'genericNoCapability', registry.NormalizedString(_("""You're missing some capability you need. This could be because you actually possess the anti-capability for the capability that's required of you, or because the channel provides that anti-capability by default, or because the global capabilities include that anti-capability. Or, it could be because the channel or supybot.capabilities.default is set to False, meaning that no commands are allowed unless explicitly in your capabilities. Either way, you can't do what you want to do."""), _("""Determines what generic error message is given when the bot is telling someone that they aren't cool enough to use the command they tried to use, and the author of the code calling errorNoCapability didn't provide an explicit capability for whatever reason."""))) registerChannelValue(supybot.replies, 'requiresPrivacy', registry.NormalizedString(_("""That operation cannot be done in a channel."""), _("""Determines what error messages the bot sends to people who try to do things in a channel that really should be done in private."""))) registerChannelValue(supybot.replies, 'possibleBug', registry.NormalizedString(_("""This may be a bug. If you think it is, please file a bug report at ."""), _("""Determines what message the bot sends when it thinks you've encountered a bug that the developers don't know about."""))) ### # End supybot.replies. ### registerGlobalValue(supybot, 'snarfThrottle', registry.Float(10.0, _("""A floating point number of seconds to throttle snarfed URLs, in order to prevent loops between two bots snarfing the same URLs and having the snarfed URL in the output of the snarf message."""))) registerGlobalValue(supybot, 'upkeepInterval', registry.PositiveInteger(3600, _("""Determines the number of seconds between running the upkeep function that flushes (commits) open databases, collects garbage, and records some useful statistics at the debugging level."""))) registerGlobalValue(supybot, 'flush', registry.Boolean(True, _("""Determines whether the bot will periodically flush data and configuration files to disk. Generally, the only time you'll want to set this to False is when you want to modify those configuration files by hand and don't want the bot to flush its current version over your modifications. Do note that if you change this to False inside the bot, your changes won't be flushed. To make this change permanent, you must edit the registry yourself."""))) ### # supybot.commands. For stuff relating to commands. ### registerGroup(supybot, 'commands') class ValidQuotes(registry.Value): """Value must consist solely of \", ', and ` characters.""" def setValue(self, v): if [c for c in v if c not in '"`\'']: self.error() super(ValidQuotes, self).setValue(v) def __str__(self): return str(self.value) registerChannelValue(supybot.commands, 'quotes', ValidQuotes('"', _("""Determines what characters are valid for quoting arguments to commands in order to prevent them from being tokenized. """))) # This is a GlobalValue because bot owners should be able to say, "There will # be no nesting at all on this bot." Individual channels can just set their # brackets to the empty string. registerGlobalValue(supybot.commands, 'nested', registry.Boolean(True, _("""Determines whether the bot will allow nested commands, which rule. You definitely should keep this on."""))) registerGlobalValue(supybot.commands.nested, 'maximum', registry.PositiveInteger(10, _("""Determines what the maximum number of nested commands will be; users will receive an error if they attempt commands more nested than this."""))) class ValidBrackets(registry.OnlySomeStrings): validStrings = ('', '[]', '<>', '{}', '()') registerChannelValue(supybot.commands.nested, 'brackets', ValidBrackets('[]', _("""Supybot allows you to specify what brackets are used for your nested commands. Valid sets of brackets include [], <>, {}, and (). [] has strong historical motivation, but <> or () might be slightly superior because they cannot occur in a nick. If this string is empty, nested commands will not be allowed in this channel."""))) registerChannelValue(supybot.commands.nested, 'pipeSyntax', registry.Boolean(False, _("""Supybot allows nested commands. Enabling this option will allow nested commands with a syntax similar to UNIX pipes, for example: 'bot: foo | bar'."""))) registerGroup(supybot.commands, 'defaultPlugins', orderAlphabetically=True, help=_("""Determines what commands have default plugins set, and which plugins are set to be the default for each of those commands.""")) registerGlobalValue(supybot.commands.defaultPlugins, 'importantPlugins', registry.SpaceSeparatedSetOfStrings( ['Admin', 'Channel', 'Config', 'Misc', 'Owner', 'User'], _("""Determines what plugins automatically get precedence over all other plugins when selecting a default plugin for a command. By default, this includes the standard loaded plugins. You probably shouldn't change this if you don't know what you're doing; if you do know what you're doing, then also know that this set is case-sensitive."""))) # For this config variable to make sense, it must no be writable via IRC. # Make sure it is always blacklisted from the Config plugin. registerGlobalValue(supybot.commands, 'allowShell', registry.Boolean(True, _("""Allows this bot's owner user to use commands that grants them shell access. This config variable exists in case you want to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops) from gaining shell access to the bot's server by impersonating the owner. Setting this to False also disables plugins and commands that can be used to indirectly gain shell access."""))) # supybot.commands.disabled moved to callbacks for canonicalName. ### # supybot.abuse. For stuff relating to abuse of the bot. ### registerGroup(supybot, 'abuse') registerGroup(supybot.abuse, 'flood') registerGlobalValue(supybot.abuse.flood, 'interval', registry.PositiveInteger(60, _("""Determines the interval used for the history storage."""))) registerGlobalValue(supybot.abuse.flood, 'command', registry.Boolean(True, _("""Determines whether the bot will defend itself against command-flooding."""))) registerGlobalValue(supybot.abuse.flood.command, 'maximum', registry.PositiveInteger(12, _("""Determines how many commands users are allowed per minute. If a user sends more than this many commands in any 60 second period, they will be ignored for supybot.abuse.flood.command.punishment seconds."""))) registerGlobalValue(supybot.abuse.flood.command, 'punishment', registry.PositiveInteger(300, _("""Determines how many seconds the bot will ignore users who flood it with commands."""))) registerGlobalValue(supybot.abuse.flood.command, 'notify', registry.Boolean(True, _("""Determines whether the bot will notify people that they're being ignored for command flooding."""))) registerGlobalValue(supybot.abuse.flood.command, 'invalid', registry.Boolean(True, _("""Determines whether the bot will defend itself against invalid command-flooding."""))) registerGlobalValue(supybot.abuse.flood.command.invalid, 'maximum', registry.PositiveInteger(5, _("""Determines how many invalid commands users are allowed per minute. If a user sends more than this many invalid commands in any 60 second period, they will be ignored for supybot.abuse.flood.command.invalid.punishment seconds. Typically, this value is lower than supybot.abuse.flood.command.maximum, since it's far less likely (and far more annoying) for users to flood with invalid commands than for them to flood with valid commands."""))) registerGlobalValue(supybot.abuse.flood.command.invalid, 'punishment', registry.PositiveInteger(600, _("""Determines how many seconds the bot will ignore users who flood it with invalid commands. Typically, this value is higher than supybot.abuse.flood.command.punishment, since it's far less likely (and far more annoying) for users to flood with invalid commands than for them to flood with valid commands."""))) registerGlobalValue(supybot.abuse.flood.command.invalid, 'notify', registry.Boolean(True, _("""Determines whether the bot will notify people that they're being ignored for invalid command flooding."""))) ### # supybot.drivers. For stuff relating to Supybot's drivers (duh!) ### registerGroup(supybot, 'drivers') registerGlobalValue(supybot.drivers, 'poll', registry.PositiveFloat(1.0, _("""Determines the default length of time a driver should block waiting for input."""))) class ValidDriverModule(registry.OnlySomeStrings): validStrings = ('default', 'Socket', 'Twisted') registerGlobalValue(supybot.drivers, 'module', ValidDriverModule('default', _("""Determines what driver module the bot will use. The default is Socket which is simple and stable and supports SSL. Twisted doesn't work if the IRC server which you are connecting to has IPv6 (most of them do)."""))) registerGlobalValue(supybot.drivers, 'maxReconnectWait', registry.PositiveFloat(300.0, _("""Determines the maximum time the bot will wait before attempting to reconnect to an IRC server. The bot may, of course, reconnect earlier if possible."""))) ### # supybot.directories, for stuff relating to directories. ### # XXX This shouldn't make directories willy-nilly. As it is now, if it's # configured, it'll still make the default directories, I think. class Directory(registry.String): def __call__(self): # ??? Should we perhaps always return an absolute path here? v = super(Directory, self).__call__() if not os.path.exists(v): os.mkdir(v) return v def dirize(self, filename): myself = self() if os.path.isabs(filename): filename = os.path.abspath(filename) selfAbs = os.path.abspath(myself) commonPrefix = os.path.commonprefix([selfAbs, filename]) filename = filename[len(commonPrefix):] elif not os.path.isabs(myself): if filename.startswith(myself): filename = filename[len(myself):] filename = filename.lstrip(os.path.sep) # Stupid os.path.join! return os.path.join(myself, filename) class DataFilename(registry.String): def __call__(self): v = super(DataFilename, self).__call__() dataDir = supybot.directories.data() if not v.startswith(dataDir): v = os.path.basename(v) v = os.path.join(dataDir, v) self.setValue(v) return v class DataFilenameDirectory(DataFilename, Directory): def __call__(self): v = DataFilename.__call__(self) v = Directory.__call__(self) return v registerGroup(supybot, 'directories') registerGlobalValue(supybot.directories, 'conf', Directory('conf', _("""Determines what directory configuration data is put into."""))) registerGlobalValue(supybot.directories, 'data', Directory('data', _("""Determines what directory data is put into."""))) registerGlobalValue(supybot.directories, 'backup', Directory('backup', _("""Determines what directory backup data is put into. Set it to /dev/null to disable backup (it is a special value, so it also works on Windows and systems without /dev/null)."""))) registerGlobalValue(supybot.directories, 'log', Directory('logs', """Determines what directory the bot will store its logfiles in.""")) registerGlobalValue(supybot.directories.data, 'tmp', DataFilenameDirectory('tmp', _("""Determines what directory temporary files are put into."""))) registerGlobalValue(supybot.directories.data, 'web', DataFilenameDirectory('web', _("""Determines what directory files of the web server (templates, custom images, ...) are put into."""))) def _update_tmp(): utils.file.AtomicFile.default.tmpDir = supybot.directories.data.tmp supybot.directories.data.tmp.addCallback(_update_tmp) _update_tmp() def _update_backup(): utils.file.AtomicFile.default.backupDir = supybot.directories.backup supybot.directories.backup.addCallback(_update_backup) _update_backup() registerGlobalValue(supybot.directories, 'plugins', registry.CommaSeparatedListOfStrings([], _("""Determines what directories the bot will look for plugins in. Accepts a comma-separated list of strings. This means that to add another directory, you can nest the former value and add a new one. E.g. you can say: bot: 'config supybot.directories.plugins [config supybot.directories.plugins], newPluginDirectory'."""))) registerGlobalValue(supybot, 'plugins', registry.SpaceSeparatedSetOfStrings([], _("""Determines what plugins will be loaded."""), orderAlphabetically=True)) registerGlobalValue(supybot.plugins, 'alwaysLoadImportant', registry.Boolean(True, _("""Determines whether the bot will always load important plugins (Admin, Channel, Config, Misc, Owner, and User) regardless of what their configured state is. Generally, if these plugins are configured not to load, you didn't do it on purpose, and you still want them to load. Users who don't want to load these plugins are smart enough to change the value of this variable appropriately :)"""))) ### # supybot.databases. For stuff relating to Supybot's databases (duh!) ### class Databases(registry.SpaceSeparatedListOfStrings): def __call__(self): v = super(Databases, self).__call__() if not v: v = ['anydbm', 'dbm', 'cdb', 'flat', 'pickle'] if 'sqlite' in sys.modules: v.insert(0, 'sqlite') if 'sqlite3' in sys.modules: v.insert(0, 'sqlite3') if 'sqlalchemy' in sys.modules: v.insert(0, 'sqlalchemy') return v def serialize(self): return ' '.join(self.value) registerGlobalValue(supybot, 'databases', Databases([], _("""Determines what databases are available for use. If this value is not configured (that is, if its value is empty) then sane defaults will be provided."""))) registerGroup(supybot.databases, 'users') registerGlobalValue(supybot.databases.users, 'filename', registry.String('users.conf', _("""Determines what filename will be used for the users database. This file will go into the directory specified by the supybot.directories.conf variable."""))) registerGlobalValue(supybot.databases.users, 'timeoutIdentification', registry.Integer(0, _("""Determines how long it takes identification to time out. If the value is less than or equal to zero, identification never times out."""))) registerGlobalValue(supybot.databases.users, 'allowUnregistration', registry.Boolean(False, _("""Determines whether the bot will allow users to unregister their users. This can wreak havoc with already-existing databases, so by default we don't allow it. Enable this at your own risk. (Do also note that this does not prevent the owner of the bot from using the unregister command.) """))) registerGroup(supybot.databases, 'ignores') registerGlobalValue(supybot.databases.ignores, 'filename', registry.String('ignores.conf', _("""Determines what filename will be used for the ignores database. This file will go into the directory specified by the supybot.directories.conf variable."""))) registerGroup(supybot.databases, 'channels') registerGlobalValue(supybot.databases.channels, 'filename', registry.String('channels.conf', _("""Determines what filename will be used for the channels database. This file will go into the directory specified by the supybot.directories.conf variable."""))) # TODO This will need to do more in the future (such as making sure link.allow # will let the link occur), but for now let's just leave it as this. class ChannelSpecific(registry.Boolean): def getChannelLink(self, channel): channelSpecific = supybot.databases.plugins.channelSpecific channels = [channel] def hasLinkChannel(channel): if not get(channelSpecific, channel): lchannel = get(channelSpecific.link, channel) if not get(channelSpecific.link.allow, lchannel): return False return channel != lchannel return False lchannel = channel while hasLinkChannel(lchannel): lchannel = get(channelSpecific.link, lchannel) if lchannel not in channels: channels.append(lchannel) else: # Found a cyclic link. We'll just use the current channel lchannel = channel break return lchannel registerGroup(supybot.databases, 'plugins') registerChannelValue(supybot.databases.plugins, 'requireRegistration', registry.Boolean(True, _("""Determines whether the bot will require user registration to use 'add' commands in database-based Supybot plugins."""))) registerChannelValue(supybot.databases.plugins, 'channelSpecific', ChannelSpecific(True, _("""Determines whether database-based plugins that can be channel-specific will be so. This can be overridden by individual channels. Do note that the bot needs to be restarted immediately after changing this variable or your db plugins may not work for your channel; also note that you may wish to set supybot.databases.plugins.channelSpecific.link appropriately if you wish to share a certain channel's databases globally."""))) registerChannelValue(supybot.databases.plugins.channelSpecific, 'link', ValidChannel('#', _("""Determines what channel global (non-channel-specific) databases will be considered a part of. This is helpful if you've been running channel-specific for awhile and want to turn the databases for your primary channel into global databases. If supybot.databases.plugins.channelSpecific.link.allow prevents linking, the current channel will be used. Do note that the bot needs to be restarted immediately after changing this variable or your db plugins may not work for your channel."""))) registerChannelValue(supybot.databases.plugins.channelSpecific.link, 'allow', registry.Boolean(True, _("""Determines whether another channel's global (non-channel-specific) databases will be allowed to link to this channel's databases. Do note that the bot needs to be restarted immediately after changing this variable or your db plugins may not work for your channel. """))) class CDB(registry.Boolean): def connect(self, filename): from . import cdb basename = os.path.basename(filename) journalName = supybot.directories.data.tmp.dirize(basename+'.journal') return cdb.open_db(filename, 'c', journalName=journalName, maxmods=self.maximumModifications()) registerGroup(supybot.databases, 'types') registerGlobalValue(supybot.databases.types, 'cdb', CDB(True, _("""Determines whether CDB databases will be allowed as a database implementation."""))) registerGlobalValue(supybot.databases.types.cdb, 'maximumModifications', registry.Probability(0.5, _("""Determines how often CDB databases will have their modifications flushed to disk. When the number of modified records is greater than this fraction of the total number of records, the database will be entirely flushed to disk."""))) # XXX Configuration variables for dbi, sqlite, flat, mysql, etc. ### # Protocol information. ### originalIsNick = ircutils.isNick def isNick(s, strictRfc=None, **kw): if strictRfc is None: strictRfc = supybot.protocols.irc.strictRfc() return originalIsNick(s, strictRfc=strictRfc, **kw) ircutils.isNick = isNick ### # supybot.protocols ### registerGroup(supybot, 'protocols') ### # supybot.protocols.irc ### registerGroup(supybot.protocols, 'irc') class Banmask(registry.SpaceSeparatedSetOfStrings): validStrings = ('exact', 'nick', 'user', 'host') def __init__(self, *args, **kwargs): assert self.validStrings, 'There must be some valid strings. ' \ 'This is a bug.' self.__parent = super(Banmask, self) self.__parent.__init__(*args, **kwargs) self.__doc__ = format('Valid values include %L.', list(map(repr, self.validStrings))) def help(self): strings = [s for s in self.validStrings if s] return format('%s Valid strings: %L.', self._help, strings) def normalize(self, s): lowered = s.lower() L = list(map(str.lower, self.validStrings)) try: i = L.index(lowered) except ValueError: return s # This is handled in setValue. return self.validStrings[i] def setValue(self, v): v = list(map(self.normalize, v)) for s in v: if s not in self.validStrings: self.error() self.__parent.setValue(self.List(v)) def makeBanmask(self, hostmask, options=None, channel=None): """Create a banmask from the given hostmask. If a style of banmask isn't specified via options, the value of conf.supybot.protocols.irc.banmask is used. options - A list specifying which parts of the hostmask should explicitly be matched: nick, user, host. If 'exact' is given, then only the exact hostmask will be used.""" if not channel: channel = dynamic.channel assert channel is None or ircutils.isChannel(channel) (nick, user, host) = ircutils.splitHostmask(hostmask) bnick = '*' buser = '*' bhost = '*' if not options: options = get(supybot.protocols.irc.banmask, channel) for option in options: if option == 'nick': bnick = nick elif option == 'user': buser = user elif option == 'host': bhost = host elif option == 'exact': return hostmask if (bnick, buser, bhost) == ('*', '*', '*') and \ ircutils.isUserHostmask(hostmask): return hostmask return ircutils.joinHostmask(bnick, buser, bhost) registerChannelValue(supybot.protocols.irc, 'banmask', Banmask(['host'], _("""Determines what will be used as the default banmask style."""))) registerGlobalValue(supybot.protocols.irc, 'strictRfc', registry.Boolean(False, _("""Determines whether the bot will strictly follow the RFC; currently this only affects what strings are considered to be nicks. If you're using a server or a network that requires you to message a nick such as services@this.network.server then you you should set this to False."""))) registerGlobalValue(supybot.protocols.irc, 'certfile', registry.String('', _("""Determines what certificate file (if any) the bot will use connect with SSL sockets by default."""))) registerGlobalValue(supybot.protocols.irc, 'umodes', registry.String('', _("""Determines what user modes the bot will request from the server when it first connects. Many people might choose +i; some networks allow +x, which indicates to the auth services on those networks that you should be given a fake host."""))) registerGlobalValue(supybot.protocols.irc, 'vhost', registry.String('', _("""Determines what vhost the bot will bind to before connecting a server (IRC, HTTP, ...) via IPv4."""))) registerGlobalValue(supybot.protocols.irc, 'vhostv6', registry.String('', _("""Determines what vhost the bot will bind to before connecting a server (IRC, HTTP, ...) via IPv6."""))) registerGlobalValue(supybot.protocols.irc, 'maxHistoryLength', registry.Integer(1000, _("""Determines how many old messages the bot will keep around in its history. Changing this variable will not take effect until the bot is restarted."""))) registerGlobalValue(supybot.protocols.irc, 'throttleTime', registry.Float(1.0, _("""A floating point number of seconds to throttle queued messages -- that is, messages will not be sent faster than once per throttleTime seconds."""))) registerGlobalValue(supybot.protocols.irc, 'ping', registry.Boolean(True, _("""Determines whether the bot will send PINGs to the server it's connected to in order to keep the connection alive and discover earlier when it breaks. Really, this option only exists for debugging purposes: you always should make it True unless you're testing some strange server issues."""))) registerGlobalValue(supybot.protocols.irc.ping, 'interval', registry.Integer(120, _("""Determines the number of seconds between sending pings to the server, if pings are being sent to the server."""))) registerGroup(supybot.protocols.irc, 'queuing') registerGlobalValue(supybot.protocols.irc.queuing, 'duplicates', registry.Boolean(False, _("""Determines whether the bot will refuse duplicated messages to be queued for delivery to the server. This is a safety mechanism put in place to prevent plugins from sending the same message multiple times; most of the time it doesn't matter, unless you're doing certain kinds of plugin hacking."""))) registerGroup(supybot.protocols.irc.queuing, 'rateLimit') registerGlobalValue(supybot.protocols.irc.queuing.rateLimit, 'join', registry.Float(0, _("""Determines how many seconds must elapse between JOINs sent to the server."""))) ### # supybot.protocols.http ### registerGroup(supybot.protocols, 'http') registerGlobalValue(supybot.protocols.http, 'peekSize', registry.PositiveInteger(8192, _("""Determines how many bytes the bot will 'peek' at when looking through a URL for a doctype or title or something similar. It'll give up after it reads this many bytes, even if it hasn't found what it was looking for."""))) class HttpProxy(registry.String): """Value must be a valid hostname:port string.""" def setValue(self, v): proxies = {} if v != "": if isSocketAddress(v): proxies = { 'http': v, 'https': v } else: self.error() proxyHandler = ProxyHandler(proxies) proxyOpenerDirector = build_opener(proxyHandler) install_opener(proxyOpenerDirector) super(HttpProxy, self).setValue(v) registerGlobalValue(supybot.protocols.http, 'proxy', HttpProxy('', _("""Determines what proxy all HTTP requests should go through. The value should be of the form 'host:port'."""))) utils.web.proxy = supybot.protocols.http.proxy ### # supybot.protocols.ssl ### registerGroup(supybot.protocols, 'ssl') registerGlobalValue(supybot.protocols.ssl, 'verifyCertificates', registry.Boolean(False, _("""Determines whether server certificates will be verified, which checks whether the server certificate is signed by a known certificate authority, and aborts the connection if it is not."""))) ### # HTTP server ### registerGroup(supybot, 'servers') registerGroup(supybot.servers, 'http') class IP(registry.String): """Value must be a valid IP.""" def setValue(self, v): if v and not utils.net.isIP(v): self.error() else: registry.String.setValue(self, v) class ListOfIPs(registry.SpaceSeparatedListOfStrings): Value = IP registerGlobalValue(supybot.servers.http, 'singleStack', registry.Boolean(True, _("""If true, uses IPV6_V6ONLY to disable forwaring of IPv4 traffic to IPv6 sockets. On *nix, has the same effect as setting kernel variable net.ipv6.bindv6only to 1."""))) registerGlobalValue(supybot.servers.http, 'hosts4', ListOfIPs(['0.0.0.0'], _("""Space-separated list of IPv4 hosts the HTTP server will bind."""))) registerGlobalValue(supybot.servers.http, 'hosts6', ListOfIPs(['::0'], _("""Space-separated list of IPv6 hosts the HTTP server will bind."""))) registerGlobalValue(supybot.servers.http, 'port', registry.Integer(8080, _("""Determines what port the HTTP server will bind."""))) registerGlobalValue(supybot.servers.http, 'keepAlive', registry.Boolean(False, _("""Determines whether the server will stay alive if no plugin is using it. This also means that the server will start even if it is not used."""))) registerGlobalValue(supybot.servers.http, 'favicon', registry.String('', _("""Determines the path of the file served as favicon to browsers."""))) ### # Especially boring stuff. ### registerGlobalValue(supybot, 'defaultIgnore', registry.Boolean(False, _("""Determines whether the bot will ignore unidentified users by default. Of course, that'll make it particularly hard for those users to register or identify with the bot without adding their hostmasks, but that's your problem to solve."""))) registerGlobalValue(supybot, 'externalIP', IP('', _("""A string that is the external IP of the bot. If this is the empty string, the bot will attempt to find out its IP dynamically (though sometimes that doesn't work, hence this variable). This variable is not used by Limnoria and its built-in plugins: see supybot.protocols.irc.vhost / supybot.protocols.irc.vhost6 to set the IRC bind host, and supybot.servers.http.hosts4 / supybot.servers.http.hosts6 to set the HTTP server bind host."""))) class SocketTimeout(registry.PositiveInteger): """Value must be an integer greater than supybot.drivers.poll and must be greater than or equal to 1.""" def setValue(self, v): if v < supybot.drivers.poll() or v < 1: self.error() registry.PositiveInteger.setValue(self, v) socket.setdefaulttimeout(self.value) registerGlobalValue(supybot, 'defaultSocketTimeout', SocketTimeout(10, _("""Determines what the default timeout for socket objects will be. This means that *all* sockets will timeout when this many seconds has gone by (unless otherwise modified by the author of the code that uses the sockets)."""))) registerGlobalValue(supybot, 'pidFile', registry.String('', _("""Determines what file the bot should write its PID (Process ID) to, so you can kill it more easily. If it's left unset (as is the default) then no PID file will be written. A restart is required for changes to this variable to take effect."""))) ### # Debugging options. ### registerGroup(supybot, 'debug') registerGlobalValue(supybot.debug, 'threadAllCommands', registry.Boolean(False, _("""Determines whether the bot will automatically thread all commands."""))) registerGlobalValue(supybot.debug, 'flushVeryOften', registry.Boolean(False, _("""Determines whether the bot will automatically flush all flushers *very* often. Useful for debugging when you don't know what's breaking or when, but think that it might be logged."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/commands.py0000644000175000017500000011377513233426066016277 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009-2010,2015, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Includes wrappers for commands. """ import time import getopt import inspect import threading import multiprocessing #python2.6 or later! try: import resource except ImportError: # Windows! resource = None from . import callbacks, conf, ircdb, ircmsgs, ircutils, log, \ utils, world from .utils import minisix from .i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization() ### # Non-arg wrappers -- these just change the behavior of a command without # changing the arguments given to it. ### # Thread has to be a non-arg wrapper because by the time we're parsing and # validating arguments, we're inside the function we'd want to thread. def thread(f): """Makes sure a command spawns a thread when called.""" def newf(self, irc, msg, args, *L, **kwargs): if world.isMainThread(): targetArgs = (self.callingCommand, irc, msg, args) + tuple(L) t = callbacks.CommandThread(target=self._callCommand, args=targetArgs, kwargs=kwargs) t.start() else: f(self, irc, msg, args, *L, **kwargs) return utils.python.changeFunctionName(newf, f.__name__, f.__doc__) class ProcessTimeoutError(Exception): """Gets raised when a process is killed due to timeout.""" pass def process(f, *args, **kwargs): """Runs a function in a subprocess. Several extra keyword arguments can be supplied. , the pluginname, and , the command name, are strings used to create the process name, for identification purposes. , if supplied, limits the length of execution of target function to seconds. , if supplied, limits the memory used by the target function.""" timeout = kwargs.pop('timeout', None) heap_size = kwargs.pop('heap_size', None) if resource and heap_size is None: heap_size = resource.RLIM_INFINITY if world.disableMultiprocessing: pn = kwargs.pop('pn', 'Unknown') cn = kwargs.pop('cn', 'unknown') try: return f(*args, **kwargs) except Exception as e: raise e try: q = multiprocessing.Queue() except OSError: log.error('Using multiprocessing.Queue raised an OSError.\n' 'This is probably caused by your system denying semaphore\n' 'usage. You should run these two commands:\n' '\tsudo rmdir /dev/shm\n' '\tsudo ln -Tsf /{run,dev}/shm\n' '(See https://github.com/travis-ci/travis-core/issues/187\n' 'for more information about this bug.)\n') raise def newf(f, q, *args, **kwargs): if resource: rsrc = resource.RLIMIT_DATA resource.setrlimit(rsrc, (heap_size, heap_size)) try: r = f(*args, **kwargs) q.put(r) except Exception as e: q.put(e) targetArgs = (f, q,) + args p = callbacks.CommandProcess(target=newf, args=targetArgs, kwargs=kwargs) p.start() p.join(timeout) if p.is_alive(): p.terminate() q.close() raise ProcessTimeoutError("%s aborted due to timeout." % (p.name,)) try: v = q.get(block=False) except minisix.queue.Empty: return None finally: q.close() if isinstance(v, Exception): raise v else: return v def regexp_wrapper(s, reobj, timeout, plugin_name, fcn_name): '''A convenient wrapper to stuff regexp search queries through a subprocess. This is used because specially-crafted regexps can use exponential time and hang the bot.''' def re_bool(s, reobj): """Since we can't enqueue match objects into the multiprocessing queue, we'll just wrap the function to return bools.""" if reobj.search(s) is not None: return True else: return False try: v = process(re_bool, s, reobj, timeout=timeout, pn=plugin_name, cn=fcn_name) return v except ProcessTimeoutError: return False class UrlSnarfThread(world.SupyThread): def __init__(self, *args, **kwargs): assert 'url' in kwargs kwargs['name'] = 'Thread #%s (for snarfing %s)' % \ (world.threadsSpawned, kwargs.pop('url')) super(UrlSnarfThread, self).__init__(*args, **kwargs) self.setDaemon(True) def run(self): try: super(UrlSnarfThread, self).run() except utils.web.Error as e: log.debug('Exception in urlSnarfer: %s', utils.exnToString(e)) class SnarfQueue(ircutils.FloodQueue): timeout = conf.supybot.snarfThrottle def key(self, channel): return channel _snarfed = SnarfQueue() class SnarfIrc(object): def __init__(self, irc, channel, url): self.irc = irc self.url = url self.channel = channel def __getattr__(self, attr): return getattr(self.irc, attr) def reply(self, *args, **kwargs): _snarfed.enqueue(self.channel, self.url) return self.irc.reply(*args, **kwargs) # This lock is used to serialize the calls to snarfers, so # earlier snarfers are guaranteed to beat out later snarfers. _snarfLock = threading.Lock() def urlSnarfer(f): """Protects the snarfer from loops (with other bots) and whatnot.""" def newf(self, irc, msg, match, *L, **kwargs): url = match.group(0) channel = msg.args[0] if not irc.isChannel(channel) or (ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg)): return if ircdb.channels.getChannel(channel).lobotomized: self.log.debug('Not snarfing in %s: lobotomized.', channel) return if _snarfed.has(channel, url): self.log.info('Throttling snarf of %s in %s.', url, channel) return irc = SnarfIrc(irc, channel, url) def doSnarf(): _snarfLock.acquire() try: # This has to be *after* we've acquired the lock so we can be # sure that all previous urlSnarfers have already run to # completion. if msg.repliedTo: self.log.debug('Not snarfing, msg is already repliedTo.') return f(self, irc, msg, match, *L, **kwargs) finally: _snarfLock.release() if threading.currentThread() is not world.mainThread: doSnarf() else: L = list(L) t = UrlSnarfThread(target=doSnarf, url=url) t.start() newf = utils.python.changeFunctionName(newf, f.__name__, f.__doc__) return newf ### # Converters, which take irc, msg, args, and a state object, and build up the # validated and converted args for the method in state.args. ### # This is just so we can centralize this, since it may change. def _int(s): base = 10 if s.startswith('0x'): base = 16 s = s[2:] elif s.startswith('0b'): base = 2 s = s[2:] elif s.startswith('0') and len(s) > 1: base = 8 s = s[1:] try: return int(s, base) except ValueError: if base == 10 and '.' not in s: try: return int(float(s)) except OverflowError: raise ValueError('I don\'t understand numbers that large.') else: raise def getInt(irc, msg, args, state, type=_('integer'), p=None): try: i = _int(args[0]) if p is not None: if not p(i): state.errorInvalid(type, args[0]) state.args.append(i) del args[0] except ValueError: state.errorInvalid(type, args[0]) def getNonInt(irc, msg, args, state, type=_('non-integer value')): try: _int(args[0]) state.errorInvalid(type, args[0]) except ValueError: state.args.append(args.pop(0)) def getLong(irc, msg, args, state, type='long'): getInt(irc, msg, args, state, type) state.args[-1] = minisix.long(state.args[-1]) def getFloat(irc, msg, args, state, type=_('floating point number')): try: state.args.append(float(args[0])) del args[0] except ValueError: state.errorInvalid(type, args[0]) def getPositiveInt(irc, msg, args, state, *L): getInt(irc, msg, args, state, p=lambda i: i>0, type=_('positive integer'), *L) def getNonNegativeInt(irc, msg, args, state, *L): getInt(irc, msg, args, state, p=lambda i: i>=0, type=_('non-negative integer'), *L) def getIndex(irc, msg, args, state): getInt(irc, msg, args, state, type=_('index')) if state.args[-1] > 0: state.args[-1] -= 1 def getId(irc, msg, args, state, kind=None): type = 'id' if kind is not None and not kind.endswith('id'): type = kind + ' id' original = args[0] try: args[0] = args[0].lstrip('#') getInt(irc, msg, args, state, type=type) except Exception: args[0] = original raise def getExpiry(irc, msg, args, state): now = int(time.time()) try: expires = _int(args[0]) if expires: expires += now state.args.append(expires) del args[0] except ValueError: state.errorInvalid(_('number of seconds'), args[0]) def getBoolean(irc, msg, args, state): try: state.args.append(utils.str.toBool(args[0])) del args[0] except ValueError: state.errorInvalid(_('boolean'), args[0]) def getNetworkIrc(irc, msg, args, state, errorIfNoMatch=False): if args: for otherIrc in world.ircs: if otherIrc.network.lower() == args[0].lower(): state.args.append(otherIrc) del args[0] return if errorIfNoMatch: raise callbacks.ArgumentError else: state.args.append(irc) def getHaveVoice(irc, msg, args, state, action=_('do that')): getChannel(irc, msg, args, state) if state.channel not in irc.state.channels: state.error(_('I\'m not even in %s.') % state.channel, Raise=True) if not irc.state.channels[state.channel].isVoice(irc.nick): state.error(_('I need to be voiced to %s.') % action, Raise=True) def getHaveVoicePlus(irc, msg, args, state, action=_('do that')): getChannel(irc, msg, args, state) if state.channel not in irc.state.channels: state.error(_('I\'m not even in %s.') % state.channel, Raise=True) if not irc.state.channels[state.channel].isVoicePlus(irc.nick): # isOp includes owners and protected users state.error(_('I need to be at least voiced to %s.') % action, Raise=True) def getHaveHalfop(irc, msg, args, state, action=_('do that')): getChannel(irc, msg, args, state) if state.channel not in irc.state.channels: state.error(_('I\'m not even in %s.') % state.channel, Raise=True) if not irc.state.channels[state.channel].isHalfop(irc.nick): state.error(_('I need to be halfopped to %s.') % action, Raise=True) def getHaveHalfopPlus(irc, msg, args, state, action=_('do that')): getChannel(irc, msg, args, state) if state.channel not in irc.state.channels: state.error(_('I\'m not even in %s.') % state.channel, Raise=True) if not irc.state.channels[state.channel].isHalfopPlus(irc.nick): # isOp includes owners and protected users state.error(_('I need to be at least halfopped to %s.') % action, Raise=True) def getHaveOp(irc, msg, args, state, action=_('do that')): getChannel(irc, msg, args, state) if state.channel not in irc.state.channels: state.error(_('I\'m not even in %s.') % state.channel, Raise=True) if not irc.state.channels[state.channel].isOp(irc.nick): state.error(_('I need to be opped to %s.') % action, Raise=True) def validChannel(irc, msg, args, state): if irc.isChannel(args[0]): state.args.append(args.pop(0)) else: state.errorInvalid(_('channel'), args[0]) def getHostmask(irc, msg, args, state): if ircutils.isUserHostmask(args[0]) or \ (not conf.supybot.protocols.irc.strictRfc() and args[0].startswith('$')): state.args.append(args.pop(0)) else: try: hostmask = irc.state.nickToHostmask(args[0]) state.args.append(hostmask) del args[0] except KeyError: state.errorInvalid(_('nick or hostmask'), args[0]) def getBanmask(irc, msg, args, state): getHostmask(irc, msg, args, state) getChannel(irc, msg, args, state) banmaskstyle = conf.supybot.protocols.irc.banmask state.args[-1] = banmaskstyle.makeBanmask(state.args[-1], channel=state.channel) def getUser(irc, msg, args, state): try: state.args.append(ircdb.users.getUser(msg.prefix)) except KeyError: state.errorNotRegistered(Raise=True) def getOtherUser(irc, msg, args, state): # Although ircdb.users.getUser could accept a hostmask, we're explicitly # excluding that from our interface with this check if ircutils.isUserHostmask(args[0]): state.errorNoUser(args[0]) try: state.args.append(ircdb.users.getUser(args[0])) del args[0] except KeyError: try: getHostmask(irc, msg, [args[0]], state) hostmask = state.args.pop() state.args.append(ircdb.users.getUser(hostmask)) del args[0] except (KeyError, callbacks.Error): state.errorNoUser(name=args[0]) def _getRe(f): def get(irc, msg, args, state, convert=True): original = args[:] s = args.pop(0) def isRe(s): try: f(s) return True except ValueError: return False try: while len(s) < 512 and not isRe(s): s += ' ' + args.pop(0) if len(s) < 512: if convert: state.args.append(f(s)) else: state.args.append(s) else: raise ValueError except (ValueError, IndexError): args[:] = original state.errorInvalid(_('regular expression'), s) return get getMatcher = _getRe(utils.str.perlReToPythonRe) getMatcherMany = _getRe(utils.str.perlReToFindall) getReplacer = _getRe(utils.str.perlReToReplacer) def getNick(irc, msg, args, state): if ircutils.isNick(args[0], conf.supybot.protocols.irc.strictRfc()): if 'nicklen' in irc.state.supported: if len(args[0]) > irc.state.supported['nicklen']: state.errorInvalid(_('nick'), args[0], _('That nick is too long for this server.')) state.args.append(args.pop(0)) else: state.errorInvalid(_('nick'), args[0]) def getSeenNick(irc, msg, args, state, errmsg=None): try: irc.state.nickToHostmask(args[0]) state.args.append(args.pop(0)) except KeyError: if errmsg is None: errmsg = _('I haven\'t seen %s.') % args[0] state.error(errmsg, Raise=True) def getChannel(irc, msg, args, state): if state.channel: return if args and irc.isChannel(args[0]): channel = args.pop(0) elif irc.isChannel(msg.args[0]): channel = msg.args[0] else: state.log.debug('Raising ArgumentError because there is no channel.') raise callbacks.ArgumentError state.channel = channel state.args.append(channel) def getChannelDb(irc, msg, args, state, **kwargs): channelSpecific = conf.supybot.databases.plugins.channelSpecific try: getChannel(irc, msg, args, state, **kwargs) channel = channelSpecific.getChannelLink(state.channel) state.channel = channel state.args[-1] = channel except (callbacks.ArgumentError, IndexError): if channelSpecific(): raise channel = channelSpecific.link() if not conf.get(channelSpecific.link.allow, channel): log.warning('channelSpecific.link is globally set to %s, but ' '%s disallowed linking to its db.', channel, channel) raise else: channel = channelSpecific.getChannelLink(channel) state.channel = channel state.args.append(channel) def inChannel(irc, msg, args, state): getChannel(irc, msg, args, state) if state.channel not in irc.state.channels: state.error(_('I\'m not in %s.') % state.channel, Raise=True) def onlyInChannel(irc, msg, args, state): if not (irc.isChannel(msg.args[0]) and msg.args[0] in irc.state.channels): state.error(_('This command may only be given in a channel that I am ' 'in.'), Raise=True) else: state.channel = msg.args[0] state.args.append(state.channel) def callerInGivenChannel(irc, msg, args, state): channel = args[0] if irc.isChannel(channel): if channel in irc.state.channels: if msg.nick in irc.state.channels[channel].users: state.args.append(args.pop(0)) else: state.error(_('You must be in %s.') % channel, Raise=True) else: state.error(_('I\'m not in %s.') % channel, Raise=True) else: state.errorInvalid(_('channel'), args[0]) def nickInChannel(irc, msg, args, state): originalArgs = state.args[:] inChannel(irc, msg, args, state) state.args = originalArgs if args[0] not in irc.state.channels[state.channel].users: state.error(_('%s is not in %s.') % (args[0], state.channel), Raise=True) state.args.append(args.pop(0)) def getChannelOrNone(irc, msg, args, state): try: getChannel(irc, msg, args, state) except callbacks.ArgumentError: state.args.append(None) def getChannelOrGlobal(irc, msg, args, state): if args and args[0] == 'global': channel = args.pop(0) channel = 'global' elif args and irc.isChannel(args[0]): channel = args.pop(0) state.channel = channel elif irc.isChannel(msg.args[0]): channel = msg.args[0] state.channel = channel else: state.log.debug('Raising ArgumentError because there is no channel.') raise callbacks.ArgumentError state.args.append(channel) def checkChannelCapability(irc, msg, args, state, cap): getChannel(irc, msg, args, state) cap = ircdb.canonicalCapability(cap) cap = ircdb.makeChannelCapability(state.channel, cap) if not ircdb.checkCapability(msg.prefix, cap): state.errorNoCapability(cap, Raise=True) def getOp(irc, msg, args, state): checkChannelCapability(irc, msg, args, state, 'op') def getHalfop(irc, msg, args, state): checkChannelCapability(irc, msg, args, state, 'halfop') def getVoice(irc, msg, args, state): checkChannelCapability(irc, msg, args, state, 'voice') def getLowered(irc, msg, args, state): state.args.append(ircutils.toLower(args.pop(0))) def getSomething(irc, msg, args, state, errorMsg=None, p=None): if p is None: p = lambda _: True if not args[0] or not p(args[0]): if errorMsg is None: errorMsg = _('You must not give the empty string as an argument.') state.error(errorMsg, Raise=True) else: state.args.append(args.pop(0)) def getSomethingNoSpaces(irc, msg, args, state, *L): def p(s): return len(s.split(None, 1)) == 1 L = L or [_('You must not give a string containing spaces as an argument.')] getSomething(irc, msg, args, state, p=p, *L) def private(irc, msg, args, state): if irc.isChannel(msg.args[0]): state.errorRequiresPrivacy(Raise=True) def public(irc, msg, args, state, errmsg=None): if not irc.isChannel(msg.args[0]): if errmsg is None: errmsg = _('This message must be sent in a channel.') state.error(errmsg, Raise=True) def checkCapability(irc, msg, args, state, cap): cap = ircdb.canonicalCapability(cap) if not ircdb.checkCapability(msg.prefix, cap): state.errorNoCapability(cap, Raise=True) def checkCapabilityButIgnoreOwner(irc, msg, args, state, cap): cap = ircdb.canonicalCapability(cap) if not ircdb.checkCapability(msg.prefix, cap, ignoreOwner=True): state.errorNoCapability(cap, Raise=True) def owner(irc, msg, args, state): checkCapability(irc, msg, args, state, 'owner') def admin(irc, msg, args, state): checkCapability(irc, msg, args, state, 'admin') def anything(irc, msg, args, state): state.args.append(args.pop(0)) def getGlob(irc, msg, args, state): glob = args.pop(0) if '*' not in glob and '?' not in glob: glob = '*%s*' % glob state.args.append(glob) def getUrl(irc, msg, args, state): if utils.web.urlRe.match(args[0]): state.args.append(args.pop(0)) else: state.errorInvalid(_('url'), args[0]) def getEmail(irc, msg, args, state): if utils.net.emailRe.match(args[0]): state.args.append(args.pop(0)) else: state.errorInvalid(_('email'), args[0]) def getHttpUrl(irc, msg, args, state): if utils.web.httpUrlRe.match(args[0]): state.args.append(args.pop(0)) elif utils.web.httpUrlRe.match('http://' + args[0]): state.args.append('http://' + args.pop(0)) else: state.errorInvalid(_('http url'), args[0]) def getNow(irc, msg, args, state): state.args.append(int(time.time())) def getCommandName(irc, msg, args, state): if ' ' in args[0]: state.errorInvalid(_('command name'), args[0]) else: state.args.append(callbacks.canonicalName(args.pop(0))) def getIp(irc, msg, args, state): if utils.net.isIP(args[0]): state.args.append(args.pop(0)) else: state.errorInvalid(_('ip'), args[0]) def getLetter(irc, msg, args, state): if len(args[0]) == 1: state.args.append(args.pop(0)) else: state.errorInvalid(_('letter'), args[0]) def getMatch(irc, msg, args, state, regexp, errmsg): m = regexp.search(args[0]) if m is not None: state.args.append(m) del args[0] else: state.error(errmsg, Raise=True) def getLiteral(irc, msg, args, state, literals, errmsg=None): # ??? Should we allow abbreviations? if isinstance(literals, minisix.string_types): literals = (literals,) abbrevs = utils.abbrev(literals) if args[0] in abbrevs: state.args.append(abbrevs[args.pop(0)]) elif errmsg is not None: state.error(errmsg, Raise=True) else: raise callbacks.ArgumentError def getTo(irc, msg, args, state): if args[0].lower() == 'to': args.pop(0) def getPlugin(irc, msg, args, state, require=True): cb = irc.getCallback(args[0]) if cb is not None: state.args.append(cb) del args[0] elif require: state.errorInvalid(_('plugin'), args[0]) else: state.args.append(None) def getIrcColor(irc, msg, args, state): if args[0] in ircutils.mircColors: state.args.append(ircutils.mircColors[args.pop(0)]) else: state.errorInvalid(_('irc color')) def getText(irc, msg, args, state): if args: state.args.append(' '.join(args)) args[:] = [] else: raise IndexError wrappers = ircutils.IrcDict({ 'admin': admin, 'anything': anything, 'banmask': getBanmask, 'boolean': getBoolean, 'callerInGivenChannel': callerInGivenChannel, 'isGranted': getHaveHalfopPlus, # Backward compatibility 'capability': getSomethingNoSpaces, 'channel': getChannel, 'channelOrGlobal': getChannelOrGlobal, 'channelDb': getChannelDb, 'checkCapability': checkCapability, 'checkCapabilityButIgnoreOwner': checkCapabilityButIgnoreOwner, 'checkChannelCapability': checkChannelCapability, 'color': getIrcColor, 'commandName': getCommandName, 'email': getEmail, 'expiry': getExpiry, 'filename': getSomething, # XXX Check for validity. 'float': getFloat, 'glob': getGlob, 'halfop': getHalfop, 'haveHalfop': getHaveHalfop, 'haveHalfop+': getHaveHalfopPlus, 'haveOp': getHaveOp, 'haveOp+': getHaveOp, # We don't handle modes greater than op. 'haveVoice': getHaveVoice, 'haveVoice+': getHaveVoicePlus, 'hostmask': getHostmask, 'httpUrl': getHttpUrl, 'id': getId, 'inChannel': inChannel, 'index': getIndex, 'int': getInt, 'ip': getIp, 'letter': getLetter, 'literal': getLiteral, 'long': getLong, 'lowered': getLowered, 'matches': getMatch, 'networkIrc': getNetworkIrc, 'nick': getNick, 'nickInChannel': nickInChannel, 'nonInt': getNonInt, 'nonNegativeInt': getNonNegativeInt, 'now': getNow, 'onlyInChannel': onlyInChannel, 'op': getOp, 'otherUser': getOtherUser, 'owner': owner, 'plugin': getPlugin, 'positiveInt': getPositiveInt, 'private': private, 'public': public, 'regexpMatcher': getMatcher, 'regexpMatcherMany': getMatcherMany, 'regexpReplacer': getReplacer, 'seenNick': getSeenNick, 'something': getSomething, 'somethingWithoutSpaces': getSomethingNoSpaces, 'text': getText, 'to': getTo, 'url': getUrl, 'user': getUser, 'validChannel': validChannel, 'voice': getVoice, }) def addConverter(name, wrapper): wrappers[name] = wrapper class UnknownConverter(KeyError): pass def getConverter(name): try: return wrappers[name] except KeyError as e: raise UnknownConverter(str(e)) def callConverter(name, irc, msg, args, state, *L): getConverter(name)(irc, msg, args, state, *L) ### # Contexts. These determine what the nature of conversions is; whether they're # defaulted, or many of them are allowed, etc. Contexts should be reusable; # i.e., they should not maintain state between calls. ### def contextify(spec): if not isinstance(spec, context): spec = context(spec) return spec def setDefault(state, default): if callable(default): state.args.append(default()) else: state.args.append(default) class context(object): def __init__(self, spec): self.args = () self.spec = spec # for repr if isinstance(spec, tuple): assert spec, 'tuple spec must not be empty.' self.args = spec[1:] self.converter = getConverter(spec[0]) elif spec is None: self.converter = getConverter('anything') elif isinstance(spec, minisix.string_types): self.args = () self.converter = getConverter(spec) else: assert isinstance(spec, context) self.converter = spec def __call__(self, irc, msg, args, state): log.debug('args before %r: %r', self, args) self.converter(irc, msg, args, state, *self.args) log.debug('args after %r: %r', self, args) def __repr__(self): return '<%s for %s>' % (self.__class__.__name__, self.spec) class rest(context): def __call__(self, irc, msg, args, state): if args: original = args[:] args[:] = [' '.join(args)] try: super(rest, self).__call__(irc, msg, args, state) except Exception: args[:] = original else: raise IndexError # additional means: Look for this (and make sure it's of this type). If # there are no arguments for us to check, then use our default. class additional(context): def __init__(self, spec, default=None): self.__parent = super(additional, self) self.__parent.__init__(spec) self.default = default def __call__(self, irc, msg, args, state): try: self.__parent.__call__(irc, msg, args, state) except IndexError: log.debug('Got IndexError, returning default.') setDefault(state, self.default) # optional means: Look for this, but if it's not the type I'm expecting or # there are no arguments for us to check, then use the default value. class optional(additional): def __call__(self, irc, msg, args, state): try: super(optional, self).__call__(irc, msg, args, state) except (callbacks.ArgumentError, callbacks.Error) as e: log.debug('Got %s, returning default.', utils.exnToString(e)) state.errored = False setDefault(state, self.default) class any(context): def __init__(self, spec, continueOnError=False): self.__parent = super(any, self) self.__parent.__init__(spec) self.continueOnError = continueOnError def __call__(self, irc, msg, args, state): st = state.essence() try: while args: self.__parent.__call__(irc, msg, args, st) except IndexError: pass except (callbacks.ArgumentError, callbacks.Error) as e: if not self.continueOnError: raise else: log.debug('Got %s, returning default.', utils.exnToString(e)) pass state.args.append(st.args) class many(any): def __call__(self, irc, msg, args, state): super(many, self).__call__(irc, msg, args, state) if not state.args[-1]: state.args.pop() raise callbacks.ArgumentError class first(context): def __init__(self, *specs, **kw): if 'default' in kw: self.default = kw.pop('default') assert not kw, 'Bad kwargs for first.__init__' self.spec = specs # for __repr__ self.specs = list(map(contextify, specs)) def __call__(self, irc, msg, args, state): errored = False for spec in self.specs: try: spec(irc, msg, args, state) return except Exception as e: e2 = e # 'e' is local. errored = state.errored state.errored = False continue if hasattr(self, 'default'): state.args.append(self.default) else: state.errored = errored raise e2 class reverse(context): def __call__(self, irc, msg, args, state): args[:] = args[::-1] super(reverse, self).__call__(irc, msg, args, state) args[:] = args[::-1] class commalist(context): def __call__(self, irc, msg, args, state): original = args[:] st = state.essence() trailingComma = True try: while trailingComma: arg = args.pop(0) if not arg.endswith(','): trailingComma = False for part in arg.split(','): if part: # trailing commas super(commalist, self).__call__(irc, msg, [part], st) state.args.append(st.args) except Exception: args[:] = original raise class getopts(context): """The empty string indicates that no argument is taken; None indicates that there is no converter for the argument.""" def __init__(self, getopts): self.spec = getopts # for repr self.getopts = {} self.getoptL = [] self.getoptLs = '' for (name, spec) in getopts.items(): if spec == '': if len(name) == 1: self.getoptLs += name self.getopts[name] = None self.getoptL.append(name) self.getopts[name] = None else: if len(name) == 1: self.getoptLs += name + ':' self.getopts[name] = contextify(spec) self.getoptL.append(name + '=') self.getopts[name] = contextify(spec) log.debug('getopts: %r', self.getopts) log.debug('getoptL: %r', self.getoptL) def __call__(self, irc, msg, args, state): log.debug('args before %r: %r', self, args) (optlist, rest) = getopt.getopt(args, self.getoptLs, self.getoptL) getopts = [] for (opt, arg) in optlist: if opt.startswith('--'): opt = opt[2:] # Strip -- else: opt = opt[1:] log.debug('opt: %r, arg: %r', opt, arg) context = self.getopts[opt] if context is not None: st = state.essence() context(irc, msg, [arg], st) assert len(st.args) == 1 getopts.append((opt, st.args[0])) else: getopts.append((opt, True)) state.args.append(getopts) args[:] = rest log.debug('args after %r: %r', self, args) ### # This is our state object, passed to converters along with irc, msg, and args. ### class State(object): log = log def __init__(self, types): self.args = [] self.kwargs = {} self.types = types self.channel = None self.errored = False def __getattr__(self, attr): if attr.startswith('error'): self.errored = True return getattr(dynamic.irc, attr) else: raise AttributeError(attr) def essence(self): st = State(self.types) for (attr, value) in self.__dict__.items(): if attr not in ('args', 'kwargs'): setattr(st, attr, value) return st def __repr__(self): return '%s(args=%r, kwargs=%r, channel=%r)' % (self.__class__.__name__, self.args, self.kwargs, self.channel) ### # This is a compiled Spec object. ### class Spec(object): def _state(self, types, attrs={}): st = State(types) st.__dict__.update(attrs) st.allowExtra = self.allowExtra return st def __init__(self, types, allowExtra=False): self.types = types self.allowExtra = allowExtra utils.seq.mapinto(contextify, self.types) def __call__(self, irc, msg, args, stateAttrs={}): state = self._state(self.types[:], stateAttrs) while state.types: context = state.types.pop(0) try: context(irc, msg, args, state) except IndexError: raise callbacks.ArgumentError if args and not state.allowExtra: log.debug('args and not self.allowExtra: %r', args) raise callbacks.ArgumentError return state def _wrap(f, specList=[], name=None, checkDoc=True, **kw): name = name or f.__name__ assert (not checkDoc) or (hasattr(f, '__doc__') and f.__doc__), \ 'Command %r has no docstring.' % name spec = Spec(specList, **kw) def newf(self, irc, msg, args, **kwargs): state = spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log}) self.log.debug('State before call: %s', state) if state.errored: self.log.debug('Refusing to call %s due to state.errored.', f) else: try: f(self, irc, msg, args, *state.args, **state.kwargs) except TypeError: self.log.error('Spec: %s', specList) self.log.error('Received args: %s', args) code = f.__code__ funcArgs = inspect.getargs(code)[0][len(self.commandArgs):] self.log.error('Extra args: %s', funcArgs) self.log.debug('Make sure you did not wrap a wrapped ' 'function ;)') raise newf2 = utils.python.changeFunctionName(newf, name, f.__doc__) newf2.__module__ = f.__module__ return internationalizeDocstring(newf2) def wrap(f, *args, **kwargs): if callable(f): # Old-style call OR decorator syntax with no converter. # f is the command. return _wrap(f, *args, **kwargs) else: # Call with the Python decorator syntax assert isinstance(f, list) or isinstance(f, tuple) specList = f def decorator(f): return _wrap(f, specList, *args, **kwargs) return decorator wrap.__doc__ = """Useful wrapper for plugin commands. Valid converters are: %s. :param f: A command, taking (self, irc, msg, args, ...) as arguments :param specList: A list of converters and contexts""" % \ ', '.join(sorted(wrappers.keys())) __all__ = [ # Contexts. 'any', 'many', 'optional', 'additional', 'rest', 'getopts', 'first', 'reverse', 'commalist', # Converter helpers. 'getConverter', 'addConverter', 'callConverter', # Decorators. 'urlSnarfer', 'thread', # Functions. 'wrap', 'process', 'regexp_wrapper', # Stuff for testing. 'Spec', ] # This doesn't work. Suck. ## if world.testing: ## __all__.append('Spec') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/cdb.py0000644000175000017500000003513613233426066015220 0ustar valval00000000000000### # 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. ### """ Database module, similar to dbhash. Uses a format similar to (if not entirely the same as) DJB's CDB . """ from __future__ import division import os import sys import struct import os.path from . import utils from .utils import minisix def hash(s): """DJB's hash function for CDB.""" h = 5381 for c in s: h = ((h + (h << 5)) ^ ord(c)) & minisix.L(0xFFFFFFFF) return h def unpack2Ints(s): """Returns two ints unpacked from the binary string s.""" return struct.unpack('%s\n' % (len(key), len(value), key, value)) def open_db(filename, mode='r', **kwargs): """Opens a database; used for compatibility with other database modules.""" if mode == 'r': return Reader(filename, **kwargs) elif mode == 'w': return ReaderWriter(filename, **kwargs) elif mode == 'c': if os.path.exists(filename): return ReaderWriter(filename, **kwargs) else: maker = Maker(filename) maker.finish() return ReaderWriter(filename, **kwargs) elif mode == 'n': maker = Maker(filename) maker.finish() return ReaderWriter(filename, **kwargs) else: raise ValueError('Invalid flag: %s' % mode) def shelf(filename, *args, **kwargs): """Opens a new shelf database object.""" if os.path.exists(filename): return Shelf(filename, *args, **kwargs) else: maker = Maker(filename) maker.finish() return Shelf(filename, *args, **kwargs) def _readKeyValue(fd): klen = 0 dlen = 0 s = initchar = fd.read(1) if s == '': return (None, None, None) s = fd.read(1) while s != ',': klen = 10 * klen + int(s) s = fd.read(1) s = fd.read(1) while s != ':': dlen = 10 * dlen + int(s) s = fd.read(1) key = fd.read(klen) assert fd.read(2) == '->' value = fd.read(dlen) assert fd.read(1) == '\n' return (initchar, key, value) def make(dbFilename, readFilename=None): """Makes a database from the filename, otherwise uses stdin.""" if readFilename is None: readfd = sys.stdin else: readfd = open(readFilename, 'rb') maker = Maker(dbFilename) while True: (initchar, key, value) = _readKeyValue(readfd) if initchar is None: break assert initchar == '+' maker.add(key, value) readfd.close() maker.finish() class Maker(object): """Class for making CDB databases.""" def __init__(self, filename): self.fd = utils.file.AtomicFile(filename, 'wb') self.filename = filename self.fd.seek(2048) self.hashPointers = [(0, 0)] * 256 #self.hashes = [[]] * 256 # Can't use this, [] stays the same... self.hashes = [] for _ in range(256): self.hashes.append([]) def add(self, key, data): """Adds a key->value pair to the database.""" h = hash(key) hashPointer = h % 256 startPosition = self.fd.tell() self.fd.write(pack2Ints(len(key), len(data))) self.fd.write(key.encode()) self.fd.write(data.encode()) self.hashes[hashPointer].append((h, startPosition)) def finish(self): """Finishes the current Maker object. Writes the remainder of the database to disk. """ for i in range(256): hash = self.hashes[i] self.hashPointers[i] = (self.fd.tell(), self._serializeHash(hash)) self._serializeHashPointers() self.fd.flush() self.fd.close() def _serializeHash(self, hash): hashLen = len(hash) * 2 a = [(0, 0)] * hashLen for (h, pos) in hash: i = (h // 256) % hashLen while a[i] != (0, 0): i = (i + 1) % hashLen a[i] = (h, pos) for (h, pos) in a: self.fd.write(pack2Ints(h, pos)) return hashLen def _serializeHashPointers(self): self.fd.seek(0) for (hashPos, hashLen) in self.hashPointers: self.fd.write(pack2Ints(hashPos, hashLen)) class Reader(utils.IterableMap): """Class for reading from a CDB database.""" def __init__(self, filename): self.filename = filename self.fd = open(filename, 'rb') self.loop = 0 self.khash = 0 self.kpos = 0 self.hpos = 0 self.hslots = 0 self.dpos = 0 self.dlen = 0 def close(self): self.fd.close() def _read(self, len, pos): self.fd.seek(pos) return self.fd.read(len) def _match(self, key, pos): return self._read(len(key), pos) == key def items(self): # uses loop/hslots in a strange, non-re-entrant manner. (self.loop,) = struct.unpack(' self.maxmods: self.flush() self.mods = 0 elif isinstance(self.maxmods, float): assert 0 <= self.maxmods if self.mods / max(len(self.cdb), 100) > self.maxmods: self.flush() self.mods = 0 def __getitem__(self, key): if key in self.removals: raise KeyError(key) else: try: return self.adds[key] except KeyError: return self.cdb[key] # If this raises KeyError, we lack key. def __delitem__(self, key): if key in self.removals: raise KeyError(key) else: if key in self.adds and key in self.cdb: self._journalRemoveKey(key) del self.adds[key] self.removals.add(key) elif key in self.adds: self._journalRemoveKey(key) del self.adds[key] elif key in self.cdb: self._journalRemoveKey(key) else: raise KeyError(key) self.mods += 1 self._flushIfOverLimit() def __setitem__(self, key, value): if key in self.removals: self.removals.remove(key) self._journalAddKey(key, value) self.adds[key] = value self.mods += 1 self._flushIfOverLimit() def __contains__(self, key): if key in self.removals: return False else: return key in self.adds or key in self.cdb has_key = __contains__ def items(self): already = set() for (key, value) in self.cdb.items(): if key in self.removals or key in already: continue elif key in self.adds: already.add(key) yield (key, self.adds[key]) else: yield (key, value) for (key, value) in self.adds.items(): if key not in already: yield (key, value) def setdefault(self, key, value): try: return self[key] except KeyError: self[key] = value return value def get(self, key, default=None): try: return self[key] except KeyError: return default class Shelf(ReaderWriter): """Uses pickle to mimic the shelf module.""" def __getitem__(self, key): return minisix.pickle.loads(ReaderWriter.__getitem__(self, key)) def __setitem__(self, key, value): ReaderWriter.__setitem__(self, key, minisix.pickle.dumps(value, True)) def items(self): for (key, value) in ReaderWriter.items(self): yield (key, minisix.pickle.loads(value)) if __name__ == '__main__': if sys.argv[0] == 'cdbdump': if len(sys.argv) == 2: fd = open(sys.argv[1], 'rb') else: fd = sys.stdin db = Reader(fd) dump(db) elif sys.argv[0] == 'cdbmake': if len(sys.argv) == 2: make(sys.argv[1]) else: make(sys.argv[1], sys.argv[2]) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/callbacks.py0000644000175000017500000017172713233426066016416 0ustar valval00000000000000# -*- coding: utf8 -*- ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2014, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ This module contains the basic callbacks for handling PRIVMSGs. """ import re import copy import time from . import shlex import codecs import getopt import inspect from . import (conf, ircdb, irclib, ircmsgs, ircutils, log, registry, utils, world) from .utils import minisix from .utils.iter import any, all from .i18n import PluginInternationalization _ = PluginInternationalization() def _addressed(nick, msg, prefixChars=None, nicks=None, prefixStrings=None, whenAddressedByNick=None, whenAddressedByNickAtEnd=None): def get(group): if ircutils.isChannel(target): group = group.get(target) return group() def stripPrefixStrings(payload): for prefixString in prefixStrings: if payload.startswith(prefixString): payload = payload[len(prefixString):].lstrip() return payload assert msg.command == 'PRIVMSG' (target, payload) = msg.args if not payload: return '' if prefixChars is None: prefixChars = get(conf.supybot.reply.whenAddressedBy.chars) if whenAddressedByNick is None: whenAddressedByNick = get(conf.supybot.reply.whenAddressedBy.nick) if whenAddressedByNickAtEnd is None: r = conf.supybot.reply.whenAddressedBy.nick.atEnd whenAddressedByNickAtEnd = get(r) if prefixStrings is None: prefixStrings = get(conf.supybot.reply.whenAddressedBy.strings) # We have to check this before nicks -- try "@google supybot" with supybot # and whenAddressedBy.nick.atEnd on to see why. if any(payload.startswith, prefixStrings): return stripPrefixStrings(payload) elif payload[0] in prefixChars: return payload[1:].strip() if nicks is None: nicks = get(conf.supybot.reply.whenAddressedBy.nicks) nicks = list(map(ircutils.toLower, nicks)) else: nicks = list(nicks) # Just in case. nicks.insert(0, ircutils.toLower(nick)) # Ok, let's see if it's a private message. if ircutils.nickEqual(target, nick): payload = stripPrefixStrings(payload) while payload and payload[0] in prefixChars: payload = payload[1:].lstrip() return payload # Ok, not private. Does it start with our nick? elif whenAddressedByNick: for nick in nicks: lowered = ircutils.toLower(payload) if lowered.startswith(nick): try: (maybeNick, rest) = payload.split(None, 1) toContinue = False while not ircutils.isNick(maybeNick, strictRfc=True): if maybeNick[-1].isalnum(): toContinue = True break maybeNick = maybeNick[:-1] if toContinue: continue if ircutils.nickEqual(maybeNick, nick): return rest else: continue except ValueError: # split didn't work. continue elif whenAddressedByNickAtEnd and lowered.endswith(nick): rest = payload[:-len(nick)] possiblePayload = rest.rstrip(' \t,;') if possiblePayload != rest: # There should be some separator between the nick and the # previous alphanumeric character. return possiblePayload if get(conf.supybot.reply.whenNotAddressed): return payload else: return '' def addressed(nick, msg, **kwargs): """If msg is addressed to 'name', returns the portion after the address. Otherwise returns the empty string. """ payload = msg.addressed if payload is not None: return payload else: payload = _addressed(nick, msg, **kwargs) msg.tag('addressed', payload) return payload def canonicalName(command, preserve_spaces=False): """Turn a command into its canonical form. Currently, this makes everything lowercase and removes all dashes and underscores. """ if minisix.PY2 and isinstance(command, unicode): command = command.encode('utf-8') elif minisix.PY3 and isinstance(command, bytes): command = command.decode() special = '\t-_' if not preserve_spaces: special += ' ' reAppend = '' while command and command[-1] in special: reAppend = command[-1] + reAppend command = command[:-1] return ''.join([x for x in command if x not in special]).lower() + reAppend def reply(msg, s, prefixNick=None, private=None, notice=None, to=None, action=None, error=False, stripCtcp=True): msg.tag('repliedTo') # Ok, let's make the target: # XXX This isn't entirely right. Consider to=#foo, private=True. target = ircutils.replyTo(msg) if ircutils.isChannel(to): target = to if ircutils.isChannel(target): channel = target else: channel = None if notice is None: notice = conf.get(conf.supybot.reply.withNotice, channel) if private is None: private = conf.get(conf.supybot.reply.inPrivate, channel) if prefixNick is None: prefixNick = conf.get(conf.supybot.reply.withNickPrefix, channel) if error: notice =conf.get(conf.supybot.reply.error.withNotice, channel) or notice private=conf.get(conf.supybot.reply.error.inPrivate, channel) or private s = _('Error: ') + s if private: prefixNick = False if to is None: target = msg.nick else: target = to if action: prefixNick = False if to is None: to = msg.nick if stripCtcp: s = s.strip('\x01') # Ok, now let's make the payload: s = ircutils.safeArgument(s) if not s and not action: s = _('Error: I tried to send you an empty message.') if prefixNick and ircutils.isChannel(target): # Let's may sure we don't do, "#channel: foo.". if not ircutils.isChannel(to): s = '%s: %s' % (to, s) if not ircutils.isChannel(target): if conf.supybot.reply.withNoticeWhenPrivate(): notice = True # And now, let's decide whether it's a PRIVMSG or a NOTICE. msgmaker = ircmsgs.privmsg if notice: msgmaker = ircmsgs.notice # We don't use elif here because actions can't be sent as NOTICEs. if action: msgmaker = ircmsgs.action # Finally, we'll return the actual message. ret = msgmaker(target, s) ret.tag('inReplyTo', msg) return ret def error(msg, s, **kwargs): """Makes an error reply to msg with the appropriate error payload.""" kwargs['error'] = True msg.tag('isError') return reply(msg, s, **kwargs) def getHelp(method, name=None, doc=None): if name is None: name = method.__name__ if doc is None: if method.__doc__ is None: doclines = ['This command has no help. Complain to the author.'] else: doclines = method.__doc__.splitlines() else: doclines = doc.splitlines() s = '%s %s' % (name, doclines.pop(0)) if doclines: help = ' '.join(doclines) s = '(%s) -- %s' % (ircutils.bold(s), help) return utils.str.normalizeWhitespace(s) def getSyntax(method, name=None, doc=None): if name is None: name = method.__name__ if doc is None: doclines = method.__doc__.splitlines() else: doclines = doc.splitlines() return '%s %s' % (name, doclines[0]) class Error(Exception): """Generic class for errors in Privmsg callbacks.""" pass class ArgumentError(Error): """The bot replies with a help message when this is raised.""" pass class SilentError(Error): """An error that we should not notify the user.""" pass class Tokenizer(object): # This will be used as a global environment to evaluate strings in. # Evaluation is, of course, necessary in order to allow escaped # characters to be properly handled. # # These are the characters valid in a token. Everything printable except # double-quote, left-bracket, and right-bracket. separators = '\x00\r\n \t' def __init__(self, brackets='', pipe=False, quotes='"'): if brackets: self.separators += brackets self.left = brackets[0] self.right = brackets[1] else: self.left = '' self.right = '' self.pipe = pipe if self.pipe: self.separators += '|' self.quotes = quotes self.separators += quotes def _handleToken(self, token): if token[0] == token[-1] and token[0] in self.quotes: token = token[1:-1] # FIXME: No need to tell you this is a hack. # It has to handle both IRC commands and serialized configuration. # # Whoever you are, if you make a single modification to this # code, TEST the code with Python 2 & 3, both with the unit # tests and on IRC with this: @echo "好" if minisix.PY2: try: token = token.encode('utf8').decode('string_escape') token = token.decode('utf8') except: token = token.decode('string_escape') else: token = codecs.getencoder('utf8')(token)[0] token = codecs.getdecoder('unicode_escape')(token)[0] try: token = token.encode('iso-8859-1').decode() except: # Prevent issue with tokens like '"\\x80"'. pass return token def _insideBrackets(self, lexer): ret = [] while True: token = lexer.get_token() if not token: raise SyntaxError(_('Missing "%s". You may want to ' 'quote your arguments with double ' 'quotes in order to prevent extra ' 'brackets from being evaluated ' 'as nested commands.') % self.right) elif token == self.right: return ret elif token == self.left: ret.append(self._insideBrackets(lexer)) else: ret.append(self._handleToken(token)) return ret def tokenize(self, s): lexer = shlex.shlex(minisix.io.StringIO(s)) lexer.commenters = '' lexer.quotes = self.quotes lexer.separators = self.separators args = [] ends = [] while True: token = lexer.get_token() if not token: break elif token == '|' and self.pipe: # The "and self.pipe" might seem redundant here, but it's there # for strings like 'foo | bar', where a pipe stands alone as a # token, but shouldn't be treated specially. if not args: raise SyntaxError(_('"|" with nothing preceding. I ' 'obviously can\'t do a pipe with ' 'nothing before the |.')) ends.append(args) args = [] elif token == self.left: args.append(self._insideBrackets(lexer)) elif token == self.right: raise SyntaxError(_('Spurious "%s". You may want to ' 'quote your arguments with double ' 'quotes in order to prevent extra ' 'brackets from being evaluated ' 'as nested commands.') % self.right) else: args.append(self._handleToken(token)) if ends: if not args: raise SyntaxError(_('"|" with nothing following. I ' 'obviously can\'t do a pipe with ' 'nothing after the |.')) args.append(ends.pop()) while ends: args[-1].append(ends.pop()) return args def tokenize(s, channel=None): """A utility function to create a Tokenizer and tokenize a string.""" pipe = False brackets = '' nested = conf.supybot.commands.nested if nested(): brackets = conf.get(nested.brackets, channel) if conf.get(nested.pipeSyntax, channel): # No nesting, no pipe. pipe = True quotes = conf.get(conf.supybot.commands.quotes, channel) try: ret = Tokenizer(brackets=brackets,pipe=pipe,quotes=quotes).tokenize(s) return ret except ValueError as e: raise SyntaxError(str(e)) def formatCommand(command): return ' '.join(command) def checkCommandCapability(msg, cb, commandName): if not isinstance(commandName, minisix.string_types): commandName = '.'.join(commandName) plugin = cb.name().lower() pluginCommand = '%s.%s' % (plugin, commandName) def checkCapability(capability): assert ircdb.isAntiCapability(capability) if ircdb.checkCapability(msg.prefix, capability): log.info('Preventing %s from calling %s because of %s.', msg.prefix, pluginCommand, capability) raise RuntimeError(capability) try: antiPlugin = ircdb.makeAntiCapability(plugin) antiCommand = ircdb.makeAntiCapability(commandName) antiPluginCommand = ircdb.makeAntiCapability(pluginCommand) checkCapability(antiPlugin) checkCapability(antiCommand) checkCapability(antiPluginCommand) checkAtEnd = [commandName, pluginCommand] default = conf.supybot.capabilities.default() if ircutils.isChannel(msg.args[0]): channel = msg.args[0] checkCapability(ircdb.makeChannelCapability(channel, antiCommand)) checkCapability(ircdb.makeChannelCapability(channel, antiPlugin)) checkCapability(ircdb.makeChannelCapability(channel, antiPluginCommand)) chanPlugin = ircdb.makeChannelCapability(channel, plugin) chanCommand = ircdb.makeChannelCapability(channel, commandName) chanPluginCommand = ircdb.makeChannelCapability(channel, pluginCommand) checkAtEnd += [chanCommand, chanPlugin, chanPluginCommand] default &= ircdb.channels.getChannel(channel).defaultAllow return not (default or \ any(lambda x: ircdb.checkCapability(msg.prefix, x), checkAtEnd)) except RuntimeError as e: s = ircdb.unAntiCapability(str(e)) return s class RichReplyMethods(object): """This is a mixin so these replies need only be defined once. It operates under several assumptions, including the fact that 'self' is an Irc object of some sort and there is a self.msg that is an IrcMsg.""" def __makeReply(self, prefix, s): if s: s = '%s %s' % (prefix, s) else: s = prefix return ircutils.standardSubstitute(self, self.msg, s) def _getConfig(self, wrapper): return conf.get(wrapper, self.msg.args[0]) def replySuccess(self, s='', **kwargs): v = self._getConfig(conf.supybot.replies.success) if v: s = self.__makeReply(v, s) return self.reply(s, **kwargs) else: self.noReply() def replyError(self, s='', **kwargs): v = self._getConfig(conf.supybot.replies.error) if 'msg' in kwargs: msg = kwargs['msg'] if ircdb.checkCapability(msg.prefix, 'owner'): v = self._getConfig(conf.supybot.replies.errorOwner) s = self.__makeReply(v, s) return self.reply(s, **kwargs) def _getTarget(self, to=None): """Compute the target according to self.to, the provided to, and self.private, and return it. Mainly used by reply methods.""" # FIXME: Don't set self.to. # I still set it to be sure I don't introduce a regression, # but it does not make sense for .reply() and .replies() to # change the state of this Irc object. if to is not None: self.to = self.to or to target = self.private and self.to or self.msg.args[0] return target def replies(self, L, prefixer=None, joiner=None, onlyPrefixFirst=False, oneToOne=None, **kwargs): if prefixer is None: prefixer = '' if joiner is None: joiner = utils.str.commaAndify if isinstance(prefixer, minisix.string_types): prefixer = prefixer.__add__ if isinstance(joiner, minisix.string_types): joiner = joiner.join to = self._getTarget(kwargs.get('to')) if oneToOne is None: # Can be True, False, or None if ircutils.isChannel(to): oneToOne = conf.get(conf.supybot.reply.oneToOne, to) else: oneToOne = conf.supybot.reply.oneToOne() if oneToOne: return self.reply(prefixer(joiner(L)), **kwargs) else: msg = None first = True for s in L: if onlyPrefixFirst: if first: first = False msg = self.reply(prefixer(s), **kwargs) else: msg = self.reply(s, **kwargs) else: msg = self.reply(prefixer(s), **kwargs) return msg def noReply(self, msg=None): self.repliedTo = True def _error(self, s, Raise=False, **kwargs): if Raise: raise Error(s) else: return self.error(s, **kwargs) def errorNoCapability(self, capability, s='', **kwargs): if 'Raise' not in kwargs: kwargs['Raise'] = True log.warning('Denying %s for lacking %q capability.', self.msg.prefix, capability) # noCapability means "don't send a specific capability error # message" not "don't send a capability error message at all", like # one would think if self._getConfig(conf.supybot.reply.error.noCapability) or \ capability in conf.supybot.capabilities.private(): v = self._getConfig(conf.supybot.replies.genericNoCapability) else: v = self._getConfig(conf.supybot.replies.noCapability) try: v %= capability except TypeError: # No %s in string pass s = self.__makeReply(v, s) if s: return self._error(s, **kwargs) def errorPossibleBug(self, s='', **kwargs): v = self._getConfig(conf.supybot.replies.possibleBug) if s: s += ' (%s)' % v else: s = v return self._error(s, **kwargs) def errorNotRegistered(self, s='', **kwargs): v = self._getConfig(conf.supybot.replies.notRegistered) return self._error(self.__makeReply(v, s), **kwargs) def errorNoUser(self, s='', name='that user', **kwargs): if 'Raise' not in kwargs: kwargs['Raise'] = True v = self._getConfig(conf.supybot.replies.noUser) try: v = v % name except TypeError: log.warning('supybot.replies.noUser should have one "%s" in it.') return self._error(self.__makeReply(v, s), **kwargs) def errorRequiresPrivacy(self, s='', **kwargs): v = self._getConfig(conf.supybot.replies.requiresPrivacy) return self._error(self.__makeReply(v, s), **kwargs) def errorInvalid(self, what, given=None, s='', repr=True, **kwargs): if given is not None: if repr: given = _repr(given) else: given = '"%s"' % given v = _('%s is not a valid %s.') % (given, what) else: v = _('That\'s not a valid %s.') % what if 'Raise' not in kwargs: kwargs['Raise'] = True if s: v += ' ' + s return self._error(v, **kwargs) _repr = repr class ReplyIrcProxy(RichReplyMethods): """This class is a thin wrapper around an irclib.Irc object that gives it the reply() and error() methods (as well as everything in RichReplyMethods, based on those two).""" def __init__(self, irc, msg): self.irc = irc self.msg = msg def getRealIrc(self): """Returns the real irclib.Irc object underlying this proxy chain.""" if isinstance(self.irc, irclib.Irc): return self.irc else: return self.irc.getRealIrc() # This should make us be considered equal to our irclib.Irc object for # hashing; an important thing (no more "too many open files" exceptions :)) def __hash__(self): return hash(self.getRealIrc()) def __eq__(self, other): return self.getRealIrc() == other __req__ = __eq__ def __ne__(self, other): return not (self == other) __rne__ = __ne__ def error(self, s, msg=None, **kwargs): if 'Raise' in kwargs and kwargs['Raise']: if s: raise Error(s) else: raise ArgumentError if msg is None: msg = self.msg m = error(msg, s, **kwargs) self.irc.queueMsg(m) return m def reply(self, s, msg=None, **kwargs): if msg is None: msg = self.msg assert not isinstance(s, ircmsgs.IrcMsg), \ 'Old code alert: there is no longer a "msg" argument to reply.' kwargs.pop('noLengthCheck', None) m = reply(msg, s, **kwargs) self.irc.queueMsg(m) return m def __getattr__(self, attr): return getattr(self.irc, attr) SimpleProxy = ReplyIrcProxy # Backwards-compatibility class NestedCommandsIrcProxy(ReplyIrcProxy): "A proxy object to allow proper nesting of commands (even threaded ones)." _mores = ircutils.IrcDict() def __init__(self, irc, msg, args, nested=0): assert isinstance(args, list), 'Args should be a list, not a string.' self.irc = irc self.msg = msg self.nested = nested self.repliedTo = False if not self.nested and isinstance(irc, self.__class__): # This means we were given an NestedCommandsIrcProxy instead of an # irclib.Irc, and so we're obviously nested. But nested wasn't # set! So we take our given Irc's nested value. self.nested += irc.nested maxNesting = conf.supybot.commands.nested.maximum() if maxNesting and self.nested > maxNesting: log.warning('%s attempted more than %s levels of nesting.', self.msg.prefix, maxNesting) self.error(_('You\'ve attempted more nesting than is ' 'currently allowed on this bot.')) return # The deepcopy here is necessary for Scheduler; it re-runs already # tokenized commands. There's a possibility a simple copy[:] would # work, but we're being careful. self.args = copy.deepcopy(args) self.counter = 0 self._resetReplyAttributes() if not args: self.finalEvaled = True self._callInvalidCommands() else: self.finalEvaled = False world.commandsProcessed += 1 self.evalArgs() def __eq__(self, other): return other == self.getRealIrc() def __hash__(self): return hash(self.getRealIrc()) def _resetReplyAttributes(self): self.to = None self.action = None self.notice = None self.private = None self.noLengthCheck = None if ircutils.isChannel(self.msg.args[0]): self.prefixNick = conf.get(conf.supybot.reply.withNickPrefix, self.msg.args[0]) else: self.prefixNick = conf.supybot.reply.withNickPrefix() def evalArgs(self, withClass=None): while self.counter < len(self.args): self.repliedTo = False if isinstance(self.args[self.counter], minisix.string_types): # If it's a string, just go to the next arg. There is no # evaluation to be done for strings. If, at some point, # we decided to, say, convert every string using # ircutils.standardSubstitute, this would be where we would # probably put it. self.counter += 1 else: assert isinstance(self.args[self.counter], list) # It's a list. So we spawn another NestedCommandsIrcProxy # to evaluate its args. When that class has finished # evaluating its args, it will call our reply method, which # will subsequently call this function again, and we'll # pick up where we left off via self.counter. cls = withClass or self.__class__ cls(self, self.msg, self.args[self.counter], nested=self.nested+1) # We have to return here because the new NestedCommandsIrcProxy # might not have called our reply method instantly, since # its command might be threaded. So (obviously) we can't # just fall through to self.finalEval. return # Once all the list args are evaluated, we then evaluate our own # list of args, since we're assured that they're all strings now. assert all(lambda x: isinstance(x, minisix.string_types), self.args) self.finalEval() def _callInvalidCommands(self): log.debug('Calling invalidCommands.') threaded = False cbs = [] for cb in self.irc.callbacks: if hasattr(cb, 'invalidCommand'): cbs.append(cb) threaded = threaded or cb.threaded def callInvalidCommands(): self.repliedTo = False for cb in cbs: log.debug('Calling %s.invalidCommand.', cb.name()) try: cb.invalidCommand(self, self.msg, self.args) except Error as e: self.error(str(e)) except Exception as e: log.exception('Uncaught exception in %s.invalidCommand.', cb.name()) log.debug('Finished calling %s.invalidCommand.', cb.name()) if self.repliedTo: log.debug('Done calling invalidCommands: %s.',cb.name()) return if threaded: name = 'Thread #%s (for invalidCommands)' % world.threadsSpawned t = world.SupyThread(target=callInvalidCommands, name=name) t.setDaemon(True) t.start() else: callInvalidCommands() def findCallbacksForArgs(self, args): """Returns a two-tuple of (command, plugins) that has the command (a list of strings) and the plugins for which it was a command.""" assert isinstance(args, list) args = list(map(canonicalName, args)) cbs = [] maxL = [] for cb in self.irc.callbacks: if not hasattr(cb, 'getCommand'): continue L = cb.getCommand(args) #log.debug('%s.getCommand(%r) returned %r', cb.name(), args, L) if L and L >= maxL: maxL = L cbs.append((cb, L)) assert isinstance(L, list), \ 'getCommand now returns a list, not a method.' assert utils.iter.startswith(L, args), \ 'getCommand must return a prefix of the args given. ' \ '(args given: %r, returned: %r)' % (args, L) log.debug('findCallbacksForArgs: %r', cbs) cbs = [cb for (cb, L) in cbs if L == maxL] if len(maxL) == 1: # Special case: one arg determines the callback. In this case, we # have to check, in order: # 1. Whether the arg is the same as the name of a callback. This # callback would then win. for cb in cbs: if cb.canonicalName() == maxL[0]: return (maxL, [cb]) # 2. Whether a defaultplugin is defined. defaultPlugins = conf.supybot.commands.defaultPlugins try: defaultPlugin = defaultPlugins.get(maxL[0])() log.debug('defaultPlugin: %r', defaultPlugin) if defaultPlugin: cb = self.irc.getCallback(defaultPlugin) if cb in cbs: # This is just a sanity check, but there's a small # possibility that a default plugin for a command # is configured to point to a plugin that doesn't # actually have that command. return (maxL, [cb]) except registry.NonExistentRegistryEntry: pass # 3. Whether an importantPlugin is one of the responses. important = defaultPlugins.importantPlugins() important = list(map(canonicalName, important)) importants = [] for cb in cbs: if cb.canonicalName() in important: importants.append(cb) if len(importants) == 1: return (maxL, importants) return (maxL, cbs) def finalEval(self): # Now that we've already iterated through our args and made sure # that any list of args was evaluated (by spawning another # NestedCommandsIrcProxy to evaluated it into a string), we can finally # evaluated our own list of arguments. assert not self.finalEvaled, 'finalEval called twice.' self.finalEvaled = True # Now, the way we call a command is we iterate over the loaded pluings, # asking each one if the list of args we have interests it. The # way we do that is by calling getCommand on the plugin. # The plugin will return a list of args which it considers to be # "interesting." We will then give our args to the plugin which # has the *longest* list. The reason we pick the longest list is # that it seems reasonable that the longest the list, the more # specific the command is. That is, given a list of length X, a list # of length X+1 would be even more specific (assuming that both lists # used the same prefix. Of course, if two plugins return a list of the # same length, we'll just error out with a message about ambiguity. (command, cbs) = self.findCallbacksForArgs(self.args) if not cbs: # We used to handle addressedRegexps here, but I think we'll let # them handle themselves in getCommand. They can always just # return the full list of args as their "command". self._callInvalidCommands() elif len(cbs) > 1: names = sorted([cb.name() for cb in cbs]) command = formatCommand(command) self.error(format(_('The command %q is available in the %L ' 'plugins. Please specify the plugin ' 'whose command you wish to call by using ' 'its name as a command before %q.'), command, names, command)) else: cb = cbs[0] args = self.args[len(command):] if world.isMainThread() and \ (cb.threaded or conf.supybot.debug.threadAllCommands()): t = CommandThread(target=cb._callCommand, args=(command, self, self.msg, args)) t.start() else: cb._callCommand(command, self, self.msg, args) def reply(self, s, noLengthCheck=False, prefixNick=None, action=None, private=None, notice=None, to=None, msg=None, sendImmediately=False, stripCtcp=True): """ Keyword arguments: * `noLengthCheck=False`: True if the length shouldn't be checked (used for 'more' handling) * `prefixNick=True`: False if the nick shouldn't be prefixed to the reply. * `action=False`: True if the reply should be an action. * `private=False`: True if the reply should be in private. * `notice=False`: True if the reply should be noticed when the bot is configured to do so. * `to=`: The nick or channel the reply should go to. Defaults to msg.args[0] (or msg.nick if private) * `sendImmediately=False`: True if the reply should use sendMsg() which bypasses conf.supybot.protocols.irc.throttleTime and gets sent before any queued messages """ # These use and or or based on whether or not they default to True or # False. Those that default to True use and; those that default to # False use or. assert not isinstance(s, ircmsgs.IrcMsg), \ 'Old code alert: there is no longer a "msg" argument to reply.' self.repliedTo = True if sendImmediately: sendMsg = self.irc.sendMsg else: sendMsg = self.irc.queueMsg if msg is None: msg = self.msg if prefixNick is not None: self.prefixNick = prefixNick if action is not None: self.action = self.action or action if action: self.prefixNick = False if notice is not None: self.notice = self.notice or notice if private is not None: self.private = self.private or private target = self._getTarget(to) # action=True implies noLengthCheck=True and prefixNick=False self.noLengthCheck=noLengthCheck or self.noLengthCheck or self.action if not isinstance(s, minisix.string_types): # avoid trying to str() unicode s = str(s) # Allow non-string esses. if self.finalEvaled: try: if isinstance(self.irc, self.__class__): s = s[:conf.supybot.reply.maximumLength()] return self.irc.reply(s, to=self.to, notice=self.notice, action=self.action, private=self.private, prefixNick=self.prefixNick, noLengthCheck=self.noLengthCheck, stripCtcp=stripCtcp) elif self.noLengthCheck: # noLengthCheck only matters to NestedCommandsIrcProxy, so # it's not used here. Just in case you were wondering. m = reply(msg, s, to=self.to, notice=self.notice, action=self.action, private=self.private, prefixNick=self.prefixNick, stripCtcp=stripCtcp) sendMsg(m) return m else: s = ircutils.safeArgument(s) allowedLength = conf.get(conf.supybot.reply.mores.length, target) if not allowedLength: # 0 indicates this. allowedLength = 470 - len(self.irc.prefix) allowedLength -= len(msg.nick) # The '(XX more messages)' may have not the same # length in the current locale allowedLength -= len(_('(XX more messages)')) maximumMores = conf.get(conf.supybot.reply.mores.maximum, target) maximumLength = allowedLength * maximumMores if len(s) > maximumLength: log.warning('Truncating to %s bytes from %s bytes.', maximumLength, len(s)) s = s[:maximumLength] s_too_long = len(s.encode()) < allowedLength \ if minisix.PY3 else len(s) < allowedLength if s_too_long or \ not conf.get(conf.supybot.reply.mores, target): # In case we're truncating, we add 20 to allowedLength, # because our allowedLength is shortened for the # "(XX more messages)" trailer. if minisix.PY3: appended = _('(XX more messages)').encode() s = s.encode()[:allowedLength-len(appended)] s = s.decode('utf8', 'ignore') else: appended = _('(XX more messages)') s = s[:allowedLength-len(appended)] # There's no need for action=self.action here because # action implies noLengthCheck, which has already been # handled. Let's stick an assert in here just in case. assert not self.action m = reply(msg, s, to=self.to, notice=self.notice, private=self.private, prefixNick=self.prefixNick, stripCtcp=stripCtcp) sendMsg(m) return m msgs = ircutils.wrap(s, allowedLength, break_long_words=True) msgs.reverse() instant = conf.get(conf.supybot.reply.mores.instant,target) while instant > 1 and msgs: instant -= 1 response = msgs.pop() m = reply(msg, response, to=self.to, notice=self.notice, private=self.private, prefixNick=self.prefixNick, stripCtcp=stripCtcp) sendMsg(m) # XXX We should somehow allow these to be returned, but # until someone complains, we'll be fine :) We # can't return from here, though, for obvious # reasons. # return m if not msgs: return response = msgs.pop() if msgs: if len(msgs) == 1: more = _('more message') else: more = _('more messages') n = ircutils.bold('(%i %s)' % (len(msgs), more)) response = '%s %s' % (response, n) prefix = msg.prefix if self.to and ircutils.isNick(self.to): try: state = self.getRealIrc().state prefix = state.nickToHostmask(self.to) except KeyError: pass # We'll leave it as it is. mask = prefix.split('!', 1)[1] self._mores[mask] = msgs public = ircutils.isChannel(msg.args[0]) private = self.private or not public self._mores[msg.nick] = (private, msgs) m = reply(msg, response, to=self.to, action=self.action, notice=self.notice, private=self.private, prefixNick=self.prefixNick, stripCtcp=stripCtcp) sendMsg(m) return m finally: self._resetReplyAttributes() else: if msg.ignored: # Since the final reply string is constructed via # ' '.join(self.args), the args index for ignored commands # needs to be popped to avoid extra spaces in the final reply. self.args.pop(self.counter) msg.tag('ignored', False) else: self.args[self.counter] = s self.evalArgs() def noReply(self, msg=None): if msg is None: msg = self.msg super(NestedCommandsIrcProxy, self).noReply(msg=msg) if self.finalEvaled: if isinstance(self.irc, NestedCommandsIrcProxy): self.irc.noReply(msg=msg) else: msg.tag('ignored', True) else: self.args.pop(self.counter) msg.tag('ignored', False) self.evalArgs() def replies(self, L, prefixer=None, joiner=None, onlyPrefixFirst=False, to=None, oneToOne=None, **kwargs): if not self.finalEvaled and oneToOne is None: oneToOne = True return super(NestedCommandsIrcProxy, self).replies(L, prefixer=prefixer, joiner=joiner, onlyPrefixFirst=onlyPrefixFirst, to=to, oneToOne=oneToOne, **kwargs) def error(self, s='', Raise=False, **kwargs): self.repliedTo = True if Raise: if s: raise Error(s) else: raise ArgumentError if s: if not isinstance(self.irc, irclib.Irc): return self.irc.error(s, **kwargs) else: m = error(self.msg, s, **kwargs) self.irc.queueMsg(m) return m else: raise ArgumentError def __getattr__(self, attr): return getattr(self.irc, attr) IrcObjectProxy = NestedCommandsIrcProxy class CommandThread(world.SupyThread): """Just does some extra logging and error-recovery for commands that need to run in threads. """ def __init__(self, target=None, args=(), kwargs={}): self.command = args[0] self.cb = target.__self__ threadName = 'Thread #%s (for %s.%s)' % (world.threadsSpawned, self.cb.name(), self.command) log.debug('Spawning thread %s (args: %r)', threadName, args) self.__parent = super(CommandThread, self) self.__parent.__init__(target=target, name=threadName, args=args, kwargs=kwargs) self.setDaemon(True) self.originalThreaded = self.cb.threaded self.cb.threaded = True def run(self): try: self.__parent.run() finally: self.cb.threaded = self.originalThreaded class CommandProcess(world.SupyProcess): """Just does some extra logging and error-recovery for commands that need to run in processes. """ def __init__(self, target=None, args=(), kwargs={}): pn = kwargs.pop('pn', 'Unknown') cn = kwargs.pop('cn', 'unknown') procName = 'Process #%s (for %s.%s)' % (world.processesSpawned, pn, cn) log.debug('Spawning process %s (args: %r)', procName, args) self.__parent = super(CommandProcess, self) self.__parent.__init__(target=target, name=procName, args=args, kwargs=kwargs) def run(self): self.__parent.run() class CanonicalString(registry.NormalizedString): def normalize(self, s): return canonicalName(s) class CanonicalNameSet(utils.NormalizingSet): def normalize(self, s): return canonicalName(s) class CanonicalNameDict(utils.InsensitivePreservingDict): def key(self, s): return canonicalName(s) class Disabled(registry.SpaceSeparatedListOf): sorted = True Value = CanonicalString List = CanonicalNameSet conf.registerGlobalValue(conf.supybot.commands, 'disabled', Disabled([], _("""Determines what commands are currently disabled. Such commands will not appear in command lists, etc. They will appear not even to exist."""))) class DisabledCommands(object): def __init__(self): self.d = CanonicalNameDict() for name in conf.supybot.commands.disabled(): if '.' in name: (plugin, command) = name.split('.', 1) if command in self.d: if self.d[command] is not None: self.d[command].add(plugin) else: self.d[command] = CanonicalNameSet([plugin]) else: self.d[name] = None def disabled(self, command, plugin=None): if command in self.d: if self.d[command] is None: return True elif plugin in self.d[command]: return True return False def add(self, command, plugin=None): if plugin is None: self.d[command] = None else: if command in self.d: if self.d[command] is not None: self.d[command].add(plugin) else: self.d[command] = CanonicalNameSet([plugin]) def remove(self, command, plugin=None): if plugin is None: del self.d[command] else: if self.d[command] is not None: self.d[command].remove(plugin) class BasePlugin(object): def __init__(self, *args, **kwargs): self.cbs = [] for attr in dir(self): if attr != canonicalName(attr): continue obj = getattr(self, attr) if isinstance(obj, type) and issubclass(obj, BasePlugin): cb = obj(*args, **kwargs) setattr(self, attr, cb) self.cbs.append(cb) cb.log = log.getPluginLogger('%s.%s' % (self.name(),cb.name())) super(BasePlugin, self).__init__() class MetaSynchronizedAndFirewalled(log.MetaFirewall, utils.python.MetaSynchronized): pass SynchronizedAndFirewalled = MetaSynchronizedAndFirewalled( 'SynchronizedAndFirewalled', (), {}) class Commands(BasePlugin, SynchronizedAndFirewalled): __synchronized__ = ( '__call__', 'callCommand', 'invalidCommand', ) # For a while, a comment stood here to say, "Eventually callCommand." But # that's wrong, because we can't do generic error handling in this # callCommand -- plugins need to be able to override callCommand and do # error handling there (see the Web plugin for an example). __firewalled__ = {'isCommand': None, '_callCommand': None} commandArgs = ['self', 'irc', 'msg', 'args'] # These must be class-scope, so all plugins use the same one. _disabled = DisabledCommands() pre_command_callbacks = [] def name(self): return self.__class__.__name__ def canonicalName(self): return canonicalName(self.name()) def isDisabled(self, command): return self._disabled.disabled(command, self.name()) def isCommandMethod(self, name): """Returns whether a given method name is a command in this plugin.""" # This function is ugly, but I don't want users to call methods like # doPrivmsg or __init__ or whatever, and this is good to stop them. # Don't normalize this name: consider outFilter(self, irc, msg). # name = canonicalName(name) if self.isDisabled(name): return False if name != canonicalName(name): return False if hasattr(self, name): method = getattr(self, name) if inspect.ismethod(method): code = method.__func__.__code__ return inspect.getargs(code)[0] == self.commandArgs else: return False else: return False def isCommand(self, command): """Convenience, backwards-compatibility, semi-deprecated.""" if isinstance(command, minisix.string_types): return self.isCommandMethod(command) else: # Since we're doing a little type dispatching here, let's not be # too liberal. assert isinstance(command, list) return self.getCommand(command) == command def getCommand(self, args, stripOwnName=True): assert args == list(map(canonicalName, args)) first = args[0] for cb in self.cbs: if first == cb.canonicalName(): return cb.getCommand(args) if first == self.canonicalName() and len(args) > 1 and \ stripOwnName: ret = self.getCommand(args[1:], stripOwnName=False) if ret: return [first] + ret if self.isCommandMethod(first): return [first] return [] def getCommandMethod(self, command): """Gets the given command from this plugin.""" #print '*** %s.getCommandMethod(%r)' % (self.name(), command) assert not isinstance(command, minisix.string_types) assert command == list(map(canonicalName, command)) assert self.getCommand(command) == command for cb in self.cbs: if command[0] == cb.canonicalName(): return cb.getCommandMethod(command) if len(command) > 1: assert command[0] == self.canonicalName() return self.getCommandMethod(command[1:]) else: method = getattr(self, command[0]) if inspect.ismethod(method): code = method.__func__.__code__ if inspect.getargs(code)[0] == self.commandArgs: return method else: raise AttributeError def listCommands(self, pluginCommands=[]): commands = set(pluginCommands) for s in dir(self): if self.isCommandMethod(s): commands.add(s) for cb in self.cbs: name = cb.canonicalName() for command in cb.listCommands(): if command == name: commands.add(command) else: commands.add(' '.join([name, command])) L = list(commands) L.sort() return L def callCommand(self, command, irc, msg, *args, **kwargs): # We run all callbacks before checking if one of them returned True if any(bool, list(cb(self, command, irc, msg, *args, **kwargs) for cb in self.pre_command_callbacks)): return method = self.getCommandMethod(command) method(irc, msg, *args, **kwargs) def _callCommand(self, command, irc, msg, *args, **kwargs): if irc.nick == msg.args[0]: self.log.info('%s called in private by %q.', formatCommand(command), msg.prefix) else: self.log.info('%s called on %s by %q.', formatCommand(command), msg.args[0], msg.prefix) # XXX I'm being extra-special-careful here, but we need to refactor # this. try: cap = checkCommandCapability(msg, self, command) if cap: irc.errorNoCapability(cap) return for name in command: cap = checkCommandCapability(msg, self, name) if cap: irc.errorNoCapability(cap) return try: self.callingCommand = command self.callCommand(command, irc, msg, *args, **kwargs) finally: self.callingCommand = None except SilentError: pass except (getopt.GetoptError, ArgumentError) as e: self.log.debug('Got %s, giving argument error.', utils.exnToString(e)) help = self.getCommandHelp(command) if 'command has no help.' in help: # Note: this case will never happen, unless 'checkDoc' is set # to False. irc.error(_('Invalid arguments for %s.') % formatCommand(command)) else: irc.reply(help) except (SyntaxError, Error) as e: self.log.debug('Error return: %s', utils.exnToString(e)) irc.error(str(e)) except Exception as e: self.log.exception('Uncaught exception in %s.', command) if conf.supybot.reply.error.detailed(): irc.error(utils.exnToString(e)) else: irc.replyError(msg=msg) def getCommandHelp(self, command, simpleSyntax=None): method = self.getCommandMethod(command) help = getHelp chan = None if dynamic.msg is not None: chan = dynamic.msg.args[0] if simpleSyntax is None: simpleSyntax = conf.get(conf.supybot.reply.showSimpleSyntax, chan) if simpleSyntax: help = getSyntax if hasattr(method, '__doc__'): return help(method, name=formatCommand(command)) else: return format(_('The %q command has no help.'), formatCommand(command)) class PluginMixin(BasePlugin, irclib.IrcCallback): public = True alwaysCall = () threaded = False noIgnore = False classModule = None Proxy = NestedCommandsIrcProxy def __init__(self, irc): myName = self.name() self.log = log.getPluginLogger(myName) self.__parent = super(PluginMixin, self) self.__parent.__init__(irc) # We can't do this because of the specialness that Owner and Misc do. # I guess plugin authors will have to get the capitalization right. # self.callAfter = map(str.lower, self.callAfter) # self.callBefore = map(str.lower, self.callBefore) def canonicalName(self): return canonicalName(self.name()) def __call__(self, irc, msg): irc = SimpleProxy(irc, msg) if msg.command == 'PRIVMSG': if hasattr(self.noIgnore, '__call__'): noIgnore = self.noIgnore(irc, msg) else: noIgnore = self.noIgnore if noIgnore or \ not ircdb.checkIgnored(msg.prefix, msg.args[0]) or \ not ircutils.isUserHostmask(msg.prefix): # Some services impl. self.__parent.__call__(irc, msg) else: self.__parent.__call__(irc, msg) def registryValue(self, name, channel=None, value=True): plugin = self.name() group = conf.supybot.plugins.get(plugin) names = registry.split(name) for name in names: group = group.get(name) if channel is not None: if ircutils.isChannel(channel): group = group.get(channel) else: self.log.debug('%s: registryValue got channel=%r', plugin, channel) if value: return group() else: return group def setRegistryValue(self, name, value, channel=None): plugin = self.name() group = conf.supybot.plugins.get(plugin) names = registry.split(name) for name in names: group = group.get(name) if channel is None: group.setValue(value) else: group.get(channel).setValue(value) def userValue(self, name, prefixOrName, default=None): try: id = str(ircdb.users.getUserId(prefixOrName)) except KeyError: return None plugin = self.name() group = conf.users.plugins.get(plugin) names = registry.split(name) for name in names: group = group.get(name) return group.get(id)() def setUserValue(self, name, prefixOrName, value, ignoreNoUser=True, setValue=True): try: id = str(ircdb.users.getUserId(prefixOrName)) except KeyError: if ignoreNoUser: return else: raise plugin = self.name() group = conf.users.plugins.get(plugin) names = registry.split(name) for name in names: group = group.get(name) group = group.get(id) if setValue: group.setValue(value) else: group.set(value) def getPluginHelp(self): if hasattr(self, '__doc__'): return self.__doc__ else: return None class Plugin(PluginMixin, Commands): pass Privmsg = Plugin # Backwards compatibility. class PluginRegexp(Plugin): """Same as Plugin, except allows the user to also include regexp-based callbacks. All regexp-based callbacks must be specified in the set (or list) attribute "regexps", "addressedRegexps", or "unaddressedRegexps" depending on whether they should always be triggered, triggered only when the bot is addressed, or triggered only when the bot isn't addressed. """ flags = re.I regexps = () """'regexps' methods are called whether the message is addressed or not.""" addressedRegexps = () """'addressedRegexps' methods are called only when the message is addressed, and then, only with the payload (i.e., what is returned from the 'addressed' function.""" unaddressedRegexps = () """'unaddressedRegexps' methods are called only when the message is *not* addressed.""" Proxy = SimpleProxy def __init__(self, irc): self.__parent = super(PluginRegexp, self) self.__parent.__init__(irc) self.res = [] self.addressedRes = [] self.unaddressedRes = [] for name in self.regexps: method = getattr(self, name) r = re.compile(method.__doc__, self.flags) self.res.append((r, name)) for name in self.addressedRegexps: method = getattr(self, name) r = re.compile(method.__doc__, self.flags) self.addressedRes.append((r, name)) for name in self.unaddressedRegexps: method = getattr(self, name) r = re.compile(method.__doc__, self.flags) self.unaddressedRes.append((r, name)) def _callRegexp(self, name, irc, msg, m): method = getattr(self, name) try: method(irc, msg, m) except Error as e: irc.error(str(e)) except Exception as e: self.log.exception('Uncaught exception in _callRegexp:') def invalidCommand(self, irc, msg, tokens): s = ' '.join(tokens) for (r, name) in self.addressedRes: for m in r.finditer(s): self._callRegexp(name, irc, msg, m) def doPrivmsg(self, irc, msg): if msg.isError: return proxy = self.Proxy(irc, msg) if not msg.addressed: for (r, name) in self.unaddressedRes: for m in r.finditer(msg.args[1]): self._callRegexp(name, proxy, msg, m) for (r, name) in self.res: for m in r.finditer(msg.args[1]): self._callRegexp(name, proxy, msg, m) PrivmsgCommandAndRegexp = PluginRegexp # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/ansi.py0000644000175000017500000000666013233426066015422 0ustar valval00000000000000### # 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. ### """ ansi.py ANSI Terminal Interface Color Usage: print RED + 'this is red' + RESET print BOLD + GREEN + WHITEBG + 'this is bold green on white' + RESET def move(new_x, new_y): 'Move cursor to new_x, new_y' def moveUp(lines): 'Move cursor up # of lines' def moveDown(lines): 'Move cursor down # of lines' def moveForward(chars): 'Move cursor forward # of chars' def moveBack(chars): 'Move cursor backward # of chars' def save(): 'Saves cursor position' def restore(): 'Restores cursor position' def clear(): 'Clears screen and homes cursor' def clrtoeol(): 'Clears screen to end of line' """ ################################ # C O L O R C O N S T A N T S # ################################ BLACK = '\033[30m' RED = '\033[31m' GREEN = '\033[32m' YELLOW = '\033[33m' BLUE = '\033[34m' MAGENTA = '\033[35m' CYAN = '\033[36m' WHITE = '\033[37m' RESET = '\033[0;0m' BOLD = '\033[1m' REVERSE = '\033[2m' BLACKBG = '\033[40m' REDBG = '\033[41m' GREENBG = '\033[42m' YELLOWBG = '\033[43m' BLUEBG = '\033[44m' MAGENTABG = '\033[45m' CYANBG = '\033[46m' WHITEBG = '\033[47m' #def move(new_x, new_y): # 'Move cursor to new_x, new_y' # print '\033[' + str(new_x) + ';' + str(new_y) + 'H' # #def moveUp(lines): # 'Move cursor up # of lines' # print '\033[' + str(lines) + 'A' # #def moveDown(lines): # 'Move cursor down # of lines' # print '\033[' + str(lines) + 'B' # #def moveForward(chars): # 'Move cursor forward # of chars' # print '\033[' + str(chars) + 'C' # #def moveBack(chars): # 'Move cursor backward # of chars' # print '\033[' + str(chars) + 'D' # #def save(): # 'Saves cursor position' # print '\033[s' # #def restore(): # 'Restores cursor position' # print '\033[u' # #def clear(): # 'Clears screen and homes cursor' # print '\033[2J' # #def clrtoeol(): # 'Clears screen to end of line' # print '\033[K' # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/__init__.py0000644000175000017500000000672313233426066016227 0ustar valval00000000000000# -*- coding: utf-8 -*- ### # 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 . import dynamicScope from . import i18n builtins = (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__) builtins['supybotInternationalization'] = i18n.PluginInternationalization() from . import utils del builtins['supybotInternationalization'] (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['format'] = utils.str.format class Author(object): def __init__(self, name=None, nick=None, email=None, **kwargs): self.__dict__.update(kwargs) self.name = name self.nick = nick self.email = email def __str__(self): return '%s (%s) <%s>' % (self.name, self.nick, utils.web.mungeEmail(self.email)) class authors(object): # This is basically a bag. jemfinch = Author('Jeremy Fincher', 'jemfinch', 'jemfinch@users.sf.net') jamessan = Author('James McCoy', 'jamessan', 'vega.james@gmail.com') strike = Author('Daniel DiPaolo', 'Strike', 'ddipaolo@users.sf.net') baggins = Author('William Robinson', 'baggins', 'airbaggins@users.sf.net') skorobeus = Author('Kevin Murphy', 'Skorobeus', 'skoro@skoroworld.com') inkedmn = Author('Brett Kelly', 'inkedmn', 'inkedmn@users.sf.net') bwp = Author('Brett Phipps', 'bwp', 'phippsb@gmail.com') bear = Author('Mike Taylor', 'bear', 'bear@code-bear.com') grantbow = Author('Grant Bowman', 'Grantbow', 'grantbow@grantbow.com') stepnem = Author('Štěpán Němec', 'stepnem', 'stepnem@gmail.com') progval = Author('Valentin Lorentz', 'ProgVal', 'progval@gmail.com') unknown = Author('Unknown author', 'unknown', 'unknown@supybot.org') # Let's be somewhat safe about this. def __getattr__(self, attr): try: return getattr(super(authors, self), attr.lower()) except AttributeError: return self.unknown # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/0000755000175000017500000000000013233426077015250 5ustar valval00000000000000limnoria-2018.01.25/src/utils/web.py0000644000175000017500000002271513233426066016404 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # 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 base64 import socket sockerrors = (socket.error,) try: sockerrors += (socket.sslerror,) except AttributeError: pass from .str import normalizeWhitespace from . import minisix if minisix.PY2: import urllib import urllib2 from httplib import InvalidURL from urlparse import urlsplit, urlunsplit, urlparse from htmlentitydefs import entitydefs, name2codepoint from HTMLParser import HTMLParser Request = urllib2.Request urlquote = urllib.quote urlquote_plus = urllib.quote_plus urlunquote = urllib.unquote urlopen = urllib2.urlopen def urlencode(*args, **kwargs): return urllib.urlencode(*args, **kwargs).encode() from urllib2 import HTTPError, URLError from urllib import splithost, splituser else: from http.client import InvalidURL from urllib.parse import urlsplit, urlunsplit, urlparse from html.entities import entitydefs, name2codepoint from html.parser import HTMLParser import urllib.request, urllib.parse, urllib.error Request = urllib.request.Request urlquote = urllib.parse.quote urlquote_plus = urllib.parse.quote_plus urlunquote = urllib.parse.unquote urlopen = urllib.request.urlopen def urlencode(*args, **kwargs): return urllib.parse.urlencode(*args, **kwargs) from urllib.error import HTTPError, URLError from urllib.parse import splithost, splituser class Error(Exception): pass _octet = r'(?:2(?:[0-4]\d|5[0-5])|1\d\d|\d{1,2})' _ipAddr = r'%s(?:\.%s){3}' % (_octet, _octet) # Base domain regex off RFC 1034 and 1738 _label = r'[0-9a-z][-0-9a-z]*[0-9a-z]?' _scheme = r'[a-z][a-z0-9+.-]*' _domain = r'%s(?:\.%s)*\.[0-9a-z][-0-9a-z]+' % (_label, _label) _urlRe = r'(%s://(?:\S+@)?(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?)' % ( _scheme, _domain, _ipAddr) urlRe = re.compile(_urlRe, re.I) _httpUrlRe = r'(https?://(?:\S+@)?(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?)' % \ (_domain, _ipAddr) httpUrlRe = re.compile(_httpUrlRe, re.I) REFUSED = 'Connection refused.' TIMED_OUT = 'Connection timed out.' UNKNOWN_HOST = 'Unknown host.' RESET_BY_PEER = 'Connection reset by peer.' FORBIDDEN = 'Client forbidden from accessing URL.' def strError(e): try: n = e.args[0] except Exception: return str(e) if n == 111: return REFUSED elif n in (110, 10060): return TIMED_OUT elif n == 104: return RESET_BY_PEER elif n in (8, 7, 3, 2, -2, -3): return UNKNOWN_HOST elif n == 403: return FORBIDDEN else: return str(e) defaultHeaders = { 'User-agent': 'Mozilla/5.0 (compatible; utils.web python module)' } # Other modules should feel free to replace this with an appropriate # application-specific function. Feel free to use a callable here. proxy = None def getUrlFd(url, headers=None, data=None, timeout=None): """getUrlFd(url, headers=None, data=None, timeout=None) Opens the given url and returns a file object. Headers and data are a dict and string, respectively, as per urllib.request.Request's arguments.""" if headers is None: headers = defaultHeaders if minisix.PY3 and isinstance(data, str): data = data.encode() try: if not isinstance(url, Request): (scheme, loc, path, query, frag) = urlsplit(url) (user, host) = splituser(loc) url = urlunsplit((scheme, host, path, query, '')) request = Request(url, headers=headers, data=data) if user: request.add_header('Authorization', 'Basic %s' % base64.b64encode(user)) else: request = url request.add_data(data) fd = urlopen(request, timeout=timeout) return fd except socket.timeout as e: raise Error(TIMED_OUT) except sockerrors as e: raise Error(strError(e)) except InvalidURL as e: raise Error('Invalid URL: %s' % e) except HTTPError as e: raise Error(strError(e)) except URLError as e: raise Error(strError(e.reason)) # Raised when urllib doesn't recognize the url type except ValueError as e: raise Error(strError(e)) def getUrlTargetAndContent(url, size=None, headers=None, data=None, timeout=None): """getUrlTargetAndContent(url, size=None, headers=None, data=None, timeout=None) Gets a page. Returns two strings that are the page gotten and the target URL (ie. after redirections). Size is an integer number of bytes to read from the URL. Headers and data are dicts as per urllib.request.Request's arguments.""" fd = getUrlFd(url, headers=headers, data=data, timeout=timeout) try: if size is None: text = fd.read() else: text = fd.read(size) except socket.timeout: raise Error(TIMED_OUT) target = fd.geturl() fd.close() return (target, text) def getUrlContent(*args, **kwargs): """getUrlContent(url, size=None, headers=None, data=None, timeout=None) Gets a page. Returns a string that is the page gotten. Size is an integer number of bytes to read from the URL. Headers and data are dicts as per urllib.request.Request's arguments.""" (target, text) = getUrlTargetAndContent(*args, **kwargs) return text def getUrl(*args, **kwargs): """Alias for getUrlContent.""" return getUrlContent(*args, **kwargs) def getDomain(url): return urlparse(url)[1] _charset_re = (']+charset=' """(?P("[^"]+"|'[^']+'))""") def getEncoding(s): try: match = re.search(_charset_re, s, re.MULTILINE) if match: return match.group('charset')[1:-1] except: match = re.search(_charset_re.encode(), s, re.MULTILINE) if match: return match.group('charset').decode()[1:-1] try: import charade.universaldetector u = charade.universaldetector.UniversalDetector() u.feed(s) u.close() return u.result['encoding'] except: return None class HtmlToText(HTMLParser, object): """Taken from some eff-bot code on c.l.p.""" entitydefs = entitydefs.copy() entitydefs['nbsp'] = ' ' entitydefs['apos'] = '\'' def __init__(self, tagReplace=' '): self.data = [] self.tagReplace = tagReplace super(HtmlToText, self).__init__() def append(self, data): self.data.append(data) def handle_starttag(self, tag, attr): self.append(self.tagReplace) def handle_endtag(self, tag): self.append(self.tagReplace) def handle_data(self, data): self.append(data) def handle_entityref(self, data): if minisix.PY3: if data in name2codepoint: self.append(chr(name2codepoint[data])) elif isinstance(data, bytes): self.append(data.decode()) else: self.append(data) else: if data in name2codepoint: self.append(unichr(name2codepoint[data])) elif isinstance(data, str): self.append(data.decode('utf8', errors='replace')) else: self.append(data) def getText(self): text = ''.join(self.data).strip() return normalizeWhitespace(text) def handle_charref(self, name): self.append(self.unescape('&#%s;' % name)) def htmlToText(s, tagReplace=' '): """Turns HTML into text. tagReplace is a string to replace HTML tags with. """ encoding = getEncoding(s) if encoding: s = s.decode(encoding) else: try: if minisix.PY2 or isinstance(s, bytes): s = s.decode('utf8') except: pass x = HtmlToText(tagReplace) x.feed(s) x.close() return x.getText() def mungeEmail(s): s = s.replace('@', ' AT ') s = s.replace('.', ' DOT ') return s # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/transaction.py0000644000175000017500000002111613233426066020146 0ustar valval00000000000000### # 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. ### """ Defines a Transaction class for multi-file transactions. """ import os import shutil import os.path from . import error, file as File, python # 'txn' is used as an abbreviation for 'transaction' in the following source. class FailedAcquisition(error.Error): def __init__(self, txnDir, e=None): self.txnDir = txnDir msg = 'Could not acquire transaction directory: %s.' % self.txnDir error.Error.__init__(self, msg, e) class InProgress(error.Error): def __init__(self, inProgress, e=None): self.inProgress = inProgress msg = 'Transaction appears to be in progress already: %s exists.' % \ self.inProgress error.Error.__init__(self, msg, e) class InvalidCwd(Exception): pass class TransactionMixin(python.Object): JOURNAL = 'journal' ORIGINALS = 'originals' INPROGRESS = '.inProgress' REPLACEMENTS = 'replacements' # expects a self.dir. used by Transaction and Rollback. def __init__(self, txnDir): self.txnDir = txnDir self.dir = self.txnDir + self.INPROGRESS self._journalName = self.dirize(self.JOURNAL) def escape(self, filename): return os.path.abspath(filename)[1:] def dirize(self, *args): return os.path.join(self.dir, *args) def _original(self, filename): return self.dirize(self.ORIGINALS, self.escape(filename)) def _replacement(self, filename): return self.dirize(self.REPLACEMENTS, self.escape(filename)) def _checkCwd(self): expected = File.contents(self.dirize('cwd')) if os.getcwd() != expected: raise InvalidCwd(expected) def _journalCommands(self): journal = open(self._journalName) for line in journal: line = line.rstrip('\n') (command, rest) = line.split(None, 1) args = rest.split() yield (command, args) journal.close() class Transaction(TransactionMixin): # XXX Transaction needs to be made threadsafe. def __init__(self, *args, **kwargs): """Transaction(txnDir) -> None txnDir is the directory that will hold the transaction's working files and such. If it can't be renamed, there is probably an active transaction. """ TransactionMixin.__init__(self, *args, **kwargs) if os.path.exists(self.dir): raise InProgress(self.dir) if not os.path.exists(self.txnDir): raise FailedAcquisition(self.txnDir) try: os.rename(self.txnDir, self.dir) except EnvironmentError as e: raise FailedAcquisition(self.txnDir, e) os.mkdir(self.dirize(self.ORIGINALS)) os.mkdir(self.dirize(self.REPLACEMENTS)) self._journal = open(self._journalName, 'a') cwd = open(self.dirize('cwd'), 'w') cwd.write(os.getcwd()) cwd.close() def _journalCommand(self, command, *args): File.writeLine(self._journal, '%s %s' % (command, ' '.join(map(str, args)))) self._journal.flush() def _makeOriginal(self, filename): File.copy(filename, self._original(filename)) # XXX There needs to be a way, given a transaction, to get a # "sub-transaction", which: # # 1. Doesn't try to grab the txnDir and move it, but instead is just # given the actual directory being used and uses that. # 2. Acquires the lock of the original transaction, only releasing it # when its .commit method is called (assuming Transaction is # threadsafe). # 3. Has a no-op .commit method (i.e., doesn't commit). # # This is so that, for instance, an object with an active Transaction # can give other objects a Transaction-ish object without worrying that # the transaction will be committed, while still allowing those objects # to work properly with real transactions (i.e., they still call # as they would on a normal Transaction, it just has no effect with a # sub-transaction). # The method that returns a subtransaction should be called "child." def child(self): raise NotImplementedError # XXX create, replace, etc. return file objects. This class should keep a # list of such file descriptors and only allow a commit if all of them # are closed. Trying to commit with open file objects should raise an # exception. def create(self, filename): """ Returns a file object for a filename that should be created (with the contents as they were written to the filename) when the transaction is committed. """ raise NotImplementedError # XXX. def mkdir(self, filename): raise NotImplementedError # XXX def delete(self, filename): raise NotImplementedError # XXX def replace(self, filename): """ Returns a file object for a filename that should be replaced by the contents written to the file object when the transaction is committed. """ self._checkCwd() self._makeOriginal(filename) self._journalCommand('replace', filename) return File.open(self._replacement(filename)) def append(self, filename): self._checkCwd() length = os.stat(filename).st_size self._journalCommand('append', filename, length) replacement = self._replacement(filename) File.copy(filename, replacement) return open(replacement, 'a') def commit(self, removeWhenComplete=True): self._journal.close() self._checkCwd() File.touch(self.dirize('commit')) for (command, args) in self._journalCommands(): methodName = 'commit%s' % command.capitalize() getattr(self, methodName)(*args) File.touch(self.dirize('committed')) if removeWhenComplete: shutil.rmtree(self.dir) def commitReplace(self, filename): shutil.copy(self._replacement(filename), filename) def commitAppend(self, filename, length): shutil.copy(self._replacement(filename), filename) # XXX need to be able to rename files transactionally. (hard; especially # with renames that depend on one another. It might be easier to do # rename separate from relocate.) class Rollback(TransactionMixin): def rollback(self, removeWhenComplete=True): self._checkCwd() if not os.path.exists(self.dirize('commit')): return # No action taken; commit hadn't begun. for (command, args) in self._journalCommands(): methodName = 'rollback%s' % command.capitalize() getattr(self, methodName)(*args) if removeWhenComplete: shutil.rmtree(self.dir) def rollbackReplace(self, filename): shutil.copy(self._original(filename), filename) def rollbackAppend(self, filename, length): fd = open(filename, 'a') fd.truncate(int(length)) fd.close() # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: limnoria-2018.01.25/src/utils/structures.py0000644000175000017500000003514213233426066020050 0ustar valval00000000000000### # Copyright (c) 2002-2009, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Data structures for Python. """ import time import collections class RingBuffer(object): """Class to represent a fixed-size ring buffer.""" __slots__ = ('L', 'i', 'full', 'maxSize') def __init__(self, maxSize, seq=()): if maxSize <= 0: raise ValueError('maxSize must be > 0.') self.maxSize = maxSize self.reset() for elt in seq: self.append(elt) def reset(self): self.full = False self.L = [] self.i = 0 def resize(self, size): L = list(self) i = self.i self.reset() self.maxSize = size for elt in L[i+1:]: self.append(elt) for elt in L[0:i]: self.append(elt) def __len__(self): return len(self.L) def __eq__(self, other): if self.__class__ == other.__class__ and \ self.maxSize == other.maxSize and len(self) == len(other): iterator = iter(other) for elt in self: otherelt = next(iterator) if not elt == otherelt: return False return True return False def __bool__(self): return len(self) > 0 __nonzero__ = __bool__ def __contains__(self, elt): return elt in self.L def append(self, elt): if self.full: self.L[self.i] = elt self.i += 1 self.i %= len(self.L) elif len(self) == self.maxSize: self.full = True self.append(elt) else: self.L.append(elt) def extend(self, seq): for elt in seq: self.append(elt) def __getitem__(self, idx): if self.full: oidx = idx if isinstance(oidx, slice): L = [] for i in range(*slice.indices(oidx, len(self))): L.append(self[i]) return L else: (m, idx) = divmod(oidx, len(self.L)) if m and m != -1: raise IndexError(oidx) idx = (idx + self.i) % len(self.L) return self.L[idx] else: if isinstance(idx, slice): L = [] for i in range(*slice.indices(idx, len(self))): L.append(self[i]) return L else: return self.L[idx] def __setitem__(self, idx, elt): if self.full: oidx = idx if isinstance(oidx, slice): range_ = range(*slice.indices(oidx, len(self))) if len(range_) != len(elt): raise ValueError('seq must be the same length as slice.') else: for (i, x) in zip(range_, elt): self[i] = x else: (m, idx) = divmod(oidx, len(self.L)) if m and m != -1: raise IndexError(oidx) idx = (idx + self.i) % len(self.L) self.L[idx] = elt else: if isinstance(idx, slice): range_ = range(*slice.indices(idx, len(self))) if len(range_) != len(elt): raise ValueError('seq must be the same length as slice.') else: for (i, x) in zip(range_, elt): self[i] = x else: self.L[idx] = elt def __repr__(self): return 'RingBuffer(%r, %r)' % (self.maxSize, list(self)) def __getstate__(self): return (self.maxSize, self.full, self.i, self.L) def __setstate__(self, state): (maxSize, full, i, L) = state self.maxSize = maxSize self.full = full self.i = i self.L = L class queue(object): """Queue class for handling large queues. Queues smaller than 1,000 or so elements are probably better served by the smallqueue class. """ __slots__ = ('front', 'back') def __init__(self, seq=()): self.back = [] self.front = [] for elt in seq: self.enqueue(elt) def reset(self): self.back[:] = [] self.front[:] = [] def enqueue(self, elt): self.back.append(elt) def dequeue(self): try: return self.front.pop() except IndexError: self.back.reverse() self.front = self.back self.back = [] return self.front.pop() def peek(self): if self.front: return self.front[-1] else: return self.back[0] def __len__(self): return len(self.front) + len(self.back) def __bool__(self): return bool(self.back or self.front) __nonzero__ = __bool__ def __contains__(self, elt): return elt in self.front or elt in self.back def __iter__(self): for elt in reversed(self.front): yield elt for elt in self.back: yield elt def __eq__(self, other): if len(self) == len(other): otheriter = iter(other) for elt in self: otherelt = next(otheriter) if not (elt == otherelt): return False return True else: return False def __repr__(self): return 'queue([%s])' % ', '.join(map(repr, self)) def __getitem__(self, oidx): if len(self) == 0: raise IndexError('queue index out of range') if isinstance(oidx, slice): L = [] for i in range(*slice.indices(oidx, len(self))): L.append(self[i]) return L else: (m, idx) = divmod(oidx, len(self)) if m and m != -1: raise IndexError(oidx) if len(self.front) > idx: return self.front[-(idx+1)] else: return self.back[(idx-len(self.front))] def __setitem__(self, oidx, value): if len(self) == 0: raise IndexError('queue index out of range') if isinstance(oidx, slice): range_ = range(*slice.indices(oidx, len(self))) if len(range_) != len(value): raise ValueError('seq must be the same length as slice.') else: for i in range_: (m, idx) = divmod(oidx, len(self)) if m and m != -1: raise IndexError(oidx) for (i, x) in zip(range_, value): self[i] = x else: (m, idx) = divmod(oidx, len(self)) if m and m != -1: raise IndexError(oidx) if len(self.front) > idx: self.front[-(idx+1)] = value else: self.back[idx-len(self.front)] = value def __delitem__(self, oidx): if isinstance(oidx, slice): range_ = range(*slice.indices(oidx, len(self))) for i in range_: del self[i] else: (m, idx) = divmod(oidx, len(self)) if m and m != -1: raise IndexError(oidx) if len(self.front) > idx: del self.front[-(idx+1)] else: del self.back[idx-len(self.front)] def __getstate__(self): return (list(self),) def __setstate__(self, state): (L,) = state L.reverse() self.front = L self.back = [] class smallqueue(list): __slots__ = () def enqueue(self, elt): self.append(elt) def dequeue(self): return self.pop(0) def peek(self): return self[0] def __repr__(self): return 'smallqueue([%s])' % ', '.join(map(repr, self)) def reset(self): self[:] = [] class TimeoutQueue(object): __slots__ = ('queue', 'timeout') def __init__(self, timeout, queue=None): if queue is None: queue = smallqueue() self.queue = queue self.timeout = timeout def reset(self): self.queue.reset() def __repr__(self): self._clearOldElements() return '%s(timeout=%r, queue=%r)' % (self.__class__.__name__, self.timeout, self.queue) def _getTimeout(self): if callable(self.timeout): return self.timeout() else: return self.timeout def _clearOldElements(self): now = time.time() while self.queue and now - self.queue.peek()[0] > self._getTimeout(): self.queue.dequeue() def setTimeout(self, i): self.timeout = i def enqueue(self, elt, at=None): if at is None: at = time.time() self.queue.enqueue((at, elt)) def dequeue(self): self._clearOldElements() return self.queue.dequeue()[1] def __iter__(self): # We could _clearOldElements here, but what happens if someone stores # the resulting generator and elements that should've timed out are # yielded? Hmm? What happens then, smarty-pants? for (t, elt) in self.queue: if time.time() - t < self._getTimeout(): yield elt def __len__(self): # No dependency on utils.iter # return ilen(self) self._clearOldElements() return len(self.queue) class MaxLengthQueue(queue): __slots__ = ('length',) def __init__(self, length, seq=()): self.length = length queue.__init__(self, seq) def __getstate__(self): return (self.length, queue.__getstate__(self)) def __setstate__(self, state): (length, q) = state self.length = length queue.__setstate__(self, q) def enqueue(self, elt): queue.enqueue(self, elt) if len(self) > self.length: self.dequeue() class TwoWayDictionary(dict): __slots__ = () def __init__(self, seq=(), **kwargs): if hasattr(seq, 'iteritems'): seq = seq.iteritems() elif hasattr(seq, 'items'): seq = seq.items() for (key, value) in seq: self[key] = value self[value] = key for (key, value) in kwargs.items(): self[key] = value self[value] = key def __setitem__(self, key, value): dict.__setitem__(self, key, value) dict.__setitem__(self, value, key) def __delitem__(self, key): value = self[key] dict.__delitem__(self, key) dict.__delitem__(self, value) class MultiSet(object): __slots__ = ('d',) def __init__(self, seq=()): self.d = {} for elt in seq: self.add(elt) def add(self, elt): try: self.d[elt] += 1 except KeyError: self.d[elt] = 1 def remove(self, elt): self.d[elt] -= 1 if not self.d[elt]: del self.d[elt] def __getitem__(self, elt): return self.d[elt] def __contains__(self, elt): return elt in self.d class CacheDict(collections.MutableMapping): __slots__ = ('d', 'max') def __init__(self, max, **kwargs): self.d = dict(**kwargs) self.max = max def __getitem__(self, key): return self.d[key] def __setitem__(self, key, value): if len(self.d) >= self.max: self.d.clear() self.d[key] = value def __delitem__(self, key): del self.d[key] def keys(self): return self.d.keys() def items(self): return self.d.items() def __iter__(self): return iter(self.d) def __len__(self): return len(self.d) class TruncatableSet(collections.MutableSet): """A set that keeps track of the order of inserted elements so the oldest can be removed.""" __slots__ = ('_ordered_items', '_items') def __init__(self, iterable=[]): self._ordered_items = list(iterable) self._items = set(self._ordered_items) def __repr__(self): return 'TruncatableSet({%r})' % self._items def __contains__(self, item): return item in self._items def __iter__(self): return iter(self._items) def __len__(self): return len(self._items) def add(self, item): if item not in self._items: self._items.add(item) self._ordered_items.append(item) def discard(self, item): self._items.discard(item) self._ordered_items.remove(item) def truncate(self, size): assert size >= 0 removed_size = len(self)-size # I make two different cases depending on removed_size>> nItems(4, '') '4' >>> nItems(1, 'clock') '1 clock' >>> nItems(10, 'clock') '10 clocks' >>> nItems(4, '', between='grandfather') '4 grandfather' >>> nItems(10, 'clock', between='grandfather') '10 grandfather clocks' """ assert isinstance(n, minisix.integer_types), \ 'The order of the arguments to nItems changed again, sorry.' if item == '': if between is None: return format('%s', n) else: return format('%s %s', n, item) if between is None: if n != 1: return format('%s %p', n, item) else: return format('%s %s', n, item) else: if n != 1: return format('%s %s %p', n, between, item) else: return format('%s %s %s', n, between, item) @internationalizeFunction('ordinal') def ordinal(i): """Returns i + the ordinal indicator for the number. Example: ordinal(3) => '3rd' """ i = int(i) if i % 100 in (11,12,13): return '%sth' % i ord = 'th' test = i % 10 if test == 1: ord = 'st' elif test == 2: ord = 'nd' elif test == 3: ord = 'rd' return '%s%s' % (i, ord) @internationalizeFunction('be') def be(i): """Returns the form of the verb 'to be' based on the number i.""" if i == 1: return 'is' else: return 'are' @internationalizeFunction('has') def has(i): """Returns the form of the verb 'to have' based on the number i.""" if i == 1: return 'has' else: return 'have' def toBool(s): s = s.strip().lower() if s in ('true', 'on', 'enable', 'enabled', '1'): return True elif s in ('false', 'off', 'disable', 'disabled', '0'): return False else: raise ValueError('Invalid string for toBool: %s' % quoted(s)) # When used with Supybot, this is overriden when supybot.conf is loaded def timestamp(t): if t is None: t = time.time() return time.ctime(t) def url(url): return url _formatRe = re.compile('%((?:\d+)?\.\d+f|[bfhiLnpqrsStTuv%])') def format(s, *args, **kwargs): """w00t. %: 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: nItems (takes a 2-tuple of (n, item) or a 3-tuple of (n, between, item)) S: returns a human-readable size (takes an int) t: time, formatted (takes an int) T: time delta, formatted (takes an int) u: url, wrapped in braces (this should be configurable at some point) v: void : takes one or many arguments, but doesn't display it (useful for translation) """ # Note to developers: If you want to add an argument type, do not forget # to add the character to the _formatRe regexp or it will be ignored # (and hard to debug if you don't know the trick). # Of course, you should also document it in the docstring above. if minisix.PY2: def pred(s): if isinstance(s, unicode): return s.encode('utf8') else: return s args = map(pred, args) args = list(args) args.reverse() # For more efficient popping. def sub(match): char = match.group(1) if char == 's': token = args.pop() if isinstance(token, str): return token elif minisix.PY2 and isinstance(token, unicode): return token.encode('utf8', 'replace') else: return str(token) elif char == 'i': # XXX Improve me! return str(args.pop()) elif char.endswith('f'): return ('%'+char) % args.pop() elif char == 'b': return be(args.pop()) elif char == 'h': return has(args.pop()) elif char == 'L': t = args.pop() if isinstance(t, tuple) and len(t) == 2: if not isinstance(t[0], list): raise ValueError('Invalid list for %%L in format: %s' % t) if not isinstance(t[1], minisix.string_types): raise ValueError('Invalid string for %%L in format: %s' % t) return commaAndify(t[0], And=t[1]) elif hasattr(t, '__iter__'): return commaAndify(t) else: raise ValueError('Invalid value for %%L in format: %s' % t) elif char == 'p': return pluralize(args.pop()) elif char == 'q': return quoted(args.pop()) elif char == 'r': return repr(args.pop()) elif char == 'n': t = args.pop() if not isinstance(t, (tuple, list)): raise ValueError('Invalid value for %%n in format: %s' % t) if len(t) == 2: return nItems(*t) elif len(t) == 3: return nItems(t[0], t[2], between=t[1]) else: raise ValueError('Invalid value for %%n in format: %s' % t) elif char == 'S': t = args.pop() if not isinstance(t, minisix.integer_types): raise ValueError('Invalid value for %%S in format: %s' % t) for suffix in ['B','KB','MB','GB','TB']: if t < 1024: return "%i%s" % (t, suffix) t /= 1024 elif char == 't': return timestamp(args.pop()) elif char == 'T': from .gen import timeElapsed return timeElapsed(args.pop()) elif char == 'u': return url(args.pop()) elif char == 'v': args.pop() return '' elif char == '%': return '%' else: raise ValueError('Invalid char in sub (in format).') try: return _formatRe.sub(sub, s) except IndexError: raise ValueError('Extra format chars in format spec: %r' % s) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/seq.py0000644000175000017500000001003513233426066016407 0ustar valval00000000000000### # 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. ### def window(L, size): """list * size -> window iterable Returns a sliding 'window' through the list L of size size.""" assert not isinstance(L, int), 'Argument order swapped: window(L, size)' if size < 1: raise ValueError('size <= 0 disallowed.') for i in range(len(L) - (size-1)): yield L[i:i+size] def mapinto(f, L): for (i, x) in enumerate(L): L[i] = f(x) def renumerate(L): for i in range(len(L)-1, -1, -1): yield (i, L[i]) def dameraulevenshtein(seq1, seq2): """Calculate the Damerau-Levenshtein distance between sequences. This distance is the number of additions, deletions, substitutions, and transpositions needed to transform the first sequence into the second. Although generally used with strings, any sequences of comparable objects will work. Transpositions are exchanges of *consecutive* characters; all other operations are self-explanatory. This implementation is O(N*M) time and O(M) space, for N and M the lengths of the two sequences. >>> dameraulevenshtein('ba', 'abc') 2 >>> dameraulevenshtein('fee', 'deed') 2 It works with arbitrary sequences too: >>> dameraulevenshtein('abcd', ['b', 'a', 'c', 'd', 'e']) 2 """ # codesnippet:D0DE4716-B6E6-4161-9219-2903BF8F547F # Conceptually, this is based on a len(seq1) + 1 * len(seq2) + 1 matrix. # However, only the current and two previous rows are needed at once, # so we only store those. # Sourced from http://mwh.geek.nz/2009/04/26/python-damerau-levenshtein-distance/ oneago = None thisrow = list(range(1, len(seq2) + 1)) + [0] for x in range(len(seq1)): # Python lists wrap around for negative indices, so put the # leftmost column at the *end* of the list. This matches with # the zero-indexed strings and saves extra calculation. twoago, oneago, thisrow = oneago, thisrow, [0] * len(seq2) + [x + 1] for y in range(len(seq2)): delcost = oneago[y] + 1 addcost = thisrow[y - 1] + 1 subcost = oneago[y - 1] + (seq1[x] != seq2[y]) thisrow[y] = min(delcost, addcost, subcost) # This block deals with transpositions if (x > 0 and y > 0 and seq1[x] == seq2[y - 1] and seq1[x-1] == seq2[y] and seq1[x] != seq2[y]): thisrow[y] = min(thisrow[y], twoago[y - 2] + 1) return thisrow[len(seq2) - 1] # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/python.py0000644000175000017500000001615513233426066017151 0ustar valval00000000000000### # Copyright (c) 2005-2009, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # 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 types import fnmatch import threading def universalImport(*names): """Attempt to import the given modules, in order, returning the first successfully imported module. ImportError will be raised, as usual, if no imports succeed. To emulate ``from ModuleA import ModuleB'', pass the string 'ModuleA.ModuleB'""" f = sys._getframe(1) for name in names: try: # __import__ didn't gain keyword arguments until 2.5 ret = __import__(name, f.f_globals) except ImportError: continue else: if '.' in name: parts = name.split('.')[1:] while parts: ret = getattr(ret, parts[0]) del parts[0] return ret raise ImportError(','.join(names)) def changeFunctionName(f, name, doc=None): if doc is None: doc = f.__doc__ if hasattr(f, '__closure__'): closure = f.__closure__ else: # Pypy closure = f.func_closure newf = types.FunctionType(f.__code__, f.__globals__, name, f.__defaults__, closure) newf.__doc__ = doc return newf class Object(object): def __ne__(self, other): return not self == other class MetaSynchronized(type): METHODS = '__synchronized__' LOCK = '_MetaSynchronized_rlock' def __new__(cls, name, bases, dict): sync = set() for base in bases: if hasattr(base, MetaSynchronized.METHODS): sync.update(getattr(base, MetaSynchronized.METHODS)) if MetaSynchronized.METHODS in dict: sync.update(dict[MetaSynchronized.METHODS]) if sync: def synchronized(f): def g(self, *args, **kwargs): lock = getattr(self, MetaSynchronized.LOCK) lock.acquire() try: f(self, *args, **kwargs) finally: lock.release() return changeFunctionName(g, f.__name__, f.__doc__) for attr in sync: if attr in dict: dict[attr] = synchronized(dict[attr]) original__init__ = dict.get('__init__') def __init__(self, *args, **kwargs): if not hasattr(self, MetaSynchronized.LOCK): setattr(self, MetaSynchronized.LOCK, threading.RLock()) if original__init__: original__init__(self, *args, **kwargs) else: # newclass is defined below. super(newclass, self).__init__(*args, **kwargs) dict['__init__'] = __init__ newclass = super(MetaSynchronized, cls).__new__(cls, name, bases, dict) return newclass Synchronized = MetaSynchronized('Synchronized', (), {}) def glob2re(g): pattern = fnmatch.translate(g) if pattern.startswith('(?s:') and pattern.endswith(')\\Z'): # Python >= 3.6 return pattern[4:-3] + '\\Z' elif pattern.endswith('\\Z(?ms)'): # Python >= 2.6 and < 3.6 # # Translate glob to regular expression, trimming the "match EOL" # portion of the regular expression. # Some Python versions use \Z(?ms) per # https://bugs.python.org/issue6665 return pattern[:-7] else: assert False, 'Python < 2.6, or unknown behavior of fnmatch.translate.' _debug_software_name = 'Limnoria' _debug_software_version = None # From http://code.activestate.com/recipes/52215-get-more-information-from-tracebacks/ def collect_extra_debug_data(): """ Print the usual traceback information, followed by a listing of all the local variables in each frame. """ data = '' try: tb = sys.exc_info()[2] stack = [] while tb: stack.append(tb.tb_frame) tb = tb.tb_next finally: del tb if _debug_software_version: data += '%s version: %s\n\n' % \ (_debug_software_name, _debug_software_version) else: data += '(Cannot get %s version.)\n\n' % _debug_software_name data += 'Locals by frame, innermost last:\n' for frame in stack: data += '\n\n' data += ('Frame %s in %s at line %s\n' % (frame.f_code.co_name, frame.f_code.co_filename, frame.f_lineno)) frame_locals = frame.f_locals for inspected in ('self', 'cls'): if inspected in frame_locals: if hasattr(frame_locals[inspected], '__dict__') and \ frame_locals[inspected].__dict__: for (key, value) in frame_locals[inspected].__dict__.items(): frame_locals['%s.%s' % (inspected, key)] = value for key, value in frame_locals.items(): if key == '__builtins__': # This is flooding continue data += ('\t%20s = ' % key) #We have to be careful not to cause a new error in our error #printer! Calling str() on an unknown object could cause an #error we don't want. try: data += repr(value) + '\n' except: data += '\n' data += '\n' data += '+-----------------------+\n' data += '| End of locals display |\n' data += '+-----------------------+\n' data += '\n' return data # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=78: limnoria-2018.01.25/src/utils/net.py0000644000175000017500000001621513233426066016413 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2011, 2013, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Simple utility modules. """ import re import ssl import socket import hashlib from .web import _ipAddr, _domain emailRe = re.compile(r"^(\w&.+-]+!)*[\w&.+-]+@(%s|%s)$" % (_domain, _ipAddr), re.I) def getAddressFromHostname(host, port=None, attempt=0): addrinfo = socket.getaddrinfo(host, port) addresses = [] for (family, socktype, proto, canonname, sockaddr) in addrinfo: if sockaddr[0] not in addresses: addresses.append(sockaddr[0]) return addresses[attempt % len(addresses)] def getSocket(host, port=None, socks_proxy=None, vhost=None, vhostv6=None): """Returns a socket of the correct AF_INET type (v4 or v6) in order to communicate with host. """ if not socks_proxy: addrinfo = socket.getaddrinfo(host, port) host = addrinfo[0][4][0] if socks_proxy: import socks s = socks.socksocket() hostname, port = socks_proxy.rsplit(':', 1) s.setproxy(socks.PROXY_TYPE_SOCKS5, hostname, int(port), rdns=True) return s if isIPV4(host): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if vhost: s.bind((vhost, 0)) return s elif isIPV6(host): s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) if vhostv6: s.bind((vhostv6, 0)) return s else: raise socket.error('Something wonky happened.') def isSocketAddress(s): if ':' in s: host, port = s.rsplit(':', 1) try: int(port) sock = getSocket(host, port) return True except (ValueError, socket.error): pass return False def isIP(s): """Returns whether or not a given string is an IP address. >>> isIP('255.255.255.255') 1 >>> isIP('::1') 0 """ return isIPV4(s) or isIPV6(s) def isIPV4(s): """Returns whether or not a given string is an IPV4 address. >>> isIPV4('255.255.255.255') 1 >>> isIPV4('abc.abc.abc.abc') 0 """ if set(s) - set('0123456789.'): # inet_aton ignores trailing data after the first valid IP address return False try: return bool(socket.inet_aton(str(s))) except socket.error: return False def bruteIsIPV6(s): if s.count('::') <= 1: L = s.split(':') if len(L) <= 8: for x in L: if x: try: int(x, 16) except ValueError: return False return True return False def isIPV6(s): """Returns whether or not a given string is an IPV6 address.""" try: if hasattr(socket, 'inet_pton'): return bool(socket.inet_pton(socket.AF_INET6, s)) else: return bruteIsIPV6(s) except socket.error: try: socket.inet_pton(socket.AF_INET6, '::') except socket.error: # We gotta fake it. return bruteIsIPV6(s) return False normalize_fingerprint = lambda fp: fp.replace(':', '').lower() FINGERPRINT_ALGORITHMS = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512') def check_certificate_fingerprint(conn, trusted_fingerprints): trusted_fingerprints = set(normalize_fingerprint(fp) for fp in trusted_fingerprints) cert = conn.getpeercert(binary_form=True) for algorithm in FINGERPRINT_ALGORITHMS: h = hashlib.new(algorithm) h.update(cert) if h.hexdigest() in trusted_fingerprints: return raise ssl.CertificateError('No matching fingerprint.') if hasattr(ssl, 'create_default_context'): def ssl_wrap_socket(conn, hostname, logger, certfile=None, trusted_fingerprints=None, verify=True, ca_file=None, **kwargs): context = ssl.create_default_context(**kwargs) if trusted_fingerprints or not verify: # Do not use Certification Authorities context.check_hostname = False context.verify_mode = ssl.CERT_NONE if ca_file: context.load_verify_locations(cafile=ca_file) if certfile: context.load_cert_chain(certfile) conn = context.wrap_socket(conn, server_hostname=hostname) if verify and trusted_fingerprints: check_certificate_fingerprint(conn, trusted_fingerprints) return conn else: def ssl_wrap_socket(conn, hostname, logger, verify=True, certfile=None, ca_file=None, trusted_fingerprints=None): # TLSv1.0 is the only TLS version Python < 2.7.9 supports # (besides SSLv2 and v3, which are known to be insecure) try: conn = ssl.wrap_socket(conn, server_hostname=hostname, certfile=certfile, ca_certs=ca_file, ssl_version=ssl.PROTOCOL_TLSv1) except TypeError: # server_hostname is not supported conn = ssl.wrap_socket(conn, certfile=certfile, ca_certs=ca_file, ssl_version=ssl.PROTOCOL_TLSv1) if trusted_fingerprints: check_certificate_fingerprint(conn, trusted_fingerprints) elif verify: logger.critical('This Python version does not support SSL/TLS ' 'certification authority verification, which makes your ' 'connection vulnerable to man-in-the-middle attacks. See: ' '') return conn # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/minisix.py0000644000175000017500000000750513233426066017307 0ustar valval00000000000000### # Copyright (c) 2014, 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. ### """Restricted equivalent to six.""" from __future__ import division import sys import warnings if sys.version_info[0] >= 3: PY2 = False PY3 = True intern = sys.intern integer_types = (int,) string_types = (str,) long = int import io import pickle import queue u = lambda x:x L = lambda x:x def make_datetime_utc(dt): import datetime return dt.replace(tzinfo=datetime.timezone.utc) def timedelta__totalseconds(td): return td.total_seconds() if sys.version_info >= (3, 3): def datetime__timestamp(dt): return dt.timestamp() else: def datetime__timestamp(dt): import datetime td = dt - datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) return timedelta__totalseconds(td) else: PY2 = True PY3 = False if isinstance(__builtins__, dict): intern = __builtins__['intern'] else: intern = __builtins__.intern integer_types = (int, long) string_types = (basestring,) long = long class io: # cStringIO is buggy with Python 2.6 ( # see http://paste.progval.net/show/227/ ) # and it does not handle unicode objects in Python 2.x from StringIO import StringIO from cStringIO import StringIO as BytesIO import cPickle as pickle import Queue as queue u = lambda x:x.decode('utf8') L = lambda x:long(x) def make_datetime_utc(dt): warnings.warn('Timezones are not available on this version of ' 'Python and may lead to incorrect results. You should ' 'consider upgrading to Python 3.') return dt.replace(tzinfo=None) if sys.version_info >= (2, 7): def timedelta__totalseconds(td): return td.total_seconds() else: def timedelta__totalseconds(td): return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 def datetime__timestamp(dt): import datetime warnings.warn('Timezones are not available on this version of ' 'Python and may lead to incorrect results. You should ' 'consider upgrading to Python 3.') return timedelta__totalseconds(dt - datetime.datetime(1970, 1, 1)) limnoria-2018.01.25/src/utils/iter.py0000644000175000017500000001137013233426066016565 0ustar valval00000000000000### # 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 __future__ import division import random from itertools import * from . import minisix # For old plugins ifilter = filter def filterfalse(p, L): if p is None: p = lambda x:x return filter(lambda x:not p(x), L) ifilterfalse = filterfalse imap = map def len(iterable): """Returns the length of an iterator.""" i = 0 for _ in iterable: i += 1 return i def trueCycle(iterable): while True: yielded = False for x in iterable: yield x yielded = True if not yielded: raise StopIteration def partition(p, iterable): """Partitions an iterable based on a predicate p. Returns a (yes,no) tuple""" no = [] yes = [] for elt in iterable: if p(elt): yes.append(elt) else: no.append(elt) return (yes, no) def any(p, iterable): """Returns true if any element in iterable satisfies predicate p.""" for elt in filter(p, iterable): return True else: return False def all(p, iterable): """Returns true if all elements in iterable satisfy predicate p.""" for elt in filterfalse(p, iterable): return False else: return True def choice(iterable): if isinstance(iterable, (list, tuple)): return random.choice(iterable) else: n = 1 found = False for x in iterable: if random.random() < 1/n: ret = x found = True n += 1 if not found: raise IndexError return ret def flatten(iterable, strings=False): """Flattens a list of lists into a single list. See the test for examples. """ for elt in iterable: if not strings and isinstance(elt, minisix.string_types): yield elt else: try: for x in flatten(elt): yield x except TypeError: yield elt def split(isSeparator, iterable, maxsplit=-1, yieldEmpty=False): """split(isSeparator, iterable, maxsplit=-1, yieldEmpty=False) Splits an iterator based on a predicate isSeparator.""" if isinstance(isSeparator, minisix.string_types): f = lambda s: s == isSeparator else: f = isSeparator acc = [] for element in iterable: if maxsplit == 0 or not f(element): acc.append(element) else: maxsplit -= 1 if acc or yieldEmpty: yield acc acc = [] if acc or yieldEmpty: yield acc def ilen(iterable): i = 0 for _ in iterable: i += 1 return i def startswith(long_, short): longI = iter(long_) shortI = iter(short) try: while True: if next(shortI) != next(longI): return False except StopIteration: return True def limited(iterable, limit): i = limit iterable = iter(iterable) try: while i: yield next(iterable) i -= 1 except StopIteration: raise ValueError('Expected %s elements in iterable (%r), got %s.' % \ (limit, iterable, limit-i)) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/gen.py0000644000175000017500000002633513233426066016402 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from __future__ import print_function import os import sys import ast import textwrap import warnings import functools import traceback import collections from . import crypt from .str import format from .file import mktemp from . import minisix from . import internationalization as _ def warn_non_constant_time(f): @functools.wraps(f) def newf(*args, **kwargs): # This method takes linear time whereas the subclass could probably # do it in constant time. warnings.warn('subclass of IterableMap does provide an efficient ' 'implementation of %s' % f.__name__, DeprecationWarning) return f(*args, **kwargs) return newf def abbrev(strings, d=None): """Returns a dictionary mapping unambiguous abbreviations to full forms.""" def eachSubstring(s): for i in range(1, len(s)+1): yield s[:i] if len(strings) != len(set(strings)): raise ValueError( 'strings given to utils.abbrev have duplicates: %r' % strings) if d is None: d = {} for s in strings: for abbreviation in eachSubstring(s): if abbreviation not in d: d[abbreviation] = s else: if abbreviation not in strings: d[abbreviation] = None removals = [] for key in d: if d[key] is None: removals.append(key) for key in removals: del d[key] return d def timeElapsed(elapsed, short=False, leadingZeroes=False, years=True, weeks=True, days=True, hours=True, minutes=True, seconds=True): """Given seconds, returns a string with an English description of the amount of time passed. leadingZeroes determines whether 0 days, 0 hours, etc. will be printed; the others determine what larger time periods should be used. """ ret = [] before = False def Format(s, i): if i or leadingZeroes or ret: if short: ret.append('%s%s' % (i, s[0])) else: ret.append(format('%n', (i, s))) elapsed = int(elapsed) # Handle negative times if elapsed < 0: before = True elapsed = -elapsed assert years or weeks or days or \ hours or minutes or seconds, 'One flag must be True' if years: (yrs, elapsed) = (elapsed // 31536000, elapsed % 31536000) Format(_('year'), yrs) if weeks: (wks, elapsed) = (elapsed // 604800, elapsed % 604800) Format(_('week'), wks) if days: (ds, elapsed) = (elapsed // 86400, elapsed % 86400) Format(_('day'), ds) if hours: (hrs, elapsed) = (elapsed // 3600, elapsed % 3600) Format(_('hour'), hrs) if minutes or seconds: (mins, secs) = (elapsed // 60, elapsed % 60) if leadingZeroes or mins: Format(_('minute'), mins) if seconds: leadingZeroes = True Format(_('second'), secs) if not ret: raise ValueError('Time difference not great enough to be noted.') result = '' if short: result = ' '.join(ret) else: result = format('%L', ret) if before: result = _('%s ago') % result return result def findBinaryInPath(s): """Return full path of a binary if it's in PATH, otherwise return None.""" cmdLine = None for dir in os.getenv('PATH').split(':'): filename = os.path.join(dir, s) if os.path.exists(filename): cmdLine = filename break return cmdLine def sortBy(f, L): """Uses the decorate-sort-undecorate pattern to sort L by function f.""" for (i, elt) in enumerate(L): L[i] = (f(elt), i, elt) L.sort() for (i, elt) in enumerate(L): L[i] = L[i][2] def saltHash(password, salt=None, hash='sha'): if salt is None: salt = mktemp()[:8] if hash == 'sha': hasher = crypt.sha elif hash == 'md5': hasher = crypt.md5 return '|'.join([salt, hasher((salt + password).encode('utf8')).hexdigest()]) _astStr2 = ast.Str if minisix.PY2 else ast.Bytes def safeEval(s, namespace=None): """Evaluates s, safely. Useful for turning strings into tuples/lists/etc. without unsafely using eval().""" try: node = ast.parse(s, mode='eval').body except SyntaxError as e: raise ValueError('Invalid string: %s.' % e) def checkNode(node): if node.__class__ is ast.Expr: node = node.value if node.__class__ in (ast.Num, ast.Str, _astStr2): return True elif node.__class__ in (ast.List, ast.Tuple): return all([checkNode(x) for x in node.elts]) elif node.__class__ is ast.Dict: return all([checkNode(x) for x in node.values]) and \ all([checkNode(x) for x in node.values]) elif node.__class__ is ast.Name: if namespace is None and node.id in ('True', 'False', 'None'): # For Python < 3.4, which does not have NameConstant. return True elif namespace is not None and node.id in namespace: return True else: return False elif sys.version_info[0:2] >= (3, 4) and \ node.__class__ is ast.NameConstant: return True else: return False if checkNode(node): if namespace is None: return eval(s, namespace, namespace) else: # Probably equivalent to eval() because checkNode(node) is True, # but it's an extra security. return ast.literal_eval(node) else: raise ValueError(format('Unsafe string: %q', s)) def exnToString(e): """Turns a simple exception instance into a string (better than str(e))""" strE = str(e) if strE: return '%s: %s' % (e.__class__.__name__, strE) else: return e.__class__.__name__ class IterableMap(object): """Define .items() in a class and subclass this to get the other iters. """ def items(self): if minisix.PY3 and hasattr(self, 'iteritems'): # For old plugins return self.iteritems() # avoid 2to3 else: raise NotImplementedError() __iter__ = items def keys(self): for (key, __) in self.items(): yield key def values(self): for (__, value) in self.items(): yield value @warn_non_constant_time def __len__(self): ret = 0 for __ in self.items(): ret += 1 return ret @warn_non_constant_time def __bool__(self): for __ in self.items(): return True return False __nonzero__ = __bool__ class InsensitivePreservingDict(collections.MutableMapping): def key(self, s): """Override this if you wish.""" if s is not None: s = s.lower() return s def __init__(self, dict=None, key=None): if key is not None: self.key = key self.data = {} if dict is not None: self.update(dict) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.data) def fromkeys(cls, keys, s=None, dict=None, key=None): d = cls(dict=dict, key=key) for key in keys: d[key] = s return d fromkeys = classmethod(fromkeys) def __getitem__(self, k): return self.data[self.key(k)][1] def __setitem__(self, k, v): self.data[self.key(k)] = (k, v) def __delitem__(self, k): del self.data[self.key(k)] def __iter__(self): return iter(self.data) def __len__(self): return len(self.data) def items(self): return self.data.values() def items(self): return self.data.values() def keys(self): L = [] for (k, __) in self.items(): L.append(k) return L def __reduce__(self): return (self.__class__, (dict(self.data.values()),)) class NormalizingSet(set): def __init__(self, iterable=()): iterable = list(map(self.normalize, iterable)) super(NormalizingSet, self).__init__(iterable) def normalize(self, x): return x def add(self, x): return super(NormalizingSet, self).add(self.normalize(x)) def remove(self, x): return super(NormalizingSet, self).remove(self.normalize(x)) def discard(self, x): return super(NormalizingSet, self).discard(self.normalize(x)) def __contains__(self, x): return super(NormalizingSet, self).__contains__(self.normalize(x)) has_key = __contains__ def stackTrace(frame=None, compact=True): if frame is None: frame = sys._getframe() if compact: L = [] while frame: lineno = frame.f_lineno funcname = frame.f_code.co_name filename = os.path.basename(frame.f_code.co_filename) L.append('[%s|%s|%s]' % (filename, funcname, lineno)) frame = frame.f_back return textwrap.fill(' '.join(L)) else: return traceback.format_stack(frame) def callTracer(fd=None, basename=True): if fd is None: fd = sys.stdout def tracer(frame, event, __): if event == 'call': code = frame.f_code lineno = frame.f_lineno funcname = code.co_name filename = code.co_filename if basename: filename = os.path.basename(filename) print('%s: %s(%s)' % (filename, funcname, lineno), file=fd) return tracer # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/file.py0000644000175000017500000002153213233426066016542 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import time import codecs import random import shutil import os.path from . import crypt def contents(filename): with open(filename) as fd: return fd.read() def open_mkdir(filename, mode='wb', *args, **kwargs): """filename -> file object. Returns a file object for filename, creating as many directories as may be necessary. I.e., if the filename is ./foo/bar/baz, and . exists, and ./foo exists, but ./foo/bar does not exist, bar will be created before opening baz in it. """ if mode not in ('w', 'wb'): raise ValueError('utils.file.open expects to write.') (dirname, basename) = os.path.split(filename) os.makedirs(dirname) return open(filename, mode, *args, **kwargs) def copy(src, dst): """src, dst -> None Copies src to dst, using this module's 'open' function to open dst. """ srcfd = open(src) dstfd = open_mkdir(dst, 'wb') shutil.copyfileobj(srcfd, dstfd) srcfd.close() dstfd.close() def writeLine(fd, line): fd.write(line) if not line.endswith('\n'): fd.write('\n') def readLines(filename): fd = open(filename) try: return [line.rstrip('\r\n') for line in fd.readlines()] finally: fd.close() def touch(filename): fd = open(filename, 'w') fd.close() def mktemp(suffix=''): """Gives a decent random string, suitable for a filename.""" r = random.Random() m = crypt.md5(suffix.encode('utf8')) r.seed(time.time()) s = str(r.getstate()) period = random.random() now = start = time.time() while start + period < now: time.sleep() # Induce a context switch, if possible. now = time.time() m.update(str(random.random())) m.update(s) m.update(str(now)) s = m.hexdigest() return crypt.sha((s + str(time.time())).encode('utf8')).hexdigest()+suffix def nonCommentLines(fd): for line in fd: if not line.startswith('#'): yield line def nonEmptyLines(fd): return filter(str.strip, fd) def nonCommentNonEmptyLines(fd): return nonEmptyLines(nonCommentLines(fd)) def chunks(fd, size): return iter(lambda : fd.read(size), '') ## chunk = fd.read(size) ## while chunk: ## yield chunk ## chunk = fd.read(size) class AtomicFile(object): """Used for files that need to be atomically written -- i.e., if there's a failure, the original file remains, unmodified. mode must be 'w' or 'wb'""" class default(object): # Holder for values. # Callables? tmpDir = None backupDir = None makeBackupIfSmaller = True allowEmptyOverwrite = True def __init__(self, filename, mode='w', allowEmptyOverwrite=None, makeBackupIfSmaller=None, tmpDir=None, backupDir=None, encoding=None): if tmpDir is None: tmpDir = force(self.default.tmpDir) if backupDir is None: backupDir = force(self.default.backupDir) if makeBackupIfSmaller is None: makeBackupIfSmaller = force(self.default.makeBackupIfSmaller) if allowEmptyOverwrite is None: allowEmptyOverwrite = force(self.default.allowEmptyOverwrite) if encoding is None and 'b' not in mode: encoding = 'utf8' if mode not in ('w', 'wb'): raise ValueError(format('Invalid mode: %q', mode)) self.rolledback = False self.allowEmptyOverwrite = allowEmptyOverwrite self.makeBackupIfSmaller = makeBackupIfSmaller self.filename = filename self.backupDir = backupDir if tmpDir is None: # If not given a tmpDir, we'll just put a random token on the end # of our filename and put it in the same directory. self.tempFilename = '%s.%s' % (self.filename, mktemp()) else: # If given a tmpDir, we'll get the basename (just the filename, no # directory), put our random token on the end, and put it in tmpDir tempFilename = '%s.%s' % (os.path.basename(self.filename), mktemp()) self.tempFilename = os.path.join(tmpDir, tempFilename) # This doesn't work because of the uncollectable garbage effect. # self.__parent = super(AtomicFile, self) self._fd = codecs.open(self.tempFilename, mode, encoding=encoding) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if exc_type: self.rollback() else: self.close() @property def closed(self): return self._fd.closed def write(self, data): return self._fd.write(data) def writelines(self, lines): return self._fd.writelines(lines) def rollback(self): if not self.closed: self._fd.close() if os.path.exists(self.tempFilename): os.remove(self.tempFilename) self.rolledback = True def seek(self, offset): return self._fd.seek(offset) def tell(self): return self._fd.tell() def flush(self): return self._fd.flush() def close(self): if not self.rolledback: self._fd.close() # We don't mind writing an empty file if the file we're overwriting # doesn't exist. newSize = os.path.getsize(self.tempFilename) originalExists = os.path.exists(self.filename) if newSize or self.allowEmptyOverwrite or not originalExists: if originalExists: oldSize = os.path.getsize(self.filename) if self.makeBackupIfSmaller and newSize < oldSize and \ self.backupDir != '/dev/null': now = int(time.time()) backupFilename = '%s.backup.%s' % (self.filename, now) if self.backupDir is not None: backupFilename = os.path.basename(backupFilename) backupFilename = os.path.join(self.backupDir, backupFilename) shutil.copy(self.filename, backupFilename) # We use shutil.move here instead of os.rename because # the latter doesn't work on Windows when self.filename # (the target) already exists. shutil.move handles those # intricacies for us. # This raises IOError if we can't write to the file. Since # in *nix, it only takes write perms to the *directory* to # rename a file (and shutil.move will use os.rename if # possible), we first check if we have the write permission # and only then do we write. fd = open(self.filename, 'a') fd.close() shutil.move(self.tempFilename, self.filename) else: raise ValueError('AtomicFile.close called after rollback.') def __del__(self): # We rollback because if we're deleted without being explicitly closed, # that's bad. We really should log this here, but as of yet we've got # no logging facility in utils. I've got some ideas for this, though. self.rollback() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/error.py0000644000175000017500000000363413233426066016757 0ustar valval00000000000000### # 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 os from . import gen class Error(Exception): def __init__(self, msg, e=None): self.msg = msg self.e = e def __str__(self): if self.e is not None: return os.linesep.join([self.msg, gen.exnToString(self.e)]) else: return self.msg # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: limnoria-2018.01.25/src/utils/crypt.py0000644000175000017500000000324113233426066016761 0ustar valval00000000000000### # Copyright (c) 2008, James McCoy # 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 hashlib import md5 from hashlib import sha1 as sha # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/utils/__init__.py0000644000175000017500000000504613233426066017364 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # 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 . import minisix ### # csv.{join,split} -- useful functions that should exist. ### import csv def join(L): fd = minisix.io.StringIO() writer = csv.writer(fd) writer.writerow(L) return fd.getvalue().rstrip('\r\n') def split(s): fd = minisix.io.StringIO(s) reader = csv.reader(fd) return next(reader) csv.join = join csv.split = split builtins = (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__) # We use this often enough that we're going to stick it in builtins. def force(x): if callable(x): return x() else: return x builtins['force'] = force internationalization = builtins.get('supybotInternationalization', None) # These imports need to happen below the block above, so things get put into # __builtins__ appropriately. from .gen import * from . import crypt, error, file, iter, net, python, seq, str, transaction, web # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/drivers/0000755000175000017500000000000013233426077015566 5ustar valval00000000000000limnoria-2018.01.25/src/drivers/__init__.py0000644000175000017500000001604013233426066017676 0ustar valval00000000000000### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008-2009, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Contains various drivers (network, file, and otherwise) for using IRC objects. """ import socket from .. import conf, ircmsgs, log as supylog, utils from ..utils import minisix _drivers = {} _deadDrivers = set() _newDrivers = [] class IrcDriver(object): """Base class for drivers.""" def __init__(self, *args, **kwargs): add(self.name(), self) super(IrcDriver, self).__init__(*args, **kwargs) def run(self): raise NotImplementedError def die(self): # The end of any overrided die method should be # "super(Class, self).die()", in order to make # sure this (and anything else later added) is done. remove(self.name()) def reconnect(self, wait=False): raise NotImplementedError def name(self): return repr(self) class ServersMixin(object): def __init__(self, irc, servers=()): self.networkGroup = conf.supybot.networks.get(irc.network) self.servers = servers super(ServersMixin, self).__init__() def _getServers(self): # We do this, rather than utils.iter.cycle the servers in __init__, # because otherwise registry updates given as setValues or sets # wouldn't be visible until a restart. return self.networkGroup.servers()[:] # Be sure to copy! def _getNextServer(self): if not self.servers: self.servers = self._getServers() assert self.servers, 'Servers value for %s is empty.' % \ self.networkGroup._name server = self.servers.pop(0) self.currentServer = '%s:%s' % server return server def empty(): """Returns whether or not the driver loop is empty.""" return (len(_drivers) + len(_newDrivers)) == 0 def add(name, driver): """Adds a given driver the loop with the given name.""" _newDrivers.append((name, driver)) def remove(name): """Removes the driver with the given name from the loop.""" _deadDrivers.add(name) def run(): """Runs the whole driver loop.""" for (name, driver) in _drivers.items(): try: if name not in _deadDrivers: driver.run() except: log.exception('Uncaught exception in in drivers.run:') _deadDrivers.add(name) for name in _deadDrivers: try: driver = _drivers[name] if hasattr(driver, 'irc') and driver.irc is not None: # The Schedule driver has no irc object, or it's None. driver.irc.driver = None driver.irc = None log.info('Removing driver %s.', name) del _drivers[name] except KeyError: pass while _newDrivers: (name, driver) = _newDrivers.pop() log.debug('Adding new driver %s.', name) _deadDrivers.discard(name) if name in _drivers: log.warning('Driver %s already added, killing it.', name) _drivers[name].die() del _drivers[name] _drivers[name] = driver class Log(object): """This is used to have a nice, consistent interface for drivers to use.""" def connect(self, server): self.info('Connecting to %s.', server) def connectError(self, server, e): if isinstance(e, Exception): if isinstance(e, socket.gaierror): e = e.args[1] else: e = utils.exnToString(e) self.warning('Error connecting to %s: %s', server, e) def disconnect(self, server, e=None): if e: if isinstance(e, Exception): e = utils.exnToString(e) else: e = str(e) if not e.endswith('.'): e += '.' self.warning('Disconnect from %s: %s', server, e) else: self.info('Disconnect from %s.', server) def reconnect(self, network, when=None): s = 'Reconnecting to %s' % network if when is not None: if not isinstance(when, minisix.string_types): when = self.timestamp(when) s += ' at %s.' % when else: s += '.' self.info(s) def die(self, irc): self.info('Driver for %s dying.', irc) debug = staticmethod(supylog.debug) info = staticmethod(supylog.info) warning = staticmethod(supylog.warning) error = staticmethod(supylog.warning) critical = staticmethod(supylog.critical) timestamp = staticmethod(supylog.timestamp) exception = staticmethod(supylog.exception) log = Log() def newDriver(irc, moduleName=None): """Returns a new driver for the given server using the irc given and using conf.supybot.driverModule to determine what driver to pick.""" # XXX Eventually this should be made to load the drivers from a # configurable directory in addition to the installed one. if moduleName is None: moduleName = conf.supybot.drivers.module() if moduleName == 'default': moduleName = 'supybot.drivers.Socket' elif not moduleName.startswith('supybot.drivers.'): moduleName = 'supybot.drivers.' + moduleName driverModule = __import__(moduleName, {}, {}, ['not empty']) log.debug('Creating new driver (%s) for %s.', moduleName, irc) driver = driverModule.Driver(irc) irc.driver = driver return driver def parseMsg(s): s = s.strip() if s: msg = ircmsgs.IrcMsg(s) return msg else: return None # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/drivers/Twisted.py0000644000175000017500000001406413233426066017566 0ustar valval00000000000000### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # 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 .. import conf, drivers from twisted.names import client from twisted.internet import reactor, error from twisted.protocols.basic import LineReceiver from twisted.internet.protocol import ReconnectingClientFactory # This hack prevents the standard Twisted resolver from starting any # threads, which allows for a clean shut-down in Twisted>=2.0 reactor.installResolver(client.createResolver()) try: from OpenSSL import SSL from twisted.internet import ssl except ImportError: drivers.log.debug('PyOpenSSL is not available, ' 'cannot connect to SSL servers.') SSL = None class TwistedRunnerDriver(drivers.IrcDriver): def name(self): return self.__class__.__name__ def run(self): try: reactor.iterate(conf.supybot.drivers.poll()) except: drivers.log.exception('Uncaught exception outside reactor:') class SupyIrcProtocol(LineReceiver): delimiter = '\n' MAX_LENGTH = 1024 def __init__(self): self.mostRecentCall = reactor.callLater(0.1, self.checkIrcForMsgs) def lineReceived(self, line): msg = drivers.parseMsg(line) if msg is not None: self.irc.feedMsg(msg) def checkIrcForMsgs(self): if self.connected: msg = self.irc.takeMsg() while msg: self.transport.write(str(msg)) msg = self.irc.takeMsg() self.mostRecentCall = reactor.callLater(0.1, self.checkIrcForMsgs) def connectionLost(self, r): self.mostRecentCall.cancel() if r.check(error.ConnectionDone): drivers.log.disconnect(self.factory.currentServer) else: drivers.log.disconnect(self.factory.currentServer, errorMsg(r)) if self.irc.zombie: self.factory.stopTrying() while self.irc.takeMsg(): continue else: self.irc.reset() def connectionMade(self): self.factory.resetDelay() self.irc.driver = self def die(self): drivers.log.die(self.irc) self.factory.stopTrying() self.transport.loseConnection() def reconnect(self, wait=None): # We ignore wait here, because we handled our own waiting. drivers.log.reconnect(self.irc.network) self.transport.loseConnection() def errorMsg(reason): return reason.getErrorMessage() class SupyReconnectingFactory(ReconnectingClientFactory, drivers.ServersMixin): maxDelay = property(lambda self: conf.supybot.drivers.maxReconnectWait()) protocol = SupyIrcProtocol def __init__(self, irc): drivers.log.warning('Twisted driver is deprecated. You should ' 'consider switching to Socket (set ' 'supybot.drivers.module to Socket).') self.irc = irc drivers.ServersMixin.__init__(self, irc) (server, port) = self._getNextServer() vhost = conf.supybot.protocols.irc.vhost() if self.networkGroup.get('ssl').value: self.connectSSL(server, port, vhost) else: self.connectTCP(server, port, vhost) def connectTCP(self, server, port, vhost): """Connect to the server with a standard TCP connection.""" reactor.connectTCP(server, port, self, bindAddress=(vhost, 0)) def connectSSL(self, server, port, vhost): """Connect to the server using an SSL socket.""" drivers.log.info('Attempting an SSL connection.') if SSL: reactor.connectSSL(server, port, self, ssl.ClientContextFactory(), bindAddress=(vhost, 0)) else: drivers.log.error('PyOpenSSL is not available. Not connecting.') def clientConnectionFailed(self, connector, r): drivers.log.connectError(self.currentServer, errorMsg(r)) (connector.host, connector.port) = self._getNextServer() ReconnectingClientFactory.clientConnectionFailed(self, connector,r) def clientConnectionLost(self, connector, r): (connector.host, connector.port) = self._getNextServer() ReconnectingClientFactory.clientConnectionLost(self, connector, r) def startedConnecting(self, connector): drivers.log.connect(self.currentServer) def buildProtocol(self, addr): protocol = ReconnectingClientFactory.buildProtocol(self, addr) protocol.irc = self.irc return protocol Driver = SupyReconnectingFactory poller = TwistedRunnerDriver() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/src/drivers/Socket.py0000644000175000017500000003721213233426066017373 0ustar valval00000000000000## # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010, 2013, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Contains simple socket drivers. Asyncore bugged (haha, pun!) me. """ from __future__ import division import os import time import errno import select import socket from .. import (conf, drivers, log, utils, world) from ..utils import minisix from ..utils.str import decode_raw_line try: import ssl SSLError = ssl.SSLError except: drivers.log.debug('ssl module is not available, ' 'cannot connect to SSL servers.') class SSLError(Exception): pass class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): _instances = [] _selecting = [False] # We want it to be mutable. def __init__(self, irc): self._instances.append(self) assert irc is not None self.irc = irc drivers.IrcDriver.__init__(self, irc) drivers.ServersMixin.__init__(self, irc) self.conn = None self._attempt = -1 self.servers = () self.eagains = 0 self.inbuffer = b'' self.outbuffer = '' self.zombie = False self.connected = False self.writeCheckTime = None self.nextReconnectTime = None self.resetDelay() if self.networkGroup.get('ssl').value and 'ssl' not in globals(): drivers.log.error('The Socket driver can not connect to SSL ' 'servers for your Python version. Try the ' 'Twisted driver instead, or install a Python' 'version that supports SSL (2.6 and greater).') self.ssl = False else: self.ssl = self.networkGroup.get('ssl').value self.connect() def getDelay(self): ret = self.currentDelay self.currentDelay = min(self.currentDelay * 2, conf.supybot.drivers.maxReconnectWait()) return ret def resetDelay(self): self.currentDelay = 10.0 def _getNextServer(self): oldServer = getattr(self, 'currentServer', None) server = drivers.ServersMixin._getNextServer(self) if self.currentServer != oldServer: self.resetDelay() return server def _handleSocketError(self, e): # (11, 'Resource temporarily unavailable') raised if connect # hasn't finished yet. We'll keep track of how many we get. if e.args[0] != 11 or self.eagains > 120: drivers.log.disconnect(self.currentServer, e) if self in self._instances: self._instances.remove(self) try: self.conn.close() except: pass self.connected = False self.scheduleReconnect() else: log.debug('Got EAGAIN, current count: %s.', self.eagains) self.eagains += 1 def _sendIfMsgs(self): if not self.connected: return if not self.zombie: msgs = [self.irc.takeMsg()] while msgs[-1] is not None: msgs.append(self.irc.takeMsg()) del msgs[-1] self.outbuffer += ''.join(map(str, msgs)) if self.outbuffer: try: if minisix.PY2: sent = self.conn.send(self.outbuffer) else: sent = self.conn.send(self.outbuffer.encode()) self.outbuffer = self.outbuffer[sent:] self.eagains = 0 except socket.error as e: self._handleSocketError(e) if self.zombie and not self.outbuffer: self._reallyDie() @classmethod def _select(cls): if cls._selecting[0]: return try: cls._selecting[0] = True for inst in cls._instances: # Do not use a list comprehension here, we have to edit the list # and not to reassign it. if not inst.connected or \ (minisix.PY3 and inst.conn._closed) or \ (minisix.PY2 and inst.conn._sock.__class__ is socket._closedsocket): cls._instances.remove(inst) elif inst.conn.fileno() == -1: inst.reconnect() if not cls._instances: return rlist, wlist, xlist = select.select([x.conn for x in cls._instances], [], [], conf.supybot.drivers.poll()) for instance in cls._instances: if instance.conn in rlist: instance._read() except select.error as e: if e.args[0] != errno.EINTR: # 'Interrupted system call' raise finally: cls._selecting[0] = False for instance in cls._instances: if instance.irc and not instance.irc.zombie: instance._sendIfMsgs() def run(self): now = time.time() if self.nextReconnectTime is not None and now > self.nextReconnectTime: self.reconnect() elif self.writeCheckTime is not None and now > self.writeCheckTime: self._checkAndWriteOrReconnect() if not self.connected: # We sleep here because otherwise, if we're the only driver, we'll # spin at 100% CPU while we're disconnected. time.sleep(conf.supybot.drivers.poll()) return self._sendIfMsgs() self._select() def _read(self): """Called by _select() when we can read data.""" try: self.inbuffer += self.conn.recv(1024) self.eagains = 0 # If we successfully recv'ed, we can reset this. lines = self.inbuffer.split(b'\n') self.inbuffer = lines.pop() for line in lines: line = decode_raw_line(line) msg = drivers.parseMsg(line) if msg is not None and self.irc is not None: self.irc.feedMsg(msg) except socket.timeout: pass except SSLError as e: if e.args[0] == 'The read operation timed out': pass else: self._handleSocketError(e) return except socket.error as e: self._handleSocketError(e) return if self.irc and not self.irc.zombie: self._sendIfMsgs() def connect(self, **kwargs): self.reconnect(reset=False, **kwargs) def reconnect(self, wait=False, reset=True): self._attempt += 1 self.nextReconnectTime = None if self.connected: drivers.log.reconnect(self.irc.network) if self in self._instances: self._instances.remove(self) try: self.conn.shutdown(socket.SHUT_RDWR) except: # "Transport endpoint not connected" pass self.conn.close() self.connected = False if reset: drivers.log.debug('Resetting %s.', self.irc) self.irc.reset() else: drivers.log.debug('Not resetting %s.', self.irc) if wait: self.scheduleReconnect() return self.server = self._getNextServer() network_config = getattr(conf.supybot.networks, self.irc.network) socks_proxy = network_config.socksproxy() try: if socks_proxy: import socks except ImportError: log.error('Cannot use socks proxy (SocksiPy not installed), ' 'using direct connection instead.') socks_proxy = '' if socks_proxy: address = self.server[0] else: try: address = utils.net.getAddressFromHostname(self.server[0], attempt=self._attempt) except (socket.gaierror, socket.error) as e: drivers.log.connectError(self.currentServer, e) self.scheduleReconnect() return port = self.server[1] drivers.log.connect(self.currentServer) try: self.conn = utils.net.getSocket(address, port=port, socks_proxy=socks_proxy, vhost=conf.supybot.protocols.irc.vhost(), vhostv6=conf.supybot.protocols.irc.vhostv6(), ) except socket.error as e: drivers.log.connectError(self.currentServer, e) self.scheduleReconnect() return # We allow more time for the connect here, since it might take longer. # At least 10 seconds. self.conn.settimeout(max(10, conf.supybot.drivers.poll()*10)) try: # Connect before SSL, otherwise SSL is disabled if we use SOCKS. # See http://stackoverflow.com/q/16136916/539465 self.conn.connect((address, port)) if network_config.ssl(): self.starttls() elif not network_config.requireStarttls(): drivers.log.warning(('Connection to network %s ' 'does not use SSL/TLS, which makes it vulnerable to ' 'man-in-the-middle attacks and passive eavesdropping. ' 'You should consider upgrading your connection to SSL/TLS ' '') % self.irc.network) def setTimeout(): self.conn.settimeout(conf.supybot.drivers.poll()) conf.supybot.drivers.poll.addCallback(setTimeout) setTimeout() self.connected = True self.resetDelay() except socket.error as e: if e.args[0] == 115: now = time.time() when = now + 60 whenS = log.timestamp(when) drivers.log.debug('Connection in progress, scheduling ' 'connectedness check for %s', whenS) self.writeCheckTime = when else: drivers.log.connectError(self.currentServer, e) self.scheduleReconnect() return self._instances.append(self) def _checkAndWriteOrReconnect(self): self.writeCheckTime = None drivers.log.debug('Checking whether we are connected.') (_, w, _) = select.select([], [self.conn], [], 0) if w: drivers.log.debug('Socket is writable, it might be connected.') self.connected = True self.resetDelay() else: drivers.log.connectError(self.currentServer, 'Timed out') self.reconnect() def scheduleReconnect(self): when = time.time() + self.getDelay() if not world.dying: drivers.log.reconnect(self.irc.network, when) if self.nextReconnectTime: drivers.log.error('Updating next reconnect time when one is ' 'already present. This is a bug; please ' 'report it, with an explanation of what caused ' 'this to happen.') self.nextReconnectTime = when def die(self): if self in self._instances: self._instances.remove(self) self.zombie = True if self.nextReconnectTime is not None: self.nextReconnectTime = None if self.writeCheckTime is not None: self.writeCheckTime = None drivers.log.die(self.irc) def _reallyDie(self): if self.conn is not None: self.conn.close() drivers.IrcDriver.die(self) # self.irc.die() Kill off the ircs yourself, jerk! def name(self): return '%s(%s)' % (self.__class__.__name__, self.irc) def starttls(self): assert 'ssl' in globals() network_config = getattr(conf.supybot.networks, self.irc.network) certfile = network_config.certfile() if not certfile: certfile = conf.supybot.protocols.irc.certfile() if not certfile: certfile = None elif not os.path.isfile(certfile): drivers.log.warning('Could not find cert file %s.' % certfile) certfile = None verifyCertificates = conf.supybot.protocols.ssl.verifyCertificates() if not verifyCertificates: drivers.log.warning('Not checking SSL certificates, connections ' 'are vulnerable to man-in-the-middle attacks. Set ' 'supybot.protocols.ssl.verifyCertificates to "true" ' 'to enable validity checks.') try: self.conn = utils.net.ssl_wrap_socket(self.conn, logger=drivers.log, hostname=self.server[0], certfile=certfile, verify=verifyCertificates, trusted_fingerprints=network_config.ssl.serverFingerprints(), ca_file=network_config.ssl.authorityCertificate(), ) except getattr(ssl, 'CertificateError', None) as e: # Default to None for old Python version, which do not have # CertificateError drivers.log.error(('Certificate validation failed when ' 'connecting to %s: %s\n' 'This means either someone is doing a man-in-the-middle ' 'attack on your connection, or the server\'s certificate is ' 'not in your trusted fingerprints list.') % (self.irc.network, e.args[0])) raise ssl.SSLError('Aborting because of failed certificate ' 'verification.') except ssl.SSLError as e: drivers.log.error(('Certificate validation failed when ' 'connecting to %s: %s\n' 'This means either someone is doing a man-in-the-middle ' 'attack on your connection, or the server\'s ' 'certificate is not trusted.') % (self.irc.network, e.args[1])) raise ssl.SSLError('Aborting because of failed certificate ' 'verification.') Driver = SocketDriver # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/scripts/0000755000175000017500000000000013233426077015010 5ustar valval00000000000000limnoria-2018.01.25/scripts/supybot-wizard0000644000175000017500000010476513233426066017751 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2003-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from __future__ import print_function import os import sys def error(s): sys.stderr.write(s) if not s.endswith(os.linesep): sys.stderr.write(os.linesep) sys.exit(-1) if sys.version_info < (2, 6, 0): error('This program requires Python >= 2.6.0') import supybot import re import time import pydoc import pprint import socket import logging import optparse try: import supybot.i18n as i18n except ImportError: sys.stderr.write("""Error: You are running a mix of Limnoria and stock Supybot code. Although you run one of Limnoria\'s executables, Python tries to load stock Supybot\'s library. To fix this issue, uninstall Supybot ("%s -m pip uninstall supybot" should do the job) and install Limnoria again. For your information, Supybot's libraries are installed here: %s\n""" % (sys.executable, '\n '.join(supybot.__path__))) exit(-1) import supybot.ansi as ansi import supybot.utils as utils import supybot.ircutils as ircutils import supybot.registry as registry # supybot.plugin, supybot.log, and supybot.conf will be imported later, # because we need to set a language before loading the conf import supybot.questions as questions from supybot.questions import output, yn, anything, something, expect, getpass def getPlugins(pluginDirs): plugins = set([]) join = os.path.join for pluginDir in pluginDirs: try: for filename in os.listdir(pluginDir): fname = join(pluginDir, filename) if (filename.endswith('.py') or os.path.isdir(fname)) \ and filename[0].isupper(): plugins.add(os.path.splitext(filename)[0]) except OSError: continue plugins.discard('Owner') plugins = list(plugins) plugins.sort() return plugins def loadPlugin(name): import supybot.plugin as plugin try: module = plugin.loadPluginModule(name) if hasattr(module, 'Class'): return module else: output("""That plugin loaded fine, but didn't seem to be a real Supybot plugin; there was no Class variable to tell us what class to load when we load the plugin. We'll skip over it for now, but you can always add it later.""") return None except Exception as e: output("""We encountered a bit of trouble trying to load plugin %r. Python told us %r. We'll skip over it for now, you can always add it later.""" % (name, utils.gen.exnToString(e))) return None def describePlugin(module, showUsage): if module.__doc__: output(module.__doc__, unformatted=False) elif hasattr(module.Class, '__doc__'): output(module.Class.__doc__, unformatted=False) else: output("""Unfortunately, this plugin doesn't seem to have any documentation. Sorry about that.""") if showUsage: if hasattr(module, 'example'): if yn('This plugin has a usage example. ' 'Would you like to see it?', default=False): pydoc.pager(module.example) else: output("""This plugin has no usage example.""") def clearLoadedPlugins(plugins, pluginRegistry): for plugin in plugins: try: pluginKey = pluginRegistry.get(plugin) if pluginKey(): plugins.remove(plugin) except registry.NonExistentRegistryEntry: continue _windowsVarRe = re.compile(r'%(\w+)%') def getDirectoryName(default, basedir=os.curdir, prompt=True): done = False while not done: if prompt: dir = something('What directory do you want to use?', default=os.path.join(basedir, default)) else: dir = os.path.join(basedir, default) orig_dir = dir dir = os.path.expanduser(dir) dir = _windowsVarRe.sub(r'$\1', dir) dir = os.path.expandvars(dir) dir = os.path.abspath(dir) try: os.makedirs(dir) done = True except OSError as e: # 17 is File exists for Linux (and likely other POSIX systems) # 183 is the same for Windows if e.args[0] == 17 or (os.name == 'nt' and e.args[0] == 183): done = True else: output("""Sorry, I couldn't make that directory for some reason. The Operating System told me %s. You're going to have to pick someplace else.""" % e) prompt = True return (dir, os.path.dirname(orig_dir)) def main(): import supybot.version as version parser = optparse.OptionParser(usage='Usage: %prog [options]', version='Supybot %s' % version.version) parser.add_option('', '--allow-root', action='store_true', dest='allowRoot', help='Determines whether the wizard will be allowed to ' 'run as root. You don\'t want this. Don\'t do it.' ' Even if you think you want it, you don\'t. ' 'You\'re probably dumb if you do this.') parser.add_option('', '--allow-home', action='store_true', dest='allowHome', help='Determines whether the wizard will be allowed to ' 'run directly in the HOME directory. ' 'You should not do this unless you want it to ' 'create multiple files in your HOME directory.') parser.add_option('', '--no-network', action='store_false', dest='network', help='Determines whether the wizard will be allowed to ' 'run without a network connection.') (options, args) = parser.parse_args() if os.name == 'posix': if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot: error('Please, don\'t run this as root.') if os.name == 'posix': if (os.getcwd() == os.path.expanduser('~')) and not options.allowHome: error('Please, don\'t run this in your HOME directory.') if os.path.isfile(os.path.join('scripts', 'supybot-wizard')) or \ os.path.isfile(os.path.join('..', 'scripts', 'supybot-wizard')): print('') print('+------------------------------------------------------------+') print('| +--------------------------------------------------------+ |') print('| | Warning: It looks like you are running the wizard from | |') print('| | the Supybot source directory. This is not recommended. | |') print('| | Please press Ctrl-C and change to another directory. | |') print('| +--------------------------------------------------------+ |') print('+------------------------------------------------------------+') print('') if args: parser.error('This program takes no non-option arguments.') output("""This is a wizard to help you start running supybot. What it will do is create the necessary config files based on the options you select here. So hold on tight and be ready to be interrogated :)""") output("""First of all, we can bold the questions you're asked so you can easily distinguish the mostly useless blather (like this) from the questions that you actually have to answer.""") if yn('Would you like to try this bolding?', default=True): questions.useBold = True if not yn('Do you see this in bold?'): output("""Sorry, it looks like your terminal isn't ANSI compliant. Try again some other day, on some other terminal :)""") questions.useBold = False else: output("""Great!""") ### # Preliminary questions. ### output("""We've got some preliminary things to get out of the way before we can really start asking you questions that directly relate to what your bot is going to be like.""") # Advanced? output("""We want to know if you consider yourself an advanced Supybot user because some questions are just utterly boring and useless for new users. Others might not make sense unless you've used Supybot for some time.""") advanced = yn('Are you an advanced Supybot user?', default=False) # Language? output("""This version of Supybot (known as Limnoria) includes another language. This can be changed at any time. You need to answer with a short id for the language, such as 'en', 'fr', 'it' (without the quotes). If you want to use English, just press enter.""") language = something('What language do you want to use?', default='en') class Empty: """This is a hack to allow the i18n to get the current language, before loading the conf module, before the conf module needs i18n to set the default strings.""" def __call__(self): return self.value fakeConf = Empty() fakeConf.supybot = Empty() fakeConf.supybot.language = Empty() fakeConf.supybot.language.value = language i18n.conf = fakeConf i18n.currentLocale = language i18n.reloadLocales() import supybot.conf as conf i18n.import_conf() # It imports the real conf module ### Directories. # We set these variables in cache because otherwise conf and log will # create directories for the default values, which might not be what the # user wants. if advanced: output("""Now we've got to ask you some questions about where some of your directories are (or, perhaps, will be :)). If you're running this wizard from the directory you'll actually be starting your bot from and don't mind creating some directories in the current directory, then just don't give answers to these questions and we'll create the directories we need right here in this directory.""") # conf.supybot.directories.log output("""Your bot will need to put its logs somewhere. Do you have any specific place you'd like them? If not, just press enter and we'll make a directory named "logs" right here.""") (logDir, basedir) = getDirectoryName('logs') conf.supybot.directories.log.setValue(logDir) import supybot.log as log log._stdoutHandler.setLevel(100) # *Nothing* gets through this! # conf.supybot.directories.data output("""Your bot will need to put various data somewhere. Things like databases, downloaded files, etc. Do you have any specific place you'd like the bot to put these things? If not, just press enter and we'll make a directory named "data" right here.""") (dataDir, basedir) = getDirectoryName('data', basedir=basedir) conf.supybot.directories.data.setValue(dataDir) # conf.supybot.directories.conf output("""Your bot must know where to find its configuration files. It'll probably only make one or two, but it's gotta have some place to put them. Where should that place be? If you don't care, just press enter and we'll make a directory right here named "conf" where it'll store its stuff. """) (confDir, basedir) = getDirectoryName('conf', basedir=basedir) conf.supybot.directories.conf.setValue(confDir) # conf.supybot.directories.backup output("""Your bot must know where to place backups of its conf and data files. Where should that place be? If you don't care, just press enter and we'll make a directory right here named "backup" where it'll store its stuff.""") (backupDir, basedir) = getDirectoryName('backup', basedir=basedir) conf.supybot.directories.backup.setValue(backupDir) # conf.supybot.directories.data.tmp output("""Your bot needs a directory to put temporary files (used mainly to atomically update its configuration files).""") (tmpDir, basedir) = getDirectoryName('tmp', basedir=basedir) conf.supybot.directories.data.tmp.setValue(tmpDir) # conf.supybot.directories.data.web output("""Your bot needs a directory to put files related to the web server (templates, CSS).""") (webDir, basedir) = getDirectoryName('web', basedir=basedir) conf.supybot.directories.data.web.setValue(webDir) # imports callbacks, which imports ircdb, which requires # directories.conf import supybot.plugin as plugin # pluginDirs output("""Your bot will also need to know where to find its plugins at. Of course, it already knows where the plugins that it came with are, but your own personal plugins that you write for will probably be somewhere else.""") pluginDirs = conf.supybot.directories.plugins() output("""Currently, the bot knows about the following directories:""") output(format('%L', pluginDirs + [plugin._pluginsDir])) while yn('Would you like to add another plugin directory? ' 'Adding a local plugin directory is good style.', default=True): (pluginDir, _) = getDirectoryName('plugins', basedir=basedir) if pluginDir not in pluginDirs: pluginDirs.append(pluginDir) conf.supybot.directories.plugins.setValue(pluginDirs) else: output("""Your bot needs to create some directories in order to store the various log, config, and data files.""") basedir = something("""Where would you like to create these directories?""", default=os.curdir) # conf.supybot.directories.log (logDir, basedir) = getDirectoryName('logs', basedir=basedir, prompt=False) conf.supybot.directories.log.setValue(logDir) # conf.supybot.directories.data (dataDir, basedir) = getDirectoryName('data', basedir=basedir, prompt=False) conf.supybot.directories.data.setValue(dataDir) (tmpDir, basedir) = getDirectoryName('tmp', basedir=basedir, prompt=False) conf.supybot.directories.data.tmp.setValue(tmpDir) (webDir, basedir) = getDirectoryName('web', basedir=basedir, prompt=False) conf.supybot.directories.data.web.setValue(webDir) # conf.supybot.directories.conf (confDir, basedir) = getDirectoryName('conf', basedir=basedir, prompt=False) conf.supybot.directories.conf.setValue(confDir) # conf.supybot.directories.backup (backupDir, basedir) = getDirectoryName('backup', basedir=basedir, prompt=False) conf.supybot.directories.backup.setValue(backupDir) # pluginDirs pluginDirs = conf.supybot.directories.plugins() (pluginDir, _) = getDirectoryName('plugins', basedir=basedir, prompt=False) if pluginDir not in pluginDirs: pluginDirs.append(pluginDir) conf.supybot.directories.plugins.setValue(pluginDirs) import supybot.log as log log._stdoutHandler.setLevel(100) # *Nothing* gets through this! import supybot.plugin as plugin output("Good! We're done with the directory stuff.") ### # Bot stuff ### output("""Now we're going to ask you things that actually relate to the bot you'll be running.""") network = None while not network: output("""First, we need to know the name of the network you'd like to connect to. Not the server host, mind you, but the name of the network. If you plan to connect to chat.freenode.net, for instance, you should answer this question with 'freenode' (without the quotes). """) network = something('What IRC network will you be connecting to?') if '.' in network: output("""There shouldn't be a '.' in the network name. Remember, this is the network name, not the actual server you plan to connect to.""") network = None elif not registry.isValidRegistryName(network): output("""That's not a valid name for one reason or another. Please pick a simpler name, one more likely to be valid.""") network = None conf.supybot.networks.setValue([network]) network = conf.registerNetwork(network) defaultServer = None server = None ip = None while not ip: serverString = something('What server would you like to connect to?', default=defaultServer) if options.network: try: output("""Looking up %s...""" % serverString) ip = socket.gethostbyname(serverString) except: output("""Sorry, I couldn't find that server. Perhaps you misspelled it? Also, be sure not to put the port in the server's name -- we'll ask you about that later.""") else: ip = 'no network available' output("""Found %s (%s).""" % (serverString, ip)) # conf.supybot.networks..ssl output("""Most networks allow you to use a secure connection via SSL. If you are not sure whether this network supports SSL or not, check its website.""") use_ssl = yn('Do you want to use an SSL connection?', default=True) network.ssl.setValue(use_ssl) output("""IRC servers almost always accept connections on port 6697 (or 6667 when not using SSL). They can, however, accept connections anywhere their admins feel like they want to accept connections from.""") if yn('Does this server require connection on a non-standard port?', default=False): port = 0 while not port: port = something('What port is that?') try: i = int(port) if not (0 < i < 65536): raise ValueError() except ValueError: output("""That's not a valid port.""") port = 0 else: if network.ssl.value: port = 6697 else: port = 6667 server = ':'.join([serverString, str(port)]) network.servers.setValue([server]) # conf.supybot.nick # Force the user into specifying a nick if it didn't have one already while True: nick = something('What nick would you like your bot to use?', default=None) try: conf.supybot.nick.set(nick) break except registry.InvalidRegistryValue: output("""That's not a valid nick. Go ahead and pick another.""") # conf.supybot.user if advanced: output("""If you've ever done a /whois on a person, you know that IRC provides a way for users to show the world their full name. What would you like your bot's full name to be? If you don't care, just press enter and it'll be the same as your bot's nick.""") user = '' user = something('What would you like your bot\'s full name to be?', default=nick) conf.supybot.user.set(user) # conf.supybot.ident (if advanced) defaultIdent = 'limnoria' if advanced: output("""IRC servers also allow you to set your ident, which they might need if they can't find your identd server. What would you like your ident to be? If you don't care, press enter and we'll use 'limnoria'. In fact, we prefer that you do this, because it provides free advertising for Supybot when users /whois your bot. But, of course, it's your call.""") while True: ident = something('What would you like your bot\'s ident to be?', default=defaultIdent) try: conf.supybot.ident.set(ident) break except registry.InvalidRegistryValue: output("""That was not a valid ident. Go ahead and pick another.""") else: conf.supybot.ident.set(defaultIdent) # conf.supybot.networks..password output("""Some servers require a password to connect to them. Most public servers don't. If you try to connect to a server and for some reason it just won't work, it might be that you need to set a password.""") if yn('Do you want to set such a password?', default=False): network.password.set(getpass()) # conf.supybot.networks..channels output("""Of course, having an IRC bot isn't the most useful thing in the world unless you can make that bot join some channels.""") if yn('Do you want your bot to join some channels when it connects?', default=True): defaultChannels = ' '.join(network.channels()) output("""Separate channels with spaces. If the channel is locked with a key, follow the channel name with the key separated by a comma. For example: #supybot-bots #mychannel,mykey #otherchannel"""); while True: channels = something('What channels?', default=defaultChannels) try: network.channels.set(channels) break except registry.InvalidRegistryValue as e: output(""""%s" is an invalid IRC channel. Be sure to prefix the channel with # (or +, or !, or &, but no one uses those channels, really). Be sure the channel key (if you are supplying one) does not contain a comma.""" % e.channel) else: network.channels.setValue([]) ### # Plugins ### def configurePlugin(module, advanced): if hasattr(module, 'configure'): output("""Beginning configuration for %s...""" % module.Class.__name__) module.configure(advanced) print() # Blank line :) output("""Done!""") else: conf.registerPlugin(module.__name__, currentValue=True) plugins = getPlugins(pluginDirs + [plugin._pluginsDir]) for s in ('Admin', 'User', 'Channel', 'Misc', 'Config', 'Utilities'): m = loadPlugin(s) if m is not None: configurePlugin(m, advanced) else: error('There was an error loading one of the core plugins that ' 'under almost all circumstances are loaded. Go ahead and ' 'fix that error and run this script again.') clearLoadedPlugins(plugins, conf.supybot.plugins) output("""Now we're going to run you through plugin configuration. There's a variety of plugins in supybot by default, but you can create and add your own, of course. We'll allow you to take a look at the known plugins' descriptions and configure them if you like what you see.""") # bulk addedBulk = False if advanced and yn('Would you like to add plugins en masse first?'): addedBulk = True output(format("""The available plugins are: %L.""", plugins)) output("""What plugins would you like to add? If you've changed your mind and would rather not add plugins in bulk like this, just press enter and we'll move on to the individual plugin configuration. We suggest you to add Aka, Ctcp, Later, Network, Plugin, String, and Utilities""") massPlugins = anything('Separate plugin names by spaces or commas:') for name in re.split(r',?\s+', massPlugins): module = loadPlugin(name) if module is not None: configurePlugin(module, advanced) clearLoadedPlugins(plugins, conf.supybot.plugins) # individual if yn('Would you like to look at plugins individually?'): output("""Next comes your opportunity to learn more about the plugins that are available and select some (or all!) of them to run in your bot. Before you have to make a decision, of course, you'll be able to see a short description of the plugin and, if you choose, an example session with the plugin. Let's begin.""") # until we get example strings again, this will default to false #showUsage =yn('Would you like the option of seeing usage examples?') showUsage = False name = expect('What plugin would you like to look at?', plugins, acceptEmpty=True) while name: module = loadPlugin(name) if module is not None: describePlugin(module, showUsage) if yn('Would you like to load this plugin?', default=True): configurePlugin(module, advanced) clearLoadedPlugins(plugins, conf.supybot.plugins) if not yn('Would you like add another plugin?'): break name = expect('What plugin would you like to look at?', plugins) ### # Sundry ### output("""Although supybot offers a supybot-adduser script, with which you can add users to your bot's user database, it's *very* important that you have an owner user for you bot.""") if yn('Would you like to add an owner user for your bot?', default=True): import supybot.ircdb as ircdb name = something('What should the owner\'s username be?') try: id = ircdb.users.getUserId(name) u = ircdb.users.getUser(id) if u._checkCapability('owner'): output("""That user already exists, and has owner capabilities already. Perhaps you added it before? """) if yn('Do you want to remove its owner capability?', default=False): u.removeCapability('owner') ircdb.users.setUser(id, u) else: output("""That user already exists, but doesn't have owner capabilities.""") if yn('Do you want to add to it owner capabilities?', default=False): u.addCapability('owner') ircdb.users.setUser(id, u) except KeyError: password = getpass('What should the owner\'s password be?') u = ircdb.users.newUser() u.name = name u.setPassword(password) u.addCapability('owner') ircdb.users.setUser(u) output("""Of course, when you're in an IRC channel you can address the bot by its nick and it will respond, if you give it a valid command (it may or may not respond, depending on what your config variable replyWhenNotCommand is set to). But your bot can also respond to a short "prefix character," so instead of saying "bot: do this," you can say, "@do this" and achieve the same effect. Of course, you don't *have* to have a prefix char, but if the bot ends up participating significantly in your channel, it'll ease things.""") if yn('Would you like to set the prefix char(s) for your bot? ', default=True): output("""Enter any characters you want here, but be careful: they should be rare enough that people don't accidentally address the bot (simply because they'll probably be annoyed if they do address the bot on accident). You can even have more than one. I (jemfinch) am quite partial to @, but that's because I've been using it since my ocamlbot days.""") import supybot.callbacks as callbacks c = '' while not c: try: c = anything('What would you like your bot\'s prefix ' 'character(s) to be?') conf.supybot.reply.whenAddressedBy.chars.set(c) except registry.InvalidRegistryValue as e: output(str(e)) c = '' else: conf.supybot.reply.whenAddressedBy.chars.set('') ### # logging variables. ### if advanced: # conf.supybot.log.stdout output("""By default, your bot will log not only to files in the logs directory you gave it, but also to stdout. We find this useful for debugging, and also just for the pretty output (it's colored!)""") stdout = not yn('Would you like to turn off this logging to stdout?', default=False) conf.supybot.log.stdout.setValue(stdout) if conf.supybot.log.stdout(): # conf.something output("""Some terminals may not be able to display the pretty colors logged to stderr. By default, though, we turn the colors off for Windows machines and leave it on for *nix machines.""") if os.name is not 'nt': conf.supybot.log.stdout.colorized.setValue( not yn('Would you like to turn this colorization off?', default=False)) # conf.supybot.log.level output("""Your bot can handle debug messages at several priorities, CRITICAL, ERROR, WARNING, INFO, and DEBUG, in decreasing order of priority. By default, your bot will log all of these priorities except DEBUG. You can, however, specify that it only log messages above a certain priority level.""") priority = str(conf.supybot.log.level) logLevel = something('What would you like the minimum priority to be?' ' Just press enter to accept the default.', default=priority).lower() while logLevel not in ['debug','info','warning','error','critical']: output("""That's not a valid priority. Valid priorities include 'DEBUG', 'INFO', 'WARNING', 'ERROR', and 'CRITICAL'""") logLevel = something('What would you like the minimum priority to ' 'be? Just press enter to accept the default.', default=priority).lower() conf.supybot.log.level.set(logLevel) # conf.supybot.databases.plugins.channelSpecific output("""Many plugins in Supybot are channel-specific. Their databases, likewise, are specific to each channel the bot is in. Many people don't want this, so we have one central location in which to say that you would prefer all databases for all channels to be shared. This variable, supybot.databases.plugins.channelSpecific, is that place.""") conf.supybot.databases.plugins.channelSpecific.setValue( not yn('Would you like plugin databases to be shared by all ' 'channels, rather than specific to each channel the ' 'bot is in?')) output("""There are a lot of options we didn't ask you about simply because we'd rather you get up and running and have time left to play around with your bot. But come back and see us! When you've played around with your bot enough to know what you like, what you don't like, what you'd like to change, then take a look at your configuration file when your bot isn't running and read the comments, tweaking values to your heart's desire.""") # Let's make sure that src/ plugins are loaded. conf.registerPlugin('Admin', True) conf.registerPlugin('AutoMode', True) conf.registerPlugin('Channel', True) conf.registerPlugin('Config', True) conf.registerPlugin('Misc', True) conf.registerPlugin('Network', True) conf.registerPlugin('NickAuth', True) conf.registerPlugin('User', True) conf.registerPlugin('Utilities', True) ### # Write the registry ### # We're going to need to do a darcs predist thing here. #conf.supybot.debug.generated.setValue('...') if advanced: basedir = '.' filename = os.path.join(basedir, '%s.conf') filename = something("""In which file would you like to save this config?""", default=filename % nick) if not filename.endswith('.conf'): filename += '.conf' registry.close(conf.supybot, os.path.expanduser(filename)) # Done! output("""All done! Your new bot configuration is %s. If you're running a *nix based OS, you can probably start your bot with the command line "supybot %s". If you're not running a *nix or similar machine, you'll just have to start it like you start all your other Python scripts.""" % \ (filename, filename)) if __name__ == '__main__': try: main() except KeyboardInterrupt: # We may still be using bold text when exiting during a prompt if questions.useBold: import supybot.ansi as ansi print(ansi.RESET) print() print() output("""Well, it looks like you canceled out of the wizard before it was done. Unfortunately, I didn't get to write anything to file. Please run the wizard again to completion.""") # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/scripts/supybot-test0000644000175000017500000002172313233426066017420 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2011, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import sys import time import shutil started = time.time() import supybot import logging import traceback # We need to do this before we import conf. if not os.path.exists('test-conf'): os.mkdir('test-conf') registryFilename = os.path.join('test-conf', 'test.conf') fd = open(registryFilename, 'w') fd.write(""" supybot.directories.data: %(base_dir)s/test-data supybot.directories.conf: %(base_dir)s/test-conf supybot.directories.log: %(base_dir)s/test-logs supybot.reply.whenNotCommand: True supybot.log.stdout: False supybot.log.stdout.level: ERROR supybot.log.level: DEBUG supybot.log.format: %%(levelname)s %%(message)s supybot.log.plugins.individualLogfiles: False supybot.protocols.irc.throttleTime: 0 supybot.reply.whenAddressedBy.chars: @ supybot.networks.test.server: should.not.need.this supybot.nick: test supybot.databases.users.allowUnregistration: True """ % {'base_dir': os.getcwd()}) fd.close() import supybot.registry as registry registry.open_registry(registryFilename) import supybot.log as log import supybot.conf as conf conf.allowEval = True conf.supybot.flush.setValue(False) import re import sys import glob import atexit import os.path import unittest import supybot.utils as utils import supybot.world as world import supybot.callbacks as callbacks world.startedAt = started import logging class TestLogFilter(logging.Filter): bads = [ 'No callbacks in', 'Invalid channel database', 'Exact error', 'Invalid user dictionary', 'because of noFlush', 'Queuing NICK', 'Queuing USER', 'IgnoresDB.reload failed', 'Starting log for', 'Irc object for test dying', 'Last Irc,', ] def filter(self, record): for bad in self.bads: if bad in record.msg: return False return True log._logger.addFilter(TestLogFilter()) class path(str): """A class to represent platform-independent paths.""" _r = re.compile(r'[\\/]') def __hash__(self): return reduce(lambda h, s: h ^ hash(s), self._r.split(self), 0) def __eq__(self, other): return self._r.split(self) == self._r.split(other) if __name__ == '__main__': import glob import os.path import optparse import supybot.test as test import supybot.plugin as plugin parser = optparse.OptionParser(usage='Usage: %prog [options] [plugins]', version='Supybot %s' % conf.version) parser.add_option('-c', '--clean', action='store_true', default=False, dest='clean', help='Cleans the various data/conf/logs' 'directories before running tests.') parser.add_option('-t', '--timeout', action='store', type='float', dest='timeout', help='Sets the timeout, in seconds, for tests to return ' 'responses.') parser.add_option('-v', '--verbose', action='count', default=0, help='Increase verbosity, logging extra information ' 'about each test that runs.') parser.add_option('', '--fail-fast', action='store_true', default=False, help='Stop at first failed test.') parser.add_option('', '--no-network', action='store_true', default=False, dest='nonetwork', help='Causes the network-based tests ' 'not to run.') parser.add_option('', '--no-setuid', action='store_true', default=False, dest='nosetuid', help='Causes the tests based on a ' 'setuid executable not to run.') parser.add_option('', '--trace', action='store_true', default=False, help='Traces all calls made. Unless you\'re really in ' 'a pinch, you probably shouldn\'t do this; it results ' 'in copious amounts of output.') parser.add_option('', '--plugins-dir', action='append', dest='pluginsDirs', default=[], help='Looks in in the given directory for plugins and ' 'loads the tests from all of them.') parser.add_option('', '--exclude', action='append', dest='excludePlugins', default=[], help='List of plugins you do not want --plugins-dir ' 'to include.') parser.add_option('', '--disable-multiprocessing', action='store_true', dest='disableMultiprocessing', help='Disables multiprocessing stuff.') (options, args) = parser.parse_args() world.disableMultiprocessing = options.disableMultiprocessing # This must go before checking for args, of course. for pluginDir in options.pluginsDirs: for name in glob.glob(os.path.join(pluginDir, '*')): #print '***', name if not any(map(lambda x:name in x, map(glob.glob, options.excludePlugins))) and \ os.path.isdir(name): args.append(name) if not args: parser.print_help() sys.exit(-1) if options.timeout: test.timeout = options.timeout if options.trace: traceFilename = conf.supybot.directories.log.dirize('trace.log') fd = open(traceFilename, 'w') sys.settrace(utils.gen.callTracer(fd)) atexit.register(fd.close) atexit.register(lambda : sys.settrace(None)) world.myVerbose = options.verbose if options.nonetwork: test.network = False if options.nosetuid: test.setuid = False log.testing = True world.testing = True args = [s.rstrip('\\/') for s in args] pluginDirs = set([os.path.dirname(s) or '.' for s in args]) conf.supybot.directories.plugins.setValue(list(pluginDirs)) pluginNames = set([os.path.basename(s) for s in args]) load = unittest.defaultTestLoader.loadTestsFromModule for pluginName in pluginNames: if pluginName.endswith('.py'): pluginName = pluginName[:-3] try: pluginModule = plugin.loadPluginModule(pluginName) except (ImportError, callbacks.Error) as e: sys.stderr.write('Failed to load plugin %s:' % pluginName) traceback.print_exc() sys.stderr.write('(pluginDirs: %s)\n' % conf.supybot.directories.plugins()) continue if hasattr(pluginModule, 'test'): test.suites.append(load(pluginModule.test)) suite = unittest.TestSuite(test.suites) if options.fail_fast: if sys.version_info < (2, 7, 0): print('--fail-fast is not supported on Python 2.6.') sys.exit(1) else: runner = unittest.TextTestRunner(verbosity=2, failfast=True) else: runner = unittest.TextTestRunner(verbosity=2) print('Testing began at %s (pid %s)' % (time.ctime(), os.getpid())) if options.clean: shutil.rmtree(conf.supybot.directories.log()) shutil.rmtree(conf.supybot.directories.conf()) shutil.rmtree(conf.supybot.directories.data()) result = runner.run(suite) if hasattr(unittest, 'asserts'): print('Total asserts: %s' % unittest.asserts) if result.wasSuccessful(): sys.exit(0) else: sys.exit(1) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/scripts/supybot-plugin-doc0000644000175000017500000002723513233426066020506 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2005, Ali Afshar # Copyright (c) 2009, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import sys import shutil import supybot if sys.version_info[0] >= 3: basestring = str def error(s): sys.stderr.write('%s\n' % s) sys.exit(-1) # We need to do this before we import conf. if not os.path.exists('doc-conf'): os.mkdir('doc-conf') registryFilename = os.path.join('doc-conf', 'doc.conf') try: fd = open(registryFilename, 'w') fd.write(""" supybot.directories.data: doc-data supybot.directories.conf: doc-conf supybot.directories.log: doc-logs supybot.log.stdout: False supybot.log.level: DEBUG supybot.log.format: %(levelname)s %(message)s supybot.log.plugins.individualLogfiles: False supybot.databases: sqlite anydbm cdb flat pickle """) fd.close() except EnvironmentError as e: error('Unable to open %s for writing.' % registryFilename) import supybot.registry as registry registry.open_registry(registryFilename) import supybot.log as log import supybot.conf as conf conf.supybot.flush.setValue(False) import textwrap import supybot.utils as utils import supybot.world as world import supybot.plugin as plugin import supybot.registry as registry world.documenting = True class PluginDoc(object): def __init__(self, mod): self.mod = mod self.inst = self.mod.Class(None) self.name = self.mod.Class.__name__ self.appendExtraBlankLine = False self.lines = [] def appendLine(self, line, indent=0): line = line.strip() indent = ' ' * indent lines = textwrap.wrap(line, 79, initial_indent=indent, subsequent_indent=indent) self.lines.extend(lines) if self.appendExtraBlankLine: self.lines.append('') def renderRST(self): self.appendExtraBlankLine = False s = 'Documentation for the %s plugin for Supybot' % self.name self.appendLine(s) self.appendLine('=' * len(s)) self.lines.append('') self.appendLine('Purpose') self.appendLine('-------') pdoc = getattr(self.mod, '__doc__', 'My author didn\'t give me a purpose.') self.appendLine(pdoc) self.lines.append('') cdoc = getattr(self.mod.Class, '__doc__', None) if cdoc is not None: self.appendLine('Usage') self.appendLine('-----') self.appendLine(cdoc) self.lines.append('') commands = self.inst.listCommands() if len(commands): self.appendLine('Commands') self.appendLine('--------') for command in commands: log.debug('command: %s', command) line = '%s ' % command command = command.split() doc = self.inst.getCommandHelp(command) if doc: doc = doc.replace('\x02', '') (args, help) = doc.split(')', 1) args = args.split('(', 1)[1] args = args[len(' '.join(command)):].strip() help = help.split('--', 1)[1].strip() self.appendLine(line + args) self.appendLine(help, 1) else: self.appendLine('No help associated with this command') self.lines.append('') # now the config try: confs = conf.supybot.plugins.get(self.name) self.appendLine('Configuration') self.appendLine('-------------') except registry.NonExistentRegistryEntry: log.info('No configuration for plugin %s', plugin) self.appendLine('No configuration for this plugin') else: for confValues in self.genConfig(confs, 0): (name, isChan, help, default, indent) = confValues self.appendLine('%s' % name, indent - 1) self.appendLine('This config variable defaults to %s and %s ' 'channel specific.' % (default,isChan), indent) self.lines.append('') self.appendLine(help, indent) self.lines.append('') return '\n'.join(self.lines) + '\n' def renderSTX(self): self.appendExtraBlankLine = True self.appendLine('Documentation for the %s plugin for ' 'Supybot' % self.name) self.appendLine('Purpose', 1) pdoc = getattr(self.mod, '__doc__', 'My author didn\'t give me a purpose.') self.appendLine(pdoc, 2) cdoc = getattr(self.mod.Class, '__doc__', None) if cdoc is not None: self.appendLine('Usage', 1) self.appendLine(cdoc, 2) commands = self.inst.listCommands() if len(commands): self.appendLine('Commands', 1) for command in commands: log.debug('command: %s', command) line = '* %s ' % command command = command.split() doc = self.inst.getCommandHelp(command) if doc: doc = doc.replace('\x02', '') (args, help) = doc.split(')', 1) args = args.split('(', 1)[1] args = args[len(' '.join(command)):].strip() help = help.split('--', 1)[1].strip() self.appendLine(line + args, 2) self.appendLine(help, 3) else: self.appendLine('No help associated with this command', 3) # now the config try: confs = conf.supybot.plugins.get(self.name) self.appendLine('Configuration', 1) except registry.NonExistentRegistryEntry: log.info('No configuration for plugin %s', plugin) self.appendLine('No configuration for this plugin', 2) else: for confValues in self.genConfig(confs, 2): (name, isChan, help, default, indent) = confValues self.appendLine('* %s' % name, indent - 1) self.appendLine('This config variable defaults to %s and %s ' 'channel specific.' % (default,isChan), indent) self.appendLine(help, indent) return '\n'.join(self.lines) + '\n' def genConfig(self, item, origindent): confVars = item.getValues(getChildren=False, fullNames=False) if not confVars: return for (c, v) in confVars: name = v._name indent = origindent + 1 try: default = str(v) if isinstance(v._default, basestring) or v._default is None: default = utils.str.dqrepr(default) help = v.help() channelValue = v.channelValue except registry.NonExistentRegistryEntry: pass else: if channelValue: cv = 'is' else: cv = 'is not' yield (name, cv, help, default, indent) for confValues in self.genConfig(v, indent): yield confValues def genDoc(m, options): Plugin = PluginDoc(m) print('Generating documentation for %s...' % Plugin.name) path = os.path.join(options.outputDir, '%s.%s' % (Plugin.name, options.format)) try: fd = open(path, 'w') except EnvironmentError as e: error('Unable to open %s for writing.' % path) f = getattr(Plugin, 'render%s' % options.format.upper(), None) if f is None: fd.close() error('Unknown render format: `%s\'' % options.format) try: fd.write(f()) finally: fd.close() if __name__ == '__main__': import glob import os.path import optparse import supybot.plugin as plugin parser = optparse.OptionParser(usage='Usage: %prog [options] [plugins]', version='Supybot %s' % conf.version) parser.add_option('-c', '--clean', action='store_true', default=False, dest='clean', help='Cleans the various data/conf/logs ' 'directories after generating the docs.') parser.add_option('-o', '--output-dir', dest='outputDir', default='.', help='Specifies the directory in which to write the ' 'documentation for the plugin.') parser.add_option('-f', '--format', dest='format', choices=['rst', 'stx'], default='stx', help='Specifies which output format to ' 'use.') parser.add_option('--plugins-dir', action='append', dest='pluginsDirs', default=[], help='Looks in in the given directory for plugins and ' 'generates documentation for all of them.') (options, args) = parser.parse_args() # This must go before checking for args, of course. for pluginDir in options.pluginsDirs: for name in glob.glob(os.path.join(pluginDir, '*')): if os.path.isdir(name): args.append(name) if not args: parser.print_help() sys.exit(-1) args = [s.rstrip('\\/') for s in args] pluginDirs = set([os.path.dirname(s) or '.' for s in args]) conf.supybot.directories.plugins.setValue(list(pluginDirs)) pluginNames = set([os.path.basename(s) for s in args]) plugins = set([]) for pluginName in pluginNames: if pluginName.endswith('.py'): pluginName = pluginName[:-3] try: pluginModule = plugin.loadPluginModule(pluginName) except ImportError as e: s = 'Failed to load plugin %s: %s\n' \ '%s(pluginDirs: %s)' % (pluginName, e, s, conf.supybot.directories.plugins()) error(s) plugins.add(pluginModule) for Plugin in plugins: genDoc(Plugin, options) if options.clean: shutil.rmtree(conf.supybot.directories.log()) shutil.rmtree(conf.supybot.directories.conf()) shutil.rmtree(conf.supybot.directories.data()) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=78: limnoria-2018.01.25/scripts/supybot-plugin-create0000644000175000017500000002505413233426066021201 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from __future__ import print_function import supybot import os import sys import time import os.path import optparse def error(s): sys.stderr.write(textwrap.fill(s)) sys.stderr.write(os.linesep) sys.exit(-1) if sys.version_info < (2, 6, 0): error('This script requires Python 2.6 or newer.') import supybot.conf as conf from supybot.questions import * copyright = ''' ### # Copyright (c) %s, %%s # All rights reserved. # %%s ### ''' % time.strftime('%Y') # Here we use strip() instead of lstrip() on purpose. copyright = copyright.strip() license = ''' # 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. ''' license = license.lstrip() pluginTemplate = ''' %s import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('%s') except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x class %s(callbacks.Plugin): """%s""" %s Class = %s # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: '''.lstrip() # This removes the newlines that precede and follow the text. configTemplate = ''' %s import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('%s') 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(%r, True) %s = conf.registerPlugin(%r) # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(%s, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: '''.lstrip() __init__Template = ''' %s """ %s: %s """ 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__ = '' 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: '''.lstrip() testTemplate = ''' %s from supybot.test import * class %sTestCase(PluginTestCase): plugins = (%r,) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: '''.lstrip() readmeTemplate = ''' %s '''.lstrip() def main(): global copyright global license parser = optparse.OptionParser(usage='Usage: %prog [options]', version='Supybot %s' % conf.version) parser.add_option('-n', '--name', action='store', dest='name', help='sets the name for the plugin.') parser.add_option('-t', '--thread', action='store_true', dest='threaded', help='makes the plugin threaded.') parser.add_option('', '--real-name', action='store', dest='realName', help='Determines what real name the copyright is ' 'assigned to.') parser.add_option('', '--desc', action='store', dest='desc', help='Short description of plugin.') (options, args) = parser.parse_args() if options.name: name = options.name if options.threaded: threaded = True else: threaded = False if options.realName: realName = options.realName else: name = something('What should the name of the plugin be?') if name.endswith('.py'): name = name[:-3] while name[0].islower(): print('Plugin names must begin with a capital letter.') name = something('What should the name of the plugin be?') if name.endswith('.py'): name = name[:-3] if os.path.exists(name): error('A file or directory named %s already exists; remove or ' 'rename it and run this program again.' % name) print(textwrap.fill(textwrap.dedent(""" Sometimes you'll want a callback to be threaded. If its methods (command or regexp-based, either one) will take a significant amount of time to run, you'll want to thread them so they don't block the entire bot.""").strip())) print() threaded = yn('Does your plugin need to be threaded?') realName = something(textwrap.dedent(""" What is your real name, so I can fill in the copyright and license appropriately? """).strip()) if not yn('Do you wish to use Supybot\'s license for your plugin?'): license = '#' if not options.desc: options.desc = something(textwrap.dedent(""" Please provide a short description of the plugin: """).strip()) if threaded: threaded = 'threaded = True' else: threaded = 'pass' if name.endswith('.py'): name = name[:-3] while name[0].islower(): print('Plugin names must begin with a capital letter.') name = something('What should the name of the plugin be?') if name.endswith('.py'): name = name[:-3] copyright %= (realName, license) pathname = name # Make the directory. os.mkdir(pathname) def writeFile(filename, s): fd = open(os.path.join(pathname, filename), 'w') try: fd.write(s) finally: fd.close() writeFile('plugin.py', pluginTemplate % (copyright, name, name, options.desc, threaded, name)) writeFile('config.py', configTemplate % (copyright, name, name, name, name, name)) writeFile('__init__.py', __init__Template % (copyright, name, options.desc)) writeFile('test.py', testTemplate % (copyright, name, name)) writeFile('README.md', readmeTemplate % (options.desc,)) pathname = os.path.join(pathname, 'local') os.mkdir(pathname) writeFile('__init__.py', '# Stub so local is a module, used for third-party modules\n') print('Your new plugin template is in the %s directory.' % name) if __name__ == '__main__': try: main() except KeyboardInterrupt: print() output("""It looks like you cancelled out of this script before it was finished. Obviously, nothing was written, but just run this script again whenever you want to generate a template for a plugin.""") # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/scripts/supybot-botchk0000644000175000017500000001246013233426066017711 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # 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. ### VERBOSE = False def readPid(filename): fd = open(filename) try: return int(fd.read().strip()) finally: fd.close() def isAlive(pid): try: os.kill(pid, 0) return True except OSError: return False def debug(s): if VERBOSE: if not s.endswith(os.linesep): s += os.linesep sys.stdout.write(s) if __name__ == '__main__': # XXX I wanted this for conf.version, but this will create directories. We # really need to refactor conf so it either doesn't create directories, or # so that static information (like the version) can be imported from # somewhere else. # import supybot.conf as conf import os import sys import optparse import subprocess parser = optparse.OptionParser(usage='Usage: %prog [options]') parser.add_option('', '--verbose', action='store_true', help='Makes output verbose.') parser.add_option('', '--botdir', help='Determines what directory the bot resides in and ' 'should be started from.') parser.add_option('', '--pidfile', help='Determines what file to look in for the pid of ' 'the running bot. This should be relative to the ' 'given bot directory. Note that for this to actually ' 'work, you have to make a matching entry in the ' 'supybot.pidFile config in the supybot registry.') parser.add_option('', '--supybot', default='supybot', help='Determines where the supybot executable is ' 'located. If not given, assumes that supybot is ' 'in $PATH.') parser.add_option('', '--conffile', help='Determines what configuration file should be ' 'given to the supybot executable when (re)starting the ' 'bot.') (options, args) = parser.parse_args() VERBOSE = options.verbose if args: parser.error('Extra arguments given.') if not options.botdir: parser.error('No botdir given.') if not options.pidfile: parser.error('No pidfile given.') if not options.conffile: parser.error('No conffile given.') os.chdir(options.botdir) open(options.pidfile, 'a').close() pid = None try: pid = readPid(options.pidfile) debug('Found pidFile with proper pid contents of %s' % pid) except ValueError as e: foundBot = False if pid is not None: foundBot = isAlive(pid) if foundBot: debug('Pid %s is alive and belongs to us.' % pid) else: debug('Pid %s is not the bot.' % pid) if not foundBot: # First, we check if the pidfile is writable. If not, supybot will just exit, # so we go ahead and refuse to start it. try: open(options.pidfile, 'r+') except EnvironmentError as e: debug('pidfile (%s) is not writable: %s' % (options.pidfile, e)) sys.exit(-1) debug('Bot not found, starting.') cmdline = [options.supybot, '--daemon', options.conffile] inst = subprocess.Popen(cmdline, close_fds=True, stderr=subprocess.STDOUT, stdin=None, stdout=subprocess.PIPE) debug('Output from supybot: %r' % inst.stdout.read()) ret = inst.wait() debug('Bot started, command line %r returned %s.' % (' '.join(cmdline), ret)) sys.exit(ret) else: sys.exit(0) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/scripts/supybot-adduser0000644000175000017500000001214113233426066020062 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2002-2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot from supybot.questions import * import os import sys import optparse def main(): import supybot.log as log import supybot.conf as conf conf.supybot.log.stdout.setValue(False) parser = optparse.OptionParser(usage='Usage: %prog [options] ', version='supybot %s' % conf.version) parser.add_option('-u', '--username', action='store', default='', dest='name', help='username for the user.') parser.add_option('-p', '--password', action='store', default='', dest='password', help='password for the user.') parser.add_option('-c', '--capability', action='append', dest='capabilities', metavar='CAPABILITY', help='capability the user should have; ' 'this option may be given multiple times.') (options, args) = parser.parse_args() if len(args) is not 1: parser.error('Specify the users.conf file you\'d like to use. ' 'Be sure *not* to specify your registry file, generated ' 'by supybot-wizard. This is not the file you want. ' 'Instead, take a look in your conf directory (usually ' 'named "conf") and take a gander at the file ' '"users.conf". That\'s the one you want.') filename = os.path.abspath(args[0]) conf.supybot.directories.log.setValue('/') conf.supybot.directories.conf.setValue('/') conf.supybot.directories.data.setValue('/') conf.supybot.directories.plugins.setValue(['/']) conf.supybot.databases.users.filename.setValue(filename) import supybot.ircdb as ircdb if not options.name: name = '' while not name: name = something('What is the user\'s name?') try: # Check to see if the user is already in the database. _ = ircdb.users.getUser(name) # Uh oh. That user already exists; # otherwise we'd have KeyError'ed. output('That user already exists. Try another name.') name = '' except KeyError: # Good. No such user exists. We'll pass. pass else: try: # Same as above. We exit here instead. _ = ircdb.users.getUser(options.name) output('That user already exists. Try another name.') sys.exit(-1) except KeyError: name = options.name if not options.password: password = getpass('What is %s\'s password? ' % name) else: password = options.password if not options.capabilities: capabilities = [] prompt = 'Would you like to give %s a capability?' % name while yn(prompt): capabilities.append(anything('What capability?')) prompt = 'Would you like to give %s another capability?' % name else: capabilities = options.capabilities user = ircdb.users.newUser() user.name = name user.setPassword(password) for capability in capabilities: user.addCapability(capability) ircdb.users.setUser(user) ircdb.users.flush() #os.system('cat %s' % filename) # Was this here just for debugging? ircdb.users.close() print('User %s added.' % name) if __name__ == '__main__': try: main() except KeyboardInterrupt: pass # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/scripts/supybot0000644000175000017500000003710713233426066016446 0ustar valval00000000000000#!/usr/bin/env python ### # Copyright (c) 2003-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ This is the main program to run Supybot. """ import supybot import re import os import sys import atexit import shutil import signal if sys.version_info[0] < 3: import cStringIO as StringIO StringIO = StringIO.StringIO else: from io import StringIO if sys.version_info < (2, 6, 0): sys.stderr.write('This program requires Python >= 2.6.0') sys.stderr.write(os.linesep) sys.exit(-1) def _termHandler(signalNumber, stackFrame): raise SystemExit('Signal #%s.' % signalNumber) signal.signal(signal.SIGTERM, _termHandler) import time import optparse import textwrap started = time.time() import supybot import supybot.utils as utils import supybot.registry as registry import supybot.questions as questions import supybot.ircutils as ircutils try: import supybot.i18n as i18n except ImportError: sys.stderr.write("""Error: You are running a mix of Limnoria and stock Supybot code. Although you run one of Limnoria\'s executables, Python tries to load stock Supybot\'s library. To fix this issue, uninstall Supybot ("%s -m pip uninstall supybot" should do the job) and install Limnoria again. For your information, Supybot's libraries are installed here: %s\n""" % (sys.executable, '\n '.join(supybot.__path__))) exit(-1) from supybot.version import version def main(): import supybot.conf as conf import supybot.world as world import supybot.drivers as drivers import supybot.schedule as schedule # We schedule this event rather than have it actually run because if there # is a failure between now and the time it takes the Owner plugin to load # all the various plugins, our registry file might be wiped. That's bad. interrupted = False when = conf.supybot.upkeepInterval() schedule.addPeriodicEvent(world.upkeep, when, name='upkeep', now=False) world.startedAt = started while world.ircs: try: drivers.run() except KeyboardInterrupt: if interrupted: # Interrupted while waiting for queues to clear. Let's clear # them ourselves. for irc in world.ircs: irc._reallyDie() continue else: interrupted = True log.info('Exiting due to Ctrl-C. ' 'If the bot doesn\'t exit within a few seconds, ' 'feel free to press Ctrl-C again to make it exit ' 'without flushing its message queues.') world.upkeep() for irc in world.ircs: quitmsg = conf.supybot.plugins.Owner.quitMsg() or \ 'Ctrl-C at console.' # Because we're quitting from the console, none of the # standard msg substitutions exist, and these will show as # raw strings by default. Substitute them here with # something meaningful instead. env = dict((key, '') for key in ('who', 'nick', 'user', 'host')) quitmsg = ircutils.standardSubstitute(irc, None, quitmsg, env=env) irc.queueMsg(ircmsgs.quit(quitmsg)) irc.die() except SystemExit as e: s = str(e) if s: log.info('Exiting due to %s', s) break except: try: # Ok, now we're *REALLY* paranoid! log.exception('Exception raised out of drivers.run:') except Exception as e: print('Exception raised in log.exception. This is *really*') print('bad. Hopefully it won\'t happen again, but tell us') print('about it anyway, this is a significant problem.') print('Anyway, here\'s the exception: %s' % \ utils.gen.exnToString(e)) except: print('Oh, this really sucks. Not only did log.exception') print('raise an exception, but freaking-a, it was a string') print('exception. People who raise string exceptions should') print('die a slow, painful death.') httpserver.stopServer() now = time.time() seconds = now - world.startedAt log.info('Total uptime: %s.', utils.gen.timeElapsed(seconds)) (user, system, _, _, _) = os.times() log.info('Total CPU time taken: %.2f seconds.', user+system) log.info('No more Irc objects, exiting.') if __name__ == '__main__': parser = optparse.OptionParser(usage='Usage: %prog [options] configFile', version='Limnoria %s running on Python %s' % (version, sys.version)) parser.add_option('-P', '--profile', action='store_true', dest='profile', help='enables profiling') parser.add_option('-n', '--nick', action='store', dest='nick', default='', help='nick the bot should use') parser.add_option('-u', '--user', action='store', dest='user', default='', help='full username the bot should use') parser.add_option('-i', '--ident', action='store', dest='ident', default='', help='ident the bot should use') parser.add_option('-d', '--daemon', action='store_true', dest='daemon', help='Determines whether the bot will daemonize. ' 'This is a no-op on non-POSIX systems.') parser.add_option('', '--allow-default-owner', action='store_true', dest='allowDefaultOwner', help='Determines whether the bot will allow its ' 'defaultCapabilities not to include "-owner", thus ' 'giving all users the owner capability by default. ' ' This is dumb, hence we require a command-line ' 'option. Don\'t do this.') parser.add_option('', '--allow-root', action='store_true', dest='allowRoot', help='Determines whether the bot will be allowed to run ' 'as root. You don\'t want this. Don\'t do it. ' 'Even if you think you want it, you don\'t. ' 'You\'re probably dumb if you do this.') parser.add_option('', '--debug', action='store_true', dest='debug', help='Determines whether some extra debugging stuff ' 'will be logged in this script.') parser.add_option('', '--disable-multiprocessing', action='store_true', dest='disableMultiprocessing', help='Disables multiprocessing stuff. May lead to ' 'vulnerabilities.') (options, args) = parser.parse_args() if os.name == 'posix': if (os.getuid() == 0 or os.geteuid() == 0) and not options.allowRoot: sys.stderr.write('Don\'t even try to run this as root.') sys.stderr.write(os.linesep) sys.exit(-1) if len(args) > 1: parser.error("""Only one configuration file should be specified.""") elif not args: parser.error(utils.str.normalizeWhitespace("""It seems you've given me no configuration file. If you do have a configuration file, be sure to specify the filename. If you don't have a configuration file, read docs/GETTING_STARTED and follow the instructions.""")) else: registryFilename = args.pop() try: # The registry *MUST* be opened before importing log or conf. i18n.getLocaleFromRegistryFilename(registryFilename) registry.open_registry(registryFilename) shutil.copyfile(registryFilename, registryFilename + '.bak') except registry.InvalidRegistryFile as e: s = '%s in %s. Please fix this error and start supybot again.' % \ (e, registryFilename) s = textwrap.fill(s) sys.stderr.write(s) sys.stderr.write(os.linesep) raise sys.exit(-1) except EnvironmentError as e: sys.stderr.write(str(e)) sys.stderr.write(os.linesep) sys.exit(-1) try: import supybot.log as log except supybot.registry.InvalidRegistryValue as e: # This is raised here because supybot.log imports supybot.conf. name = e.value._name errmsg = textwrap.fill('%s: %s' % (name, e), width=78, subsequent_indent=' '*len(name)) sys.stderr.write(errmsg) sys.stderr.write(os.linesep) sys.stderr.write('Please fix this error in your configuration file ' 'and restart your bot.') sys.stderr.write(os.linesep) sys.exit(-1) import supybot.conf as conf import supybot.world as world i18n.import_conf() world.starting = True def closeRegistry(): # We only print if world.dying so we don't see these messages during # upkeep. logger = log.debug if world.dying: logger = log.info logger('Writing registry file to %s', registryFilename) registry.close(conf.supybot, registryFilename) logger('Finished writing registry file.') world.flushers.append(closeRegistry) world.registryFilename = registryFilename nick = options.nick or conf.supybot.nick() user = options.user or conf.supybot.user() ident = options.ident or conf.supybot.ident() networks = conf.supybot.networks() if not networks: questions.output("""No networks defined. Perhaps you should re-run the wizard?""", fd=sys.stderr) # XXX We should turn off logging here for a prettier presentation. sys.exit(-1) if os.name == 'posix' and options.daemon: def fork(): child = os.fork() if child != 0: if options.debug: print('Parent exiting, child PID: %s' % child) # We must us os._exit instead of sys.exit so atexit handlers # don't run. They shouldn't be dangerous, but they're ugly. os._exit(0) fork() os.setsid() # What the heck does this do? I wonder if it breaks anything... # ...It did. I don't know why, but it seems largely useless. It seems # to me reasonable that we should respect the user's umask. #os.umask(0) # Let's not do this for now (at least until I can make sure it works): # Actually, let's never do this -- we'll always have files open in the # bot directories, so they won't be able to be unmounted anyway. # os.chdir('/') fork() # Since this is the indicator that no writing should be done to stdout, # we'll set it to True before closing stdout et alii. conf.daemonized = True # Closing stdin shouldn't cause problems. We'll let it raise an # exception if it does. sys.stdin.close() # Closing these two might cause problems; we log writes to them as # level WARNING on upkeep. sys.stdout.close() sys.stderr.close() sys.stdout = StringIO() sys.stderr = StringIO() # We have to be really methodical here. os.close(0) os.close(1) os.close(2) fd = os.open('/dev/null', os.O_RDWR) os.dup2(fd, 0) os.dup2(fd, 1) os.dup2(fd, 2) signal.signal(signal.SIGHUP, signal.SIG_IGN) log.info('Completed daemonization. Current PID: %s', os.getpid()) # Stop setting our own umask. See comment above. #os.umask(077) # Let's write the PID file. This has to go after daemonization, obviously. pidFile = conf.supybot.pidFile() if pidFile: try: fd = open(pidFile, 'w') pid = os.getpid() fd.write('%s%s' % (pid, os.linesep)) fd.close() def removePidFile(): try: os.remove(pidFile) except EnvironmentError as e: log.error('Could not remove pid file: %s', e) atexit.register(removePidFile) except EnvironmentError as e: log.critical('Error opening/writing pid file %s: %s', pidFile, e) sys.exit(-1) conf.allowDefaultOwner = options.allowDefaultOwner world.disableMultiprocessing = options.disableMultiprocessing if not os.path.exists(conf.supybot.directories.log()): os.mkdir(conf.supybot.directories.log()) if not os.path.exists(conf.supybot.directories.conf()): os.mkdir(conf.supybot.directories.conf()) if not os.path.exists(conf.supybot.directories.data()): os.mkdir(conf.supybot.directories.data()) if not os.path.exists(conf.supybot.directories.data.tmp()): os.mkdir(conf.supybot.directories.tmp()) userdataFilename = os.path.join(conf.supybot.directories.conf(), 'userdata.conf') # Let's open this now since we've got our directories setup. if not os.path.exists(userdataFilename): fd = open(userdataFilename, 'w') fd.write('\n') fd.close() registry.open_registry(userdataFilename) import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs import supybot.drivers as drivers import supybot.callbacks as callbacks import supybot.plugins.Owner as Owner # These may take some resources, and it does not need to be run while boot, so # we import it as late as possible (but before plugins are loaded). import supybot.httpserver as httpserver owner = Owner.Class() if options.profile: import profile world.profiling = True profile.run('main()', '%s-%i.prof' % (nick, time.time())) else: main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/0000755000175000017500000000000013233426077015002 5ustar valval00000000000000limnoria-2018.01.25/plugins/__init__.py0000644000175000017500000005441113233426066017116 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # 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 gc import os import csv import time import codecs import fnmatch import os.path import threading import collections from .. import callbacks, conf, dbi, ircdb, ircutils, log, utils, world from ..commands import * class NoSuitableDatabase(Exception): def __init__(self, suitable): self.suitable = list(suitable) self.suitable.sort() def __str__(self): return format('No suitable databases were found. Suitable databases ' 'include %L. If you have one of these databases ' 'installed, make sure it is listed in the ' 'supybot.databases configuration variable.', self.suitable) def DB(filename, types): # We don't care if any of the DBs are actually available when # documenting, so just fake that we found something suitable if world.documenting: def junk(*args, **kwargs): pass return junk filename = conf.supybot.directories.data.dirize(filename) def MakeDB(*args, **kwargs): for type in conf.supybot.databases(): # Can't do this because Python sucks. Go ahead, try it! # filename = '.'.join([filename, type, 'db']) fn = '.'.join([filename, type, 'db']) try: return types[type](fn, *args, **kwargs) except KeyError: continue raise NoSuitableDatabase(types.keys()) return MakeDB def makeChannelFilename(filename, channel=None, dirname=None): assert channel is not None, 'Death to those who use None for their channel' filename = os.path.basename(filename) channelSpecific = conf.supybot.databases.plugins.channelSpecific channel = channelSpecific.getChannelLink(channel) channel = ircutils.toLower(channel) if dirname is None: dirname = conf.supybot.directories.data.dirize(channel) if not os.path.exists(dirname): os.makedirs(dirname) return os.path.join(dirname, filename) def getChannel(channel): assert channel is not None, 'Death to those who use None for their channel' channelSpecific = conf.supybot.databases.plugins.channelSpecific return channelSpecific.getChannelLink(channel) # XXX This shouldn't be a mixin. This should be contained by classes that # want such behavior. But at this point, it wouldn't gain much for us # to refactor it. # XXX We need to get rid of this, it's ugly and opposed to # database-independence. class ChannelDBHandler(object): """A class to handle database stuff for individual channels transparently. """ suffix = '.db' def __init__(self, suffix='.db'): self.dbCache = ircutils.IrcDict() suffix = self.suffix if self.suffix and self.suffix[0] != '.': suffix = '.' + suffix self.suffix = suffix def makeFilename(self, channel): """Override this to specialize the filenames of your databases.""" channel = ircutils.toLower(channel) className = self.__class__.__name__ return makeChannelFilename(className + self.suffix, channel) def makeDb(self, filename): """Override this to create your databases.""" raise NotImplementedError def getDb(self, channel): """Use this to get a database for a specific channel.""" currentThread = threading.currentThread() if channel not in self.dbCache and currentThread == world.mainThread: self.dbCache[channel] = self.makeDb(self.makeFilename(channel)) if currentThread != world.mainThread: db = self.makeDb(self.makeFilename(channel)) else: db = self.dbCache[channel] db.isolation_level = None return db def die(self): for db in self.dbCache.values(): try: db.commit() except AttributeError: # In case it's not an SQLite database. pass try: db.close() except AttributeError: # In case it doesn't have a close method. pass del db gc.collect() class DbiChannelDB(object): """This just handles some of the general stuff for Channel DBI databases. Check out ChannelIdDatabasePlugin for an example of how to use this.""" def __init__(self, filename): self.filename = filename self.dbs = ircutils.IrcDict() def _getDb(self, channel): filename = makeChannelFilename(self.filename, channel) try: db = self.dbs[channel] except KeyError: db = self.DB(filename) self.dbs[channel] = db return db def close(self): for db in self.dbs.values(): db.close() def flush(self): for db in self.dbs.values(): db.flush() def __getattr__(self, attr): def _getDbAndDispatcher(channel, *args, **kwargs): db = self._getDb(channel) return getattr(db, attr)(*args, **kwargs) return _getDbAndDispatcher class ChannelUserDictionary(collections.MutableMapping): IdDict = dict def __init__(self): self.channels = ircutils.IrcDict() def __getitem__(self, key): (channel, id) = key return self.channels[channel][id] def __setitem__(self, key, v): (channel, id) = key if channel not in self.channels: self.channels[channel] = self.IdDict() self.channels[channel][id] = v def __delitem__(self, key): (channel, id) = key del self.channels[channel][id] def __iter__(self): for channel, ids in self.channels.items(): for id_, value in ids.items(): yield (channel, id_) raise StopIteration() def __len__(self): return sum([len(x) for x in self.channels]) def items(self): for (channel, ids) in self.channels.items(): for (id, v) in ids.items(): yield ((channel, id), v) def keys(self): L = [] for (k, _) in self.items(): L.append(k) return L # XXX The interface to this needs to be made *much* more like the dbi.DB # interface. This is just too odd and not extensible; any extension # would very much feel like an extension, rather than part of the db # itself. class ChannelUserDB(ChannelUserDictionary): def __init__(self, filename): ChannelUserDictionary.__init__(self) self.filename = filename try: fd = codecs.open(self.filename, encoding='utf8') except EnvironmentError as e: log.warning('Couldn\'t open %s: %s.', self.filename, e) return reader = csv.reader(fd) try: lineno = 0 for t in reader: lineno += 1 try: channel = t.pop(0) id = t.pop(0) try: id = int(id) except ValueError: # We'll skip over this so, say, nicks can be kept here. pass v = self.deserialize(channel, id, t) self[channel, id] = v except Exception as e: log.warning('Invalid line #%s in %s.', lineno, self.__class__.__name__) log.debug('Exception: %s', utils.exnToString(e)) except Exception as e: # This catches exceptions from csv.reader. log.warning('Invalid line #%s in %s.', lineno, self.__class__.__name__) log.debug('Exception: %s', utils.exnToString(e)) def flush(self): mode = 'wb' if utils.minisix.PY2 else 'w' fd = utils.file.AtomicFile(self.filename, mode, makeBackupIfSmaller=False) writer = csv.writer(fd) items = list(self.items()) if not items: log.debug('%s: Refusing to write blank file.', self.__class__.__name__) fd.rollback() return try: items.sort() except TypeError: # FIXME: Implement an algorithm that can order dictionnaries # with both strings and integers as keys. pass for ((channel, id), v) in items: L = self.serialize(v) L.insert(0, id) L.insert(0, channel) writer.writerow(L) fd.close() def close(self): self.flush() self.clear() def deserialize(self, channel, id, L): """Should take a list of strings and return an object to be accessed via self.get(channel, id).""" raise NotImplementedError def serialize(self, x): """Should take an object (as returned by self.get(channel, id)) and return a list (of any type serializable to csv).""" raise NotImplementedError def getUserName(id): if isinstance(id, int): try: return ircdb.users.getUser(id).name except KeyError: return 'a user that is no longer registered' else: return id class ChannelIdDatabasePlugin(callbacks.Plugin): class DB(DbiChannelDB): class DB(dbi.DB): class Record(dbi.Record): __fields__ = [ 'at', 'by', 'text' ] def add(self, at, by, text, **kwargs): record = self.Record(at=at, by=by, text=text, **kwargs) return super(self.__class__, self).add(record) def __init__(self, irc): self.__parent = super(ChannelIdDatabasePlugin, self) self.__parent.__init__(irc) self.db = DB(self.name(), {'flat': self.DB})() def die(self): self.db.close() self.__parent.die() def getCommandHelp(self, name, simpleSyntax=None): help = self.__parent.getCommandHelp(name, simpleSyntax) help = help.replace('$Types', format('%p', self.name())) help = help.replace('$Type', self.name()) help = help.replace('$types', format('%p', self.name().lower())) help = help.replace('$type', self.name().lower()) return help def noSuchRecord(self, irc, channel, id): irc.error('There is no %s with id #%s in my database for %s.' % (self.name(), id, channel)) def checkChangeAllowed(self, irc, msg, channel, user, record): # Checks and returns True if either the user ID (integer) # or the hostmask of the caller match. if (hasattr(user, 'id') and user.id == record.by) or user == record.by: return True cap = ircdb.makeChannelCapability(channel, 'op') if ircdb.checkCapability(msg.prefix, cap): return True irc.errorNoCapability(cap) def addValidator(self, irc, text): """This should irc.error or raise an exception if text is invalid.""" pass def getUserId(self, irc, prefix, channel=None): try: user = ircdb.users.getUser(prefix) return user.id except KeyError: if conf.get(conf.supybot.databases.plugins.requireRegistration, channel): irc.errorNotRegistered(Raise=True) return def add(self, irc, msg, args, channel, text): """[] Adds to the $type database for . is only necessary if the message isn't sent in the channel itself. """ user = self.getUserId(irc, msg.prefix, channel) or msg.prefix at = time.time() self.addValidator(irc, text) if text is not None: id = self.db.add(channel, at, user, text) irc.replySuccess('%s #%s added.' % (self.name(), id)) add = wrap(add, ['channeldb', 'text']) def remove(self, irc, msg, args, channel, id): """[] Removes the $type with id from the $type database for . is only necessary if the message isn't sent in the channel itself. """ user = self.getUserId(irc, msg.prefix, channel) or msg.prefix try: record = self.db.get(channel, id) self.checkChangeAllowed(irc, msg, channel, user, record) self.db.remove(channel, id) irc.replySuccess() except KeyError: self.noSuchRecord(irc, channel, id) remove = wrap(remove, ['channeldb', 'id']) def searchSerializeRecord(self, record): text = utils.str.ellipsisify(record.text, 50) return format('#%s: %q', record.id, text) def search(self, irc, msg, args, channel, optlist, glob): """[] [--{regexp,by} ] [] Searches for $types matching the criteria given. """ predicates = [] def p(record): for predicate in predicates: if not predicate(record): return False return True for (opt, arg) in optlist: if opt == 'by': predicates.append(lambda r, arg=arg: r.by == arg.id) elif opt == 'regexp': if not ircdb.checkCapability(msg.prefix, 'trusted'): # Limited --regexp to trusted users, because specially # crafted regexps can freeze the bot. See # https://github.com/ProgVal/Limnoria/issues/855 for details irc.errorNoCapability('trusted') predicates.append(lambda r: regexp_wrapper(r.text, reobj=arg, timeout=0.1, plugin_name=self.name(), fcn_name='search')) if glob: def globP(r, glob=glob.lower()): return fnmatch.fnmatch(r.text.lower(), glob) predicates.append(globP) L = [] for record in self.db.select(channel, p): L.append(self.searchSerializeRecord(record)) if L: L.sort() irc.reply(format('%s found: %L', len(L), L)) else: what = self.name().lower() irc.reply(format('No matching %p were found.', what)) search = wrap(search, ['channeldb', getopts({'by': 'otherUser', 'regexp': 'regexpMatcher'}), additional(rest('glob'))]) def showRecord(self, record): name = getUserName(record.by) return format('%s #%s: %q (added by %s at %t)', self.name(), record.id, record.text, name, record.at) def get(self, irc, msg, args, channel, id): """[] Gets the $type with id from the $type database for . is only necessary if the message isn't sent in the channel itself. """ try: record = self.db.get(channel, id) irc.reply(self.showRecord(record)) except KeyError: self.noSuchRecord(irc, channel, id) get = wrap(get, ['channeldb', 'id']) def change(self, irc, msg, args, channel, id, replacer): """[] Changes the $type with id according to the regular expression . is only necessary if the message isn't sent in the channel itself. """ user = self.getUserId(irc, msg.prefix, channel) or msg.prefix try: record = self.db.get(channel, id) self.checkChangeAllowed(irc, msg, channel, user, record) record.text = replacer(record.text) self.db.set(channel, id, record) irc.replySuccess() except KeyError: self.noSuchRecord(irc, channel, id) change = wrap(change, ['channeldb', 'id', 'regexpReplacer']) def stats(self, irc, msg, args, channel): """[] Returns the number of $types in the database for . is only necessary if the message isn't sent in the channel itself. """ n = self.db.size(channel) whats = self.name().lower() irc.reply(format('There %b %n in my database.', n, (n, whats))) stats = wrap(stats, ['channeldb']) class PeriodicFileDownloader(object): """A class to periodically download a file/files. A class-level dictionary 'periodicFiles' maps names of files to three-tuples of (url, seconds between downloads, function to run with downloaded file). 'url' should be in some form that urllib2.urlopen can handle (do note that urllib2.urlopen handles file:// links perfectly well.) 'seconds between downloads' is the number of seconds between downloads, obviously. An important point to remember, however, is that it is only engaged when a command is run. I.e., if you say you want the file downloaded every day, but no commands that use it are run in a week, the next time such a command is run, it'll be using a week-old file. If you don't want such behavior, you'll have to give an error mess age to the user and tell them to call you back in the morning. 'function to run with downloaded file' is a function that will be passed a string *filename* of the downloaded file. This will be some random filename probably generated via some mktemp-type-thing. You can do what you want with this; you may want to build a database, take some stats, or simply rename the file. You can pass None as your function and the file with automatically be renamed to match the filename you have it listed under. It'll be in conf.supybot.directories.data, of course. Aside from that dictionary, simply use self.getFile(filename) in any method that makes use of a periodically downloaded file, and you'll be set. """ periodicFiles = None def __init__(self): if self.periodicFiles is None: raise ValueError('You must provide files to download') self.lastDownloaded = {} self.downloadedCounter = {} for filename in self.periodicFiles: if self.periodicFiles[filename][-1] is None: fullname = os.path.join(conf.supybot.directories.data(), filename) if os.path.exists(fullname): self.lastDownloaded[filename] = os.stat(fullname).st_ctime else: self.lastDownloaded[filename] = 0 else: self.lastDownloaded[filename] = 0 self.currentlyDownloading = set() self.downloadedCounter[filename] = 0 self.getFile(filename) def _downloadFile(self, filename, url, f): self.currentlyDownloading.add(filename) try: try: infd = utils.web.getUrlFd(url) except IOError as e: self.log.warning('Error downloading %s: %s', url, e) return except utils.web.Error as e: self.log.warning('Error downloading %s: %s', url, e) return confDir = conf.supybot.directories.data() newFilename = os.path.join(confDir, utils.file.mktemp()) outfd = open(newFilename, 'wb') start = time.time() s = infd.read(4096) while s: outfd.write(s) s = infd.read(4096) infd.close() outfd.close() self.log.info('Downloaded %s in %s seconds', filename, time.time()-start) self.downloadedCounter[filename] += 1 self.lastDownloaded[filename] = time.time() if f is None: toFilename = os.path.join(confDir, filename) if os.name == 'nt': # Windows, grrr... if os.path.exists(toFilename): os.remove(toFilename) os.rename(newFilename, toFilename) else: start = time.time() f(newFilename) total = time.time() - start self.log.info('Function ran on %s in %s seconds', filename, total) finally: self.currentlyDownloading.remove(filename) def getFile(self, filename): if world.documenting: return (url, timeLimit, f) = self.periodicFiles[filename] if time.time() - self.lastDownloaded[filename] > timeLimit and \ filename not in self.currentlyDownloading: self.log.info('Beginning download of %s', url) args = (filename, url, f) name = '%s #%s' % (filename, self.downloadedCounter[filename]) t = threading.Thread(target=self._downloadFile, name=name, args=(filename, url, f)) t.setDaemon(True) t.start() world.threadsSpawned += 1 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Web/0000755000175000017500000000000013233426077015517 5ustar valval00000000000000limnoria-2018.01.25/plugins/Web/test.py0000644000175000017500000001731413233426066017054 0ustar valval00000000000000### # 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. ### from supybot.test import * class WebTestCase(ChannelPluginTestCase): plugins = ('Web', 'Admin',) timeout = 10 if network: def testHeaders(self): self.assertError('headers ftp://ftp.cdrom.com/pub/linux') self.assertNotError('headers http://www.slashdot.org/') def testDoctype(self): self.assertError('doctype ftp://ftp.cdrom.com/pub/linux') self.assertNotError('doctype http://www.slashdot.org/') m = self.getMsg('doctype http://moobot.sf.net/') self.failUnless(m.args[1].endswith('>')) def testSize(self): self.assertError('size ftp://ftp.cdrom.com/pub/linux') self.assertNotError('size http://supybot.sf.net/') self.assertNotError('size http://www.slashdot.org/') def testTitle(self): # Checks for @title not-working correctly self.assertResponse('title ' 'http://www.catb.org/~esr/jargon/html/F/foo.html', 'foo') # Checks for only grabbing the real title tags instead of title # tags inside, for example, script tags. Bug #1190350 self.assertNotRegexp('title ' 'http://www.irinnews.org/report.asp?ReportID=45910&' 'SelectRegion=West_Africa&SelectCountry=CHAD', r'document\.write\(') # Checks that title parser grabs the full title instead of just # part of it. self.assertRegexp('title http://www.n-e-r-d.com/', 'N.*E.*R.*D') # Checks that the parser doesn't hang on invalid tags self.assertNotError( 'title http://www.youtube.com/watch?v=x4BtiqPN4u8') self.assertResponse( 'title http://www.thefreedictionary.com/don%27t', "Don't - definition of don't by The Free Dictionary") self.assertRegexp( 'title ' 'https://twitter.com/rlbarnes/status/656554266744586240', '"PSA: In Firefox 44 Nightly, "http:" pages with ' ' are now marked insecure. ' 'https://t.co/qS9LxuRPdm"$') def testTitleSnarfer(self): try: conf.supybot.plugins.Web.titleSnarfer.setValue(True) self.assertSnarfRegexp('http://microsoft.com/', 'Microsoft') finally: conf.supybot.plugins.Web.titleSnarfer.setValue(False) def testNonSnarfing(self): snarf = conf.supybot.plugins.Web.nonSnarfingRegexp() title = conf.supybot.plugins.Web.titleSnarfer() try: conf.supybot.plugins.Web.nonSnarfingRegexp.set('m/sf/') try: conf.supybot.plugins.Web.titleSnarfer.setValue(True) self.assertSnarfNoResponse('http://sf.net/', 2) self.assertSnarfRegexp('http://www.sourceforge.net/', r'Sourceforge\.net') finally: conf.supybot.plugins.Web.titleSnarfer.setValue(title) finally: conf.supybot.plugins.Web.nonSnarfingRegexp.setValue(snarf) def testSnarferIgnore(self): conf.supybot.plugins.Web.titleSnarfer.setValue(True) (oldprefix, self.prefix) = (self.prefix, 'foo!bar@baz') try: self.assertSnarfRegexp('http://google.com/', 'Google') self.assertNotError('admin ignore add %s' % self.prefix) self.assertSnarfNoResponse('http://google.com/') self.assertNoResponse('title http://www.google.com/') finally: conf.supybot.plugins.Web.titleSnarfer.setValue(False) (self.prefix, oldprefix) = (oldprefix, self.prefix) self.assertNotError('admin ignore remove %s' % oldprefix) def testSnarferNotIgnore(self): conf.supybot.plugins.Web.titleSnarfer.setValue(True) conf.supybot.plugins.Web.checkIgnored.setValue(False) (oldprefix, self.prefix) = (self.prefix, 'foo!bar@baz') try: self.assertSnarfRegexp('https://google.com/', 'Google') self.assertNotError('admin ignore add %s' % self.prefix) self.assertSnarfRegexp('https://www.google.com/', 'Google') self.assertNoResponse('title http://www.google.com/') finally: conf.supybot.plugins.Web.titleSnarfer.setValue(False) conf.supybot.plugins.Web.checkIgnored.setValue(True) (self.prefix, oldprefix) = (oldprefix, self.prefix) self.assertNotError('admin ignore remove %s' % oldprefix) def testWhitelist(self): fm = conf.supybot.plugins.Web.fetch.maximum() uw = conf.supybot.plugins.Web.urlWhitelist() try: conf.supybot.plugins.Web.fetch.maximum.set(1024) self.assertNotError('web fetch http://fsf.org') conf.supybot.plugins.Web.urlWhitelist.set('http://slashdot.org') self.assertError('web fetch http://fsf.org') self.assertError('wef title http://fsf.org') self.assertError('web fetch http://slashdot.org.evildomain.com') self.assertNotError('web fetch http://slashdot.org') self.assertNotError('web fetch http://slashdot.org/recent') conf.supybot.plugins.Web.urlWhitelist.set('http://slashdot.org http://fsf.org') self.assertNotError('doctype http://fsf.org') finally: conf.supybot.plugins.Web.urlWhitelist.set('') conf.supybot.plugins.Web.fetch.maximum.set(fm) def testNonSnarfingRegexpConfigurable(self): self.assertSnarfNoResponse('http://foo.bar.baz/', 2) try: conf.supybot.plugins.Web.nonSnarfingRegexp.set('m/biff/') self.assertSnarfNoResponse('http://biff.bar.baz/', 2) finally: conf.supybot.plugins.Web.nonSnarfingRegexp.set('') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Web/plugin.py0000644000175000017500000003156113233426066017373 0ustar valval00000000000000### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # 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 sys import socket import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.utils.minisix as minisix import supybot.plugins as plugins import supybot.commands as commands import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Web') if minisix.PY3: from html.parser import HTMLParser from html.entities import entitydefs import http.client as http_client else: from HTMLParser import HTMLParser from htmlentitydefs import entitydefs import httplib as http_client class Title(utils.web.HtmlToText): entitydefs = entitydefs.copy() entitydefs['nbsp'] = ' ' def __init__(self): self.inTitle = False self.inSvg = False utils.web.HtmlToText.__init__(self) @property def inHtmlTitle(self): return self.inTitle and not self.inSvg def handle_starttag(self, tag, attrs): if tag == 'title': self.inTitle = True elif tag == 'svg': self.inSvg = True def handle_endtag(self, tag): if tag == 'title': self.inTitle = False elif tag == 'svg': self.inSvg = False def append(self, data): if self.inHtmlTitle: super(Title, self).append(data) class DelayedIrc: def __init__(self, irc): self._irc = irc self._replies = [] def reply(self, *args, **kwargs): self._replies.append(('reply', args, kwargs)) def error(self, *args, **kwargs): self._replies.append(('error', args, kwargs)) def __getattr__(self, name): assert name not in ('reply', 'error', '_irc', '_msg', '_replies') return getattr(self._irc, name) if hasattr(http_client, '_MAXHEADERS'): def fetch_sandbox(f): """Runs a command in a forked process with limited memory resources to prevent memory bomb caused by specially crafted http responses. On CPython versions with support for limiting the number of headers, this is the identity function""" return f else: # For the following CPython versions (as well as the matching Pypy # versions): # * 2.6 before 2.6.9 # * 2.7 before 2.7.9 # * 3.2 before 3.2.6 # * 3.3 before 3.3.3 def fetch_sandbox(f): """Runs a command in a forked process with limited memory resources to prevent memory bomb caused by specially crafted http responses.""" def process(self, irc, msg, *args, **kwargs): delayed_irc = DelayedIrc(irc) f(self, delayed_irc, msg, *args, **kwargs) return delayed_irc._replies def newf(self, irc, *args): try: replies = commands.process(process, self, irc, *args, timeout=10, heap_size=10*1024*1024, pn=self.name(), cn=f.__name__) except (commands.ProcessTimeoutError, MemoryError): raise utils.web.Error(_('Page is too big or the server took ' 'too much time to answer the request.')) else: for (method, args, kwargs) in replies: getattr(irc, method)(*args, **kwargs) newf.__doc__ = f.__doc__ return newf def catch_web_errors(f): """Display a nice error instead of "An error has occurred".""" def newf(self, irc, *args, **kwargs): try: f(self, irc, *args, **kwargs) except utils.web.Error as e: irc.reply(str(e)) return utils.python.changeFunctionName(newf, f.__name__, f.__doc__) class Web(callbacks.PluginRegexp): """Add the help for 'help Web' here.""" regexps = ['titleSnarfer'] threaded = True def noIgnore(self, irc, msg): return not self.registryValue('checkIgnored', msg.args[0]) def getTitle(self, irc, url, raiseErrors): size = conf.supybot.protocols.http.peekSize() timeout = self.registryValue('timeout') (target, text) = utils.web.getUrlTargetAndContent(url, size=size, timeout=timeout) try: text = text.decode(utils.web.getEncoding(text) or 'utf8', 'replace') except UnicodeDecodeError: pass parser = Title() if minisix.PY3 and isinstance(text, bytes): if raiseErrors: irc.error(_('Could not guess the page\'s encoding. (Try ' 'installing python-charade.)'), Raise=True) else: return None parser.feed(text) parser.close() title = utils.str.normalizeWhitespace(''.join(parser.data).strip()) if title: return (target, title) elif raiseErrors: if len(text) < size: irc.error(_('That URL appears to have no HTML title.'), Raise=True) else: irc.error(format(_('That URL appears to have no HTML title ' 'within the first %S.'), size), Raise=True) @fetch_sandbox def titleSnarfer(self, irc, msg, match): channel = msg.args[0] if not irc.isChannel(channel): return if callbacks.addressed(irc.nick, msg): return if self.registryValue('titleSnarfer', channel): url = match.group(0) if not self._checkURLWhitelist(url): return r = self.registryValue('nonSnarfingRegexp', channel) if r and r.search(url): self.log.debug('Not titleSnarfing %q.', url) return r = self.getTitle(irc, url, False) if not r: return (target, title) = r if title: domain = utils.web.getDomain(target if self.registryValue('snarferShowTargetDomain', channel) else url) s = format(_('Title: %s'), title) if self.registryValue('snarferShowDomain', channel): s += format(_(' (at %s)'), domain) irc.reply(s, prefixNick=False) titleSnarfer = urlSnarfer(titleSnarfer) titleSnarfer.__doc__ = utils.web._httpUrlRe def _checkURLWhitelist(self, url): if not self.registryValue('urlWhitelist'): return True passed = False for wu in self.registryValue('urlWhitelist'): if wu.endswith('/') and url.find(wu) == 0: passed = True break if (not wu.endswith('/')) and (url.find(wu + '/') == 0 or url == wu): passed = True break return passed @wrap(['httpUrl']) @catch_web_errors @fetch_sandbox def headers(self, irc, msg, args, url): """ Returns the HTTP headers of . Only HTTP urls are valid, of course. """ if not self._checkURLWhitelist(url): irc.error("This url is not on the whitelist.") return timeout = self.registryValue('timeout') fd = utils.web.getUrlFd(url, timeout=timeout) try: s = ', '.join([format(_('%s: %s'), k, v) for (k, v) in fd.headers.items()]) irc.reply(s) finally: fd.close() _doctypeRe = re.compile(r'(]+>)', re.M) @wrap(['httpUrl']) @catch_web_errors @fetch_sandbox def doctype(self, irc, msg, args, url): """ Returns the DOCTYPE string of . Only HTTP urls are valid, of course. """ if not self._checkURLWhitelist(url): irc.error("This url is not on the whitelist.") return size = conf.supybot.protocols.http.peekSize() timeout = self.registryValue('timeout') s = utils.web.getUrl(url, size=size, timeout=timeout).decode('utf8') m = self._doctypeRe.search(s) if m: s = utils.str.normalizeWhitespace(m.group(0)) irc.reply(s) else: irc.reply(_('That URL has no specified doctype.')) @wrap(['httpUrl']) @catch_web_errors @fetch_sandbox def size(self, irc, msg, args, url): """ Returns the Content-Length header of . Only HTTP urls are valid, of course. """ if not self._checkURLWhitelist(url): irc.error("This url is not on the whitelist.") return timeout = self.registryValue('timeout') fd = utils.web.getUrlFd(url, timeout=timeout) try: try: size = fd.headers['Content-Length'] if size is None: raise KeyError('content-length') irc.reply(format(_('%u is %S long.'), url, int(size))) except KeyError: size = conf.supybot.protocols.http.peekSize() s = fd.read(size) if len(s) != size: irc.reply(format(_('%u is %S long.'), url, len(s))) else: irc.reply(format(_('The server didn\'t tell me how long %u ' 'is but it\'s longer than %S.'), url, size)) finally: fd.close() @wrap([getopts({'no-filter': ''}), 'httpUrl']) @catch_web_errors @fetch_sandbox def title(self, irc, msg, args, optlist, url): """[--no-filter] Returns the HTML ... of a URL. If --no-filter is given, the bot won't strip special chars (action, DCC, ...). """ if not self._checkURLWhitelist(url): irc.error("This url is not on the whitelist.") return r = self.getTitle(irc, url, True) if not r: return (target, title) = r if title: if not [y for x,y in optlist if x == 'no-filter']: for i in range(1, 4): title = title.replace(chr(i), '') irc.reply(title) @wrap(['text']) def urlquote(self, irc, msg, args, text): """ Returns the URL quoted form of the text. """ irc.reply(utils.web.urlquote(text)) @wrap(['text']) def urlunquote(self, irc, msg, args, text): """ Returns the text un-URL quoted. """ s = utils.web.urlunquote(text) irc.reply(s) @wrap(['url']) @catch_web_errors @fetch_sandbox def fetch(self, irc, msg, args, url): """ Returns the contents of , or as much as is configured in supybot.plugins.Web.fetch.maximum. If that configuration variable is set to 0, this command will be effectively disabled. """ if not self._checkURLWhitelist(url): irc.error("This url is not on the whitelist.") return max = self.registryValue('fetch.maximum') timeout = self.registryValue('fetch.timeout') if not max: irc.error(_('This command is disabled ' '(supybot.plugins.Web.fetch.maximum is set to 0).'), Raise=True) fd = utils.web.getUrl(url, size=max, timeout=timeout).decode('utf8') irc.reply(fd) Class = Web # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Web/config.py0000644000175000017500000001137413233426066017342 0ustar valval00000000000000### # 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('Web') 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 Web = conf.registerPlugin('Web', True) if yn("""This plugin also offers a snarfer that will try to fetch the title of URLs that it sees in the channel. Would like you this snarfer to be enabled?""", default=False): Web.titleSnarfer.setValue(True) Web = conf.registerPlugin('Web') conf.registerChannelValue(Web, 'titleSnarfer', registry.Boolean(False, _("""Determines whether the bot will output the HTML title of URLs it sees in the channel."""))) conf.registerChannelValue(Web, 'snarferReportIOExceptions', registry.Boolean(False, _("""Determines whether the bot will notfiy the user about network exceptions like hostnotfound, timeout ...."""))) conf.registerChannelValue(Web, 'snarferShowDomain', registry.Boolean(True, _("""Determines whether domain names should be displayed by the title snarfer."""))) conf.registerChannelValue(Web, 'snarferShowTargetDomain', registry.Boolean(False, _("""Determines whether the domain name displayed by the snarfer will be the original one (posted on IRC) or the target one (got after following redirects, if any)."""))) conf.registerChannelValue(Web, 'nonSnarfingRegexp', registry.Regexp(None, _("""Determines what URLs matching the given regexp will not be snarfed. Give the empty string if you have no URLs that you'd like to exclude from being snarfed."""))) conf.registerChannelValue(Web, 'checkIgnored', registry.Boolean(True, _("""Determines whether the title snarfer checks if the author of a message is ignored."""))) conf.registerGlobalValue(Web, 'urlWhitelist', registry.SpaceSeparatedListOfStrings([], """If set, bot will only fetch data from urls in the whitelist, i.e. starting with http://domain/optionalpath/. This will apply to all commands that retrieve data from user-supplied URLs, including fetch, headers, title, doctype.""")) conf.registerGlobalValue(Web, 'timeout', registry.NonNegativeInteger(5, """Determines the maximum number of seconds the bot will wait for the site to respond, when using a command in this plugin other than 'fetch'. If 0, will use socket.defaulttimeout""")) conf.registerGroup(Web, 'fetch') conf.registerGlobalValue(Web.fetch, 'maximum', registry.NonNegativeInteger(0, _("""Determines the maximum number of bytes the bot will download via the 'fetch' command in this plugin."""))) conf.registerGlobalValue(Web.fetch, 'timeout', registry.NonNegativeInteger(5, """Determines the maximum number of seconds the bot will wait for the site to respond, when using the 'fetch' command in this plugin. If 0, will use socket.defaulttimeout""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Web/__init__.py0000644000175000017500000000434413233426066017633 0ustar valval00000000000000### # 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. ### """ Includes various web-related commands. """ import supybot import supybot.world as world __version__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from imp import reload 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! if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Web/locales/0000755000175000017500000000000013233426077017141 5ustar valval00000000000000limnoria-2018.01.25/plugins/Web/locales/it.po0000644000175000017500000001141213233426066020112 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2011-01-28 20:03+0100\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: config.py:50 msgid "" "Determines whether the bot will output the\n" " HTML title of URLs it sees in the channel." msgstr "" "Determina se il bot mostrerà il titolo HTML degli URL che vede in canale." #: config.py:53 msgid "" "Determines what URLs matching the given regexp\n" " will not be snarfed. Give the empty string if you have no URLs that you'd\n" " like to exclude from being snarfed." msgstr "" "Determina quali URL corrispondenti alla regexp fornita non verranno intercettati.\n" " Se non si vuole escludere alcun URL, fornire una stringa vuota.\n" #: config.py:60 msgid "" "Determines the maximum number of\n" " bytes the bot will download via the 'fetch' command in this plugin." msgstr "" "Determina il numero massimo di byte che il bot scaricherà tramite il comando \"fetch\" di questo plugin." #: plugin.py:71 #, docstring msgid "Add the help for \"help Web\" here." msgstr "" #: plugin.py:107 msgid "Title: %s (at %s)" msgstr "Titolo: %s (su %s)" #: plugin.py:114 #, docstring msgid "" "\n" "\n" " Returns the HTTP headers of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" " Restituisce gli header HTTP di . Naturalmente sono validi solo ULR HTTP.\n" " " #: plugin.py:121 msgid "%s: %s" msgstr "%s: %s" #: plugin.py:131 #, docstring msgid "" "\n" "\n" " Returns the DOCTYPE string of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" " Restituisce la stringa DOCTYPE di . Naturalmente sono validi solo URL HTTP.\n" " " #: plugin.py:143 msgid "That URL has no specified doctype." msgstr "Questo URL non ha un doctype specificato." #: plugin.py:148 #, docstring msgid "" "\n" "\n" " Returns the Content-Length header of . Only HTTP urls are valid,\n" " of course.\n" " " msgstr "" "\n" "\n" " Restituisce l'header Content-Length di . Naturalmente sono validi solo ULR HTTP.\n" " " #: plugin.py:157 plugin.py:162 msgid "%u is %S long." msgstr "%u è lungo %S." #: plugin.py:164 msgid "The server didn't tell me how long %u is but it's longer than %S." msgstr "Il server non mi ha detto quanto sia lungo %u ma è più di %S." #: plugin.py:173 #, docstring msgid "" "\n" "\n" " Returns the HTML ... of a URL.\n" " " msgstr "" "\n" "\n" " Restituisce il tag HTML ... di un URL.\n" " " #: plugin.py:188 msgid "That URL appears to have no HTML title." msgstr "Questo URL sembra non avere un titolo HTML." #: plugin.py:190 msgid "That URL appears to have no HTML title within the first %S." msgstr "Sembra che questo URL non abbia un titolo HTML entro i primi %S." #: plugin.py:198 #, docstring msgid "" "\n" "\n" " Returns Netcraft.com's determination of what operating system and\n" " webserver is running on the host given.\n" " " msgstr "" "\n" "\n" " Riporta la stima di Netcraft.com riguardo a quale sistema\n" " operativo e server web girano sull'host richiesto.\n" " " #: plugin.py:212 msgid "No results found for %s." msgstr "Nessun risultato trovato per %s." #: plugin.py:214 msgid "The format of page the was odd." msgstr "Il formato della pagina è strano." #: plugin.py:219 #, docstring msgid "" "\n" "\n" " Returns the URL quoted form of the text.\n" " " msgstr "" "\n" "\n" " Codifica il testo in base alla codifica URL.\n" " " #: plugin.py:228 #, docstring msgid "" "\n" "\n" " Returns the text un-URL quoted.\n" " " msgstr "" "\n" "\n" " Decodifica il testo codificato secondo la codifica URL.\n" " " #: plugin.py:238 #, docstring msgid "" "\n" "\n" " Returns the contents of , or as much as is configured in\n" " supybot.plugins.Web.fetch.maximum. If that configuration variable is\n" " set to 0, this command will be effectively disabled.\n" " " msgstr "" "\n" "\n" " Riporta il contenuto di , o tanti byte quanti sono definiti in\n" " supybot.plugins.Web.fetch.maximum. Se questa variabile è impostata a 0,\n" " il comando sarà disabilitato.\n" " " #: plugin.py:246 msgid "This command is disabled (supybot.plugins.Web.fetch.maximum is set to 0)." msgstr "Questo comando è disabilitato (supybot.plugins.Web.fetch.maximum è impostata a 0)." limnoria-2018.01.25/plugins/Web/locales/fr.po0000644000175000017500000001363713233426066020120 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2014-01-22 13:46+CET\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \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" #: config.py:50 msgid "" "Determines whether the bot will output the\n" " HTML title of URLs it sees in the channel." msgstr "" "Détermine si le bot affichera le titre HTML des URLs qu'il voit sur le canal." #: config.py:53 msgid "" "Determines whether the bot will notfiy the user\n" " about network exceptions like hostnotfound, timeout ...." msgstr "" "Détermine si le bot notifiera les utilisateurs à propos d’exceptions liées " "au réseau, comme hostnotfound, timeout, …" #: config.py:56 msgid "" "Determines whether the domain name displayed\n" " by the snarfer will be the original one (posted on IRC) or the target " "one\n" " (got after following redirects, if any)." msgstr "" "Détermine si le nom de domaine affiché par le snarfer est l’original (posté " "sur IRC) ou celui de la cible (après avoir suivi les redirections, s’il y en " "a)." #: config.py:60 msgid "" "Determines what URLs matching the given regexp\n" " will not be snarfed. Give the empty string if you have no URLs that " "you'd\n" " like to exclude from being snarfed." msgstr "" "Détermine quelles URLs ne seront pas écoutées. Donnez une chaîne vide si " "vous ne voulez ignorer aucune URL." #: config.py:72 msgid "" "Determines the maximum number of\n" " bytes the bot will download via the 'fetch' command in this plugin." msgstr "" "Détermine le nombre maximum d'octet que le bot téléchargera via la commande " "'fetch' de ce plugin." #: plugin.py:86 msgid "" "Runs a command in a forked process with limited memory resources\n" " to prevent memory bomb caused by specially crafted http responses." msgstr "." #: plugin.py:98 msgid "Page is too big." msgstr "La page est trop grosse." #: plugin.py:106 msgid "Display a nice error instead of \"An error has occurred\"." msgstr "." #: plugin.py:116 msgid "Add the help for \"help Web\" here." msgstr "" #: plugin.py:162 msgid "Title: %s (at %s)" msgstr "Titre : %s (de %s)" #: plugin.py:184 msgid "" "\n" "\n" " Returns the HTTP headers of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" "Retourne les en-têtes HTTP de l'. Seules les URLs HTTP sont valides, " "bien sûr." #: plugin.py:194 msgid "%s: %s" msgstr "%s : %s" #: plugin.py:206 msgid "" "\n" "\n" " Returns the DOCTYPE string of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" "Retourne le DOCTYPE de l'. Seules les URLs HTTP sont valides, bien sûr." #: plugin.py:222 msgid "That URL has no specified doctype." msgstr "Cette URL n'a pas de doctype spécifié." #: plugin.py:229 msgid "" "\n" "\n" " Returns the Content-Length header of . Only HTTP urls are " "valid,\n" " of course.\n" " " msgstr "" "\n" "\n" "Retourne le'en-têtes HTTP Content-Length de l'. Seules les URLs HTTP " "sont valides, bien sûr." #: plugin.py:241 plugin.py:246 msgid "%u is %S long." msgstr "%u est long de %S." #: plugin.py:248 msgid "The server didn't tell me how long %u is but it's longer than %S." msgstr "" "Le serveur ne m'a pas dit quelle est la longueur de %u, mais c'est sûr que " "c'est plus que %S." #: plugin.py:259 msgid "" "[--no-filter] \n" "\n" " Returns the HTML ... of a URL.\n" " If --no-filter is given, the bot won't strip special chars (action,\n" " DCC, ...).\n" " " msgstr "" "[--no-filter] Retourne le titre HTML ... d'une adresse. " "Si --no-filter est donné, le bot ne supprimera pas les caractères spéciaux " "(action, DCC, ...)" #: plugin.py:288 msgid "That URL appears to have no HTML title." msgstr "Cette URL semble ne pas avoir de titre HTML." #: plugin.py:290 msgid "That URL appears to have no HTML title within the first %S." msgstr "" "Ce URL semble ne pas avoir de titre HTML dans les %S au début du fichier." #: plugin.py:296 msgid "" "\n" "\n" " Returns the URL quoted form of the text.\n" " " msgstr "" "\n" "\n" "Retourne la forme formattée pour URLs du texte." #: plugin.py:305 msgid "" "\n" "\n" " Returns the text un-URL quoted.\n" " " msgstr "" "\n" "\n" "Retourne la forme dé-formattée pour URLs du texte." #: plugin.py:317 msgid "" "\n" "\n" " Returns the contents of , or as much as is configured in\n" " supybot.plugins.Web.fetch.maximum. If that configuration variable " "is\n" " set to 0, this command will be effectively disabled.\n" " " msgstr "" "\n" "\n" "Retourne le contenu de l', ou les supybot.plugins.Web.fetch.maximum " "premiers octets. Si la variable de configution est définie à 0, elle sera " "effectivement désactivée." #: plugin.py:328 msgid "" "This command is disabled (supybot.plugins.Web.fetch.maximum is set to 0)." msgstr "" "Cette commande est désactivée (supybot.plugins.Web.fetch.maximum vaut 0)." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the HTML ... of a URL.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne le titre HTTP de l'." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns Netcraft.com's determination of what operating system " #~ "and\n" #~ " webserver is running on the host given.\n" #~ " " #~ msgstr "" #~ "Retourne ce que Netcraft.com dit du système d'exploitation " #~ "et du serveur web utilisés par l'hôte." #~ msgid "No results found for %s." #~ msgstr "Pas de résultat trouvé pour %s." #~ msgid "The format of page the was odd." #~ msgstr "Le format de la page est bizarre." limnoria-2018.01.25/plugins/Web/locales/fi.po0000644000175000017500000001647013233426066020105 0ustar valval00000000000000# Web plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: Web plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 14:42+EET\n" "PO-Revision-Date: 2014-12-20 14:42+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: 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.6.10\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: config.py:50 msgid "" "Determines whether the bot will output the\n" " HTML title of URLs it sees in the channel." msgstr "" "Määrittää tulostaako botti\n" " kanavalle näkimiensä URL-osoitteiden HTML otsikot." #: config.py:53 msgid "" "Determines whether the bot will notfiy the user\n" " about network exceptions like hostnotfound, timeout ...." msgstr "" "Määrittää ilmoitetaanko käyttäjää verkkovirheistä,\n" " kuten isäntää ei löydy, aikakatkaisu ...." #: config.py:56 msgid "" "Determines whether the domain name displayed\n" " by the snarfer will be the original one (posted on IRC) or the target " "one\n" " (got after following redirects, if any)." msgstr "" "Määrittää onko kaappaajan näyttämä domain nimi se, joka lähetettiin " "alunperin IRC:ssä\n" " vai kohde domainin nimi (seuraten uudelleenohjauksia siinä tapauksessa, " "että niitä on)." #: config.py:60 msgid "" "Determines what URLs matching the given regexp\n" " will not be snarfed. Give the empty string if you have no URLs that " "you'd\n" " like to exclude from being snarfed." msgstr "" "Määrittää mitä säännöllistä lauseketta täsmäävät URL-osoitteet eivät tule " "kaapatuiksi.\n" " Anna tyhjä merkkiketju, mikäli sinulla ei ole URL-osoitteita, joiden et " "haluaisi tulevan\n" " kaapatuiksi." #: config.py:72 msgid "" "Determines the maximum number of\n" " bytes the bot will download via the 'fetch' command in this plugin." msgstr "" "Määrittää enimmäismäärän bittejä, jotka botti lataa \n" " käyttämällä 'fetch' komentoa tässä lisäosassa." #: plugin.py:90 msgid "" "Runs a command in a forked process with limited memory resources\n" " to prevent memory bomb caused by specially crafted http responses." msgstr "" "Suorittaa komennon forkatussa prosessissa rajoitetuilla muistiresursseilla\n" " estääkseen muistipommin, jonka aiheuttavat vartavasten luodut http-" "vastaukset." #: plugin.py:102 msgid "Page is too big or the server took too much time to answer the request." msgstr "Sivu on liian suuri tai palvelin vastasi pyyntöön liian hitaasti." #: plugin.py:111 msgid "Display a nice error instead of \"An error has occurred\"." msgstr "Näytä kiva virheilmoitus ilmoitukset \"Virhe on tapahtunut\" sijaan." #: plugin.py:121 msgid "Add the help for \"help Web\" here." msgstr "Lisää ohje komennolle \"help Web\" tähän." #: plugin.py:166 msgid "Title: %s (at %s)" msgstr "Otsikko: %s (sivustolla %s)" #: plugin.py:188 msgid "" "\n" "\n" " Returns the HTTP headers of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" " Palauttaa -osoitteen HTTP otsikot. Tietysti, vain\n" " HTTP URL-osoitteet ovat kelvollisia.\n" " " #: plugin.py:198 msgid "%s: %s" msgstr "%s: %s" #: plugin.py:210 msgid "" "\n" "\n" " Returns the DOCTYPE string of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" " Palauttaa -osoitteen DOCTYPE merkkiketjun. Tietysti, \n" " vain HTTP URL-osoitteet ovat kelvollisia.\n" " " #: plugin.py:226 msgid "That URL has no specified doctype." msgstr "Tuo URL-osoite ei ole määrittänyt doctypeä." #: plugin.py:233 msgid "" "\n" "\n" " Returns the Content-Length header of . Only HTTP urls are " "valid,\n" " of course.\n" " " msgstr "" "\n" "\n" " Palauttaa -osoitteen sisällön pituus otsikon. Tietysti, \n" " vain HTTP URL-osoitteet ovat kelvollisia.\n" " " #: plugin.py:245 plugin.py:250 msgid "%u is %S long." msgstr "%u on %S pitkä." #: plugin.py:252 msgid "The server didn't tell me how long %u is but it's longer than %S." msgstr "" "Palvelin ei kertonut minulle, kuinka pitkä %u on, mutta se on pidempi kuin " "%S." #: plugin.py:263 msgid "" "[--no-filter] \n" "\n" " Returns the HTML ... of a URL.\n" " If --no-filter is given, the bot won't strip special chars (action,\n" " DCC, ...).\n" " " msgstr "" "[--no-filter] \n" "\n" " Palauttaa ... URL-osoitteen titletageista.\n" " Jos--no-filter annetaan, erikoismerkkejä (action,\n" " DCC, ...) ei riisuta.\n" " " #: plugin.py:281 #, fuzzy msgid "Could not guess the page's encoding. (Try installing python-charade.)" msgstr "" "Sivun merkistökoodausta ei pystytty arvaamaan. (Kokeile python-charade:n " "asentamista.)" #: plugin.py:295 msgid "That URL appears to have no HTML title." msgstr "Tuolla URL-osoitteella ei vaikuta olevan HTTP otsikkoa." #: plugin.py:297 msgid "That URL appears to have no HTML title within the first %S." msgstr "" "Tuolla URL-osoitteella ei vaikuta olevan HTML otsikkoa ensinmäisissä %S." #: plugin.py:303 msgid "" "\n" "\n" " Returns the URL quoted form of the text.\n" " " msgstr "" "\n" "\n" " Palauttaa URL lainatun muodon tekstistä.\n" " " #: plugin.py:312 msgid "" "\n" "\n" " Returns the text un-URL quoted.\n" " " msgstr "" "\n" "\n" " Palauttaa tekstin URL lainaamattomassa muodossa.\n" " " #: plugin.py:324 msgid "" "\n" "\n" " Returns the contents of , or as much as is configured in\n" " supybot.plugins.Web.fetch.maximum. If that configuration variable " "is\n" " set to 0, this command will be effectively disabled.\n" " " msgstr "" "\n" "\n" " Palauttaa sisällön, tai niin paljon kuin on " "määritetty asetuksessa \n" " supybot.plugins.Web.fetch.maximum. Jos tuo asetusarvo on asetettu " "arvoon 0, \n" " tämä komento poistetaan käytöstä.\n" " " #: plugin.py:335 msgid "" "This command is disabled (supybot.plugins.Web.fetch.maximum is set to 0)." msgstr "" "Tämä komento on poistettu käytöstä (supybot.plugins.Web.fetch.maximum on " "asetettu arvoon 0)." #~ msgid "Page is too big." #~ msgstr "Sivu on liian suuri." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the HTML ... of a URL.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa tiedon ... URL-soitteesta.\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns Netcraft.com's determination of what operating system " #~ "and\n" #~ " webserver is running on the host given.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa Netcraft.comin määritelmän mitä käyttöjärjestelmää ja\n" #~ " verkkopalvelinta annettu isäntä käyttää.\n" #~ " " #~ msgid "No results found for %s." #~ msgstr "Tuloksia ei löytynyt kohteelle %s." #~ msgid "The format of page the was odd." #~ msgstr "Sivun muoto oli omituinen." limnoria-2018.01.25/plugins/Web/locales/de.po0000644000175000017500000001146513233426066020076 0ustar valval00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2012-02-15 23:19+CET\n" "PO-Revision-Date: 2012-04-27 15:47+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: German \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: de\n" #: config.py:50 msgid "" "Determines whether the bot will output the\n" " HTML title of URLs it sees in the channel." msgstr "Legt fest ob der Bot den HTML Titel, von URLs die er im Channel sieht, ausgibt." #: config.py:53 #, fuzzy msgid "" "Determines what URLs matching the given regexp\n" " will not be snarfed. Give the empty string if you have no URLs that you'd\n" " like to exclude from being snarfed." msgstr "Legt fest welche URLs im Kanal gefangen werden und in der Datenbank gespeichert werden; URLs die auf den regulären Ausdruck zutreffen werden nicht gefangen. Gebe eine leere Zeichenkette an, falls du keine URLs hast die for dem gefangen werden ausgeschlossen werden." #: config.py:59 msgid "" "Determines the maximum number of\n" " bytes the bot will download via the 'fetch' command in this plugin." msgstr "Legt die maximal Anzahl an Bytes fest die der Bot über den 'fetch' Befehl herunterläd." #: plugin.py:71 msgid "Add the help for \"help Web\" here." msgstr "Füge die Hilfe für \"help Web\" hier hinzu." #: plugin.py:107 msgid "Title: %s (at %s)" msgstr "Titel: %s (auf %s)" #: plugin.py:114 msgid "" "\n" "\n" " Returns the HTTP headers of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" "Gibt den HTTP Kopf von aus. Natürlich sind nur HTTP URLS zulässig." #: plugin.py:121 msgid "%s: %s" msgstr "%s: %s" #: plugin.py:131 msgid "" "\n" "\n" " Returns the DOCTYPE string of . Only HTTP urls are valid, of\n" " course.\n" " " msgstr "" "\n" "\n" "Gibt die DOCTYPE Zeichenkette von aus. Natürlich sind nur HTTP URLS zulässig" #: plugin.py:143 msgid "That URL has no specified doctype." msgstr "Diese URL hat doctype nicht spezifiziert." #: plugin.py:148 msgid "" "\n" "\n" " Returns the Content-Length header of . Only HTTP urls are valid,\n" " of course.\n" " " msgstr "" "\n" "\n" "Gibt Content-Length Kopf der aus. Natürlich sind nur HTTP URLs zulässig" #: plugin.py:157 #: plugin.py:162 msgid "%u is %S long." msgstr "%u ist %S lang." #: plugin.py:164 #, fuzzy msgid "The server didn't tell me how long %u is but it's longer than %S." msgstr "Der Server hat mir nicht gesagt wie lang %u ist, aber es ist länger als %S." #: plugin.py:173 msgid "" "\n" "\n" " Returns the HTML ... of a URL.\n" " " msgstr "" "\n" "\n" "Gibt den HTML ... einer URL aus." #: plugin.py:188 msgid "That URL appears to have no HTML title." msgstr "Es scheint so als habe die URL keinen HTML Titel." #: plugin.py:190 msgid "That URL appears to have no HTML title within the first %S." msgstr "Es scheint so als habe die URL keinen HTML Titel innerhalb der ersten %S." #: plugin.py:198 msgid "" "\n" "\n" " Returns Netcraft.com's determination of what operating system and\n" " webserver is running on the host given.\n" " " msgstr "" "\n" "\n" "Gibt die Vermutung von Netcraft.com, über das Betriebssystem und Webserver des gegeben Hosts, aus." #: plugin.py:212 msgid "No results found for %s." msgstr "Keine Ergebnisse für %s gefunden." #: plugin.py:214 msgid "The format of page the was odd." msgstr "Das Format der Seite ist komisch." #: plugin.py:219 msgid "" "\n" "\n" " Returns the URL quoted form of the text.\n" " " msgstr "" "\n" "\n" "Gibt die URL zitierte Form vom gegeben Text aus." #: plugin.py:228 msgid "" "\n" "\n" " Returns the text un-URL quoted.\n" " " msgstr "" "\n" "\n" "Gibt den Text nicht URL zitiert aus." #: plugin.py:238 msgid "" "\n" "\n" " Returns the contents of , or as much as is configured in\n" " supybot.plugins.Web.fetch.maximum. If that configuration variable is\n" " set to 0, this command will be effectively disabled.\n" " " msgstr "" "\n" "\n" "Gibt den Inhalt von aus. oder soviel wie in supybot.plugins.Web.fetch.maximum konfiguriert wurde. Falls diese Konfigurationsvariable auf 0 gesetzt ist, wird der Befehl praktisch deaktiviert." #: plugin.py:246 msgid "This command is disabled (supybot.plugins.Web.fetch.maximum is set to 0)." msgstr "Dieser Befehl ist abgeschaltet (supybot.plugins.Web.fetch.maximum ist auf 0 gesetzt)." limnoria-2018.01.25/plugins/Utilities/0000755000175000017500000000000013233426077016755 5ustar valval00000000000000limnoria-2018.01.25/plugins/Utilities/test.py0000644000175000017500000000751213233426066020311 0ustar valval00000000000000# -*- 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.utils.minisix import u from supybot.test import * class UtilitiesTestCase(PluginTestCase): plugins = ('Math', 'Utilities', 'String') def testIgnore(self): self.assertNoResponse('utilities ignore foo bar baz', 1) self.assertError('utilities ignore [re m/foo bar]') self.assertResponse('echo [utilities ignore foobar] qux', 'qux') def testSuccess(self): self.assertNotError('success 1') self.assertError('success [re m/foo bar]') def testLast(self): self.assertResponse('utilities last foo bar baz', 'baz') def testEcho(self): self.assertHelp('echo') self.assertResponse('echo foo', 'foo') self.assertResponse(u('echo 好'), '好') self.assertResponse(u('echo "好"'), '好') def testEchoDollarOneRepliesDollarOne(self): self.assertResponse('echo $1', '$1') def testEchoStandardSubstitute(self): self.assertNotRegexp('echo $nick', r'\$') def testEchoStripCtcp(self): self.assertResponse('echo \x01ACTION foo\x01', "ACTION foo") def testApply(self): self.assertResponse('apply "utilities last" a', 'a') self.assertResponse('apply "utilities last" a b', 'b') def testShuffle(self): self.assertResponse('shuffle a', 'a') def testSort(self): self.assertResponse('sort abc cab cba bca', 'abc bca cab cba') self.assertResponse('sort 2 12 42 7 2', '2 2 7 12 42') self.assertResponse('sort 2 8 12.2 12.11 42 7 2', '2 2 7 8 12.11 12.2 42') def testSample(self): self.assertResponse('sample 1 a', 'a') self.assertError('sample moo') self.assertError('sample 5 moo') self.assertRegexp('sample 2 a b c', '^[a-c] [a-c]$') def testCountargs(self): self.assertResponse('countargs a b c', '3') self.assertResponse('countargs a "b c"', '2') self.assertResponse('countargs', '0') def testLet(self): self.assertResponse('let x = 42 in echo foo $x bar', 'foo 42 bar') self.assertResponse( 'let y = 21 in "' 'let x = [math calc 2*[echo $y]] in ' 'echo foo $x bar"', 'foo 42 bar') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Utilities/plugin.py0000644000175000017500000001540113233426066020624 0ustar valval00000000000000### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010, James McCoy # 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 types import random from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Utilities') class Utilities(callbacks.Plugin): """Provides useful commands for bot scripting / command nesting.""" # Yes, I really do mean "requires no arguments" below. "takes no # arguments" would probably lead people to think it was a useless command. @internationalizeDocstring def ignore(self, irc, msg, args): """requires no arguments Does nothing. Useful sometimes for sequencing commands when you don't care about their non-error return values. """ msg.tag('ignored') irc.noReply() # Do be careful not to wrap this unless you do any('something'). @internationalizeDocstring def success(self, irc, msg, args, text): """[] Does nothing except to reply with a success message. This is useful when you want to run multiple commands as nested commands, and don't care about their output as long as they're successful. An error, of course, will break out of this command. , if given, will be appended to the end of the success message. """ irc.replySuccess(text) success = wrap(success, [additional('text')]) @internationalizeDocstring def last(self, irc, msg, args): """ [ ...] Returns the last argument given. Useful when you'd like multiple nested commands to run, but only the output of the last one to be returned. """ args = list(filter(None, args)) if args: irc.reply(args[-1]) else: raise callbacks.ArgumentError @internationalizeDocstring def echo(self, irc, msg, args, text): """ Returns the arguments given it. Uses our standard substitute on the string(s) given to it; $nick (or $who), $randomNick, $randomInt, $botnick, $channel, $user, $host, $today, $now, and $randomDate are all handled appropriately. """ text = ircutils.standardSubstitute(irc, msg, text) irc.reply(text, prefixNick=False) echo = wrap(echo, ['text']) @internationalizeDocstring def shuffle(self, irc, msg, args, things): """ [ ...] Shuffles the arguments given. """ random.shuffle(things) irc.reply(' '.join(things)) shuffle = wrap(shuffle, [many('anything')]) @internationalizeDocstring def sort(self, irc, msg, args, things): """ [ ...] Sorts the arguments given. """ irc.reply(' '.join(map(str, sorted(things)))) # Keep ints as ints, floats as floats, without comparing between numbers # and strings. sort = wrap(sort, [first(many(first('int', 'float')), many('anything'))]) @internationalizeDocstring def sample(self, irc, msg, args, num, things): """ [ ...] Randomly chooses items out of the arguments given. """ try: samp = random.sample(things, num) irc.reply(' '.join(samp)) except ValueError as e: irc.error('%s' % (e,)) sample = wrap(sample, ['positiveInt', many('anything')]) @internationalizeDocstring def countargs(self, irc, msg, args, things): """ [ ...] Counts the arguments given. """ irc.reply(len(things)) countargs = wrap(countargs, [any('anything')]) @internationalizeDocstring def apply(self, irc, msg, args, command, rest): """ Tokenizes and calls with the resulting arguments. """ args = [token and token or '""' for token in rest] text = ' '.join(args) commands = command.split() commands = list(map(callbacks.canonicalName, commands)) tokens = callbacks.tokenize(text) allTokens = commands + tokens self.Proxy(irc, msg, allTokens) apply = wrap(apply, ['something', many('something')]) def let(self, irc, msg, args, var_name, _, value, __, command): """ = in Defines to be equal to in the and runs the . '=' and 'in' can be omitted.""" if msg.reply_env and var_name in msg.reply_env: # For security reason (eg. a Sudo-like plugin), we don't want # to make it possible to override stuff like $nick. irc.error(_('Cannot set a variable that already exists.'), Raise=True) fake_msg = ircmsgs.IrcMsg(msg=msg) if fake_msg.reply_env is None: fake_msg.reply_env = {} fake_msg.reply_env[var_name] = value tokens = callbacks.tokenize(command) self.Proxy(irc, fake_msg, tokens) let = wrap(let, [ 'something', optional(('literal', ['='])), 'something', optional(('literal', ['in'])), 'text']) Class = Utilities # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Utilities/config.py0000644000175000017500000000470113233426066020574 0ustar valval00000000000000### # Copyright (c) 2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Utilities') 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('Utilities', True) Utilities = conf.registerPlugin('Utilities') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Utilities, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Utilities/__init__.py0000644000175000017500000000440613233426066021070 0ustar valval00000000000000### # Copyright (c) 2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Various utility commands, mostly useful for manipulating nested commands. """ 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__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from imp import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Utilities/locales/0000755000175000017500000000000013233426077020377 5ustar valval00000000000000limnoria-2018.01.25/plugins/Utilities/locales/it.po0000644000175000017500000000730313233426066021354 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2011-06-15 18:37+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plugin.py:45 #, docstring msgid "" "requires no arguments\n" "\n" " Does nothing. Useful sometimes for sequencing commands when you don't\n" " care about their non-error return values.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Non fa niente. Utile per eseguire comandi in sequenza quando non ci si\n" " cura del valore dello stato di uscita restituito.\n" " " #: plugin.py:59 #, docstring msgid "" "[]\n" "\n" " Does nothing except to reply with a success message. This is useful\n" " when you want to run multiple commands as nested commands, and don't\n" " care about their output as long as they're successful. An error, of\n" " course, will break out of this command. , if given, will be\n" " appended to the end of the success message.\n" " " msgstr "" "[]\n" "\n" " Non fa nient'altro che rispondere con un messaggio di successo. Utile\n" " quando si vuole eseguire comandi multipli come nidificati, e non ci si\n" " cura del loro output finché questi riescono con successo. Un errore,\n" " naturalmente, interromperà questo comando. , se fornito, sarà\n" " aggiunto alla fine del messaggio di successo.\n" " " #: plugin.py:72 #, docstring msgid "" " [ ...]\n" "\n" " Returns the last argument given. Useful when you'd like multiple\n" " nested commands to run, but only the output of the last one to be\n" " returned.\n" " " msgstr "" " [ ...]\n" "\n" " Restituisce l'ultimo argomento dato. Utile quando si vogliono eseguire\n" " comandi nidificati ottenendo solo l'output dell'ultimo.\n" " " #: plugin.py:86 #, docstring msgid "" "\n" "\n" " Returns the arguments given it. Uses our standard substitute on the\n" " string(s) given to it; $nick (or $who), $randomNick, $randomInt,\n" " $botnick, $channel, $user, $host, $today, $now, and $randomDate are all\n" " handled appropriately.\n" " " msgstr "" "\n" "\n" " Restituisce gli argomenti dati. Utilizza il nostro sistema di sostituzione\n" " standard con la stringa fornita; $nick (o $who), $randomNick, $randomInt, $botnick,\n" " $channel, $user, $host, $today, $now e $randomDate sono tutte gestite correttamente.\n" " " #: plugin.py:99 #, docstring msgid "" " [ ...]\n" "\n" " Shuffles the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Mescola gli argomenti forniti.\n" " " #: plugin.py:109 #, docstring msgid "" " [ ...]\n" "\n" " Randomly chooses items out of the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Sceglie in modo casuale un certo di argomenti.\n" " " #: plugin.py:122 #, docstring msgid "" " [ ...]\n" "\n" " Counts the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Conta gli argomenti forniti.\n" " " #: plugin.py:131 #, docstring msgid "" " \n" "\n" " Tokenizes and calls with the resulting arguments.\n" " " msgstr "" " \n" "\n" " Tokenizza e chiama con gli argomenti risultanti.\n" " " limnoria-2018.01.25/plugins/Utilities/locales/fr.po0000644000175000017500000000700713233426066021350 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-20 11:29+CET\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:45 msgid "" "requires no arguments\n" "\n" " Does nothing. Useful sometimes for sequencing commands when you don't\n" " care about their non-error return values.\n" " " msgstr "" "ne requiert pas d'argument\n" "\n" "Ne fait rien. Utile pour séquencer les commandes lorsque vous vous fichez de leur valeur de retour." #: plugin.py:59 msgid "" "[]\n" "\n" " Does nothing except to reply with a success message. This is useful\n" " when you want to run multiple commands as nested commands, and don't\n" " care about their output as long as they're successful. An error, of\n" " course, will break out of this command. , if given, will be\n" " appended to the end of the success message.\n" " " msgstr "" "[]\n" "\n" "Ne fait rien excepté répondre avec un message de succès. C'est utile lorsque vous voulez lancer plusieurs commandes comme commandes imbriquées, et vous ne voulez pas vous occuper de leur retour, du moment que ce n'est pas une erreur. Si c'est une erreur, bien sûr, la commande s'arrêtera. , si il est donné, sera ajouté à la fin du message de succès." #: plugin.py:72 msgid "" " [ ...]\n" "\n" " Returns the last argument given. Useful when you'd like multiple\n" " nested commands to run, but only the output of the last one to be\n" " returned.\n" " " msgstr "" " [ ...]\n" "\n" "Retourne le dernier argument de la commande. Utile lorsque vous avez plusieurs commandes imbriquées, mais seulement la sortie de la dernière sera retournée." #: plugin.py:86 msgid "" "\n" "\n" " Returns the arguments given it. Uses our standard substitute on the\n" " string(s) given to it; $nick (or $who), $randomNick, $randomInt,\n" " $botnick, $channel, $user, $host, $today, $now, and $randomDate are all\n" " handled appropriately.\n" " " msgstr "" "\n" "\n" "Retourne les arguments donnés. Utilise notre système de substitution standard sur la/les chaîne(s) qui lui est donnée ; $nick (ou $who), $randomNick, $randomInt, $botnick, $channel, $user, $host, $today, $now, et $randomDate sont tous gérés correctement." #: plugin.py:99 msgid "" " [ ...]\n" "\n" " Shuffles the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" "Mélange les arguments donnés." #: plugin.py:109 msgid "" " [ ...]\n" "\n" " Randomly chooses items out of the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" "Choisi de façon aléatoire un certain d's." #: plugin.py:122 msgid "" " [ ...]\n" "\n" " Counts the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" "Retorune le nombre d's donnés." #: plugin.py:131 msgid "" " \n" "\n" " Tokenizes and calls with the resulting arguments.\n" " " msgstr "" " \n" "\n" "Tokénise le et appelle la commande avec l'argument résultant." limnoria-2018.01.25/plugins/Utilities/locales/fi.po0000644000175000017500000001065213233426066021337 0ustar valval00000000000000# Utilities plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Utilities plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 13:30+EET\n" "PO-Revision-Date: 2014-12-20 13:58+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: 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.6.10\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: plugin.py:41 #, fuzzy msgid "Provides useful commands for bot scripting / command nesting." msgstr "" "Tarjoaa hyödyllisiä komentoja botin skriptaukseen / komentoja laittamiseen " "sisäkkäisiksi." #: plugin.py:46 msgid "" "requires no arguments\n" "\n" " Does nothing. Useful sometimes for sequencing commands when you " "don't\n" " care about their non-error return values.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Ei tee mitään. Hyödyllinen komentojen suorittamiseen ketjussa, kun " "et\n" " välitä niiden ei-virhe arvoista.\n" " " #: plugin.py:60 msgid "" "[]\n" "\n" " Does nothing except to reply with a success message. This is " "useful\n" " when you want to run multiple commands as nested commands, and " "don't\n" " care about their output as long as they're successful. An error, " "of\n" " course, will break out of this command. , if given, will be\n" " appended to the end of the success message.\n" " " msgstr "" "[]\n" "\n" " Ei tee mitään muuta, kuin vastaa onnistumisviestillä. Tämä on " "hyödyllinen, kun\n" " haluat suorittaa monta komentoa sisäkkäisinä komentoina, ja et\n" " välitä niiden ulostuloista niin kauan, kuin ne ovat onnistuneita. " "Virheilmoitus, tietenkin, \n" " murtaa tämän komennon. , lisätään, jos se on annettu, \n" " onnistumisviestin perään.\n" " " #: plugin.py:73 msgid "" " [ ...]\n" "\n" " Returns the last argument given. Useful when you'd like multiple\n" " nested commands to run, but only the output of the last one to be\n" " returned.\n" " " msgstr "" " [ ...]\n" "\n" " Palauttaa viimeisen annetun parametrin. Hyödyllinen, kun haluat " "monen sisäkkäisen komennon tulevan suoritetuksi, mutta\n" " vastaanottaa vain niistä viimeisen\n" " ulostulon.\n" " " #: plugin.py:87 msgid "" "\n" "\n" " Returns the arguments given it. Uses our standard substitute on " "the\n" " string(s) given to it; $nick (or $who), $randomNick, $randomInt,\n" " $botnick, $channel, $user, $host, $today, $now, and $randomDate are " "all\n" " handled appropriately.\n" " " msgstr "" "\n" "\n" " Palauttaa parametrit, joita sille on annettu. Käyttää " "perusmuunnoksia \n" " merkkiketju(issa), jotka on annettu sille; $nick (tai $who), " "$randomNick, $randomInt,\n" " $botnick, $channel, $user, $host, $today, $now, and $randomDate " "hoidetaan kaikki\n" " oikealla tavalla.\n" " " #: plugin.py:100 msgid "" " [ ...]\n" "\n" " Shuffles the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Sekoittaa annetut parametrit keskenään.\n" " " #: plugin.py:110 msgid "" " [ ...]\n" "\n" " Sorts the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Lajittelee annetut parametrit.\n" " " #: plugin.py:121 msgid "" " [ ...]\n" "\n" " Randomly chooses items out of the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Sattumanvaraisesti valitsee verran parametrejä annetuista " "parametreistä.\n" " " #: plugin.py:134 msgid "" " [ ...]\n" "\n" " Counts the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" " Laskee annetut parametrit.\n" " " #: plugin.py:143 msgid "" " \n" "\n" " Tokenizes and calls with the resulting arguments.\n" " " msgstr "" " \n" "\n" " Tokenizoi ja kutsuu tuloksena olevilla " "parametreillä.\n" " " limnoria-2018.01.25/plugins/Utilities/locales/de.po0000644000175000017500000000564413233426066021336 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2011-10-30 17:46+0100\n" "Last-Translator: Florian Besser \n" "Language-Team: German \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 "" "requires no arguments\n" "\n" " Does nothing. Useful sometimes for sequencing commands when you don't\n" " care about their non-error return values.\n" " " msgstr "" "benötigt keine Argumente\n" "\n" "Tut nichts. Manchmal nützlich um Befehle aneinanderzuketten, wenn die Rückgabewerte der Befehle egal ist." #: plugin.py:59 msgid "" "[]\n" "\n" " Does nothing except to reply with a success message. This is useful\n" " when you want to run multiple commands as nested commands, and don't\n" " care about their output as long as they're successful. An error, of\n" " course, will break out of this command. , if given, will be\n" " appended to the end of the success message.\n" " " msgstr "" #: plugin.py:72 msgid "" " [ ...]\n" "\n" " Returns the last argument given. Useful when you'd like multiple\n" " nested commands to run, but only the output of the last one to be\n" " returned.\n" " " msgstr "" #: plugin.py:86 msgid "" "\n" "\n" " Returns the arguments given it. Uses our standard substitute on the\n" " string(s) given to it; $nick (or $who), $randomNick, $randomInt,\n" " $botnick, $channel, $user, $host, $today, $now, and $randomDate are all\n" " handled appropriately.\n" " " msgstr "" "\n" "\n" "Gibt die angegeben Argumente an. Wendet unsere Standardsubstitution auf die Zeichenketten an: $nick (or $who), $randomNick, $randomInt, $botnick, $channel, $user, $host, $today, $now, and $randomDate werden behandelt." #: plugin.py:99 msgid "" " [ ...]\n" "\n" " Shuffles the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" "Mischt die angegebenen Argumente" #: plugin.py:109 msgid "" " [ ...]\n" "\n" " Randomly chooses items out of the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" "Wählt zufällig bestandteile aus den angegeben Argumenten aus." #: plugin.py:122 msgid "" " [ ...]\n" "\n" " Counts the arguments given.\n" " " msgstr "" " [ ...]\n" "\n" "Zählt die angegeben Argumente." #: plugin.py:131 msgid "" " \n" "\n" " Tokenizes and calls with the resulting arguments.\n" " " msgstr "" " \n" "\n" "Bricht den auseinander und ruft mit den resultierenden Argumenten auf." limnoria-2018.01.25/plugins/User/0000755000175000017500000000000013233426077015720 5ustar valval00000000000000limnoria-2018.01.25/plugins/User/test.py0000644000175000017500000002116613233426066017255 0ustar valval00000000000000### # 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. ### import re from supybot.test import PluginTestCase, network import supybot.conf as conf import supybot.world as world import supybot.ircdb as ircdb import supybot.utils as utils class UserTestCase(PluginTestCase): plugins = ('User', 'Admin', 'Config') prefix1 = 'somethingElse!user@host1.tld' prefix2 = 'EvensomethingElse!user@host2.tld' prefix3 = 'Completely!Different@host3.tld__no_testcap__' def testHostmaskList(self): self.assertError('hostmask list') original = self.prefix self.prefix = self.prefix1 self.assertNotError('register foo bar') self.prefix = original self.assertError('hostmask list foo') self.assertNotError('hostmask add foo [hostmask] bar') self.assertNotError('hostmask add foo') self.assertNotRegexp('hostmask add foo', 'IrcSet') def testHostmaskListHandlesEmptyListGracefully(self): self.assertError('hostmask list') self.prefix = self.prefix1 self.assertNotError('register foo bar') self.assertNotError('hostmask remove foo %s' % self.prefix1) self.assertNotError('identify foo bar') self.assertRegexp('hostmask list', 'no registered hostmasks') def testHostmaskOverlap(self): self.assertNotError('register foo passwd', frm=self.prefix1) self.assertNotError('register bar passwd', frm=self.prefix2) self.assertResponse('whoami', 'foo', frm=self.prefix1) self.assertResponse('whoami', 'bar', frm=self.prefix2) self.assertNotError('hostmask add foo *!*@foobar/b', frm=self.prefix1) self.assertResponse('hostmask add bar *!*@foobar/*', 'Error: That hostmask is already registered to foo.', frm=self.prefix2) self.assertRegexp('hostmask list foo', '\*!\*@foobar/b', frm=self.prefix1) self.assertNotRegexp('hostmask list bar', 'foobar', frm=self.prefix2) def testHostmaskOverlapPrivacy(self): self.assertNotError('register foo passwd', frm=self.prefix1) self.assertNotError('register bar passwd', frm=self.prefix3) self.assertResponse('whoami', 'foo', frm=self.prefix1) self.assertResponse('whoami', 'bar', frm=self.prefix3) self.assertNotError('hostmask add foo *!*@foobar/b', frm=self.prefix1) ircdb.users.getUser('bar').addCapability('owner') self.assertResponse('whoami', 'bar', frm=self.prefix3) self.assertResponse('capabilities', '[owner]', frm=self.prefix3) self.assertResponse('hostmask add *!*@foobar/*', 'Error: That hostmask is already registered to foo.', frm=self.prefix3) ircdb.users.getUser('bar').removeCapability('owner') self.assertResponse('hostmask add *!*@foobar/*', 'Error: That hostmask is already registered.', frm=self.prefix3) def testHostmask(self): self.assertResponse('hostmask', self.prefix) self.assertError('@hostmask asdf') m = self.irc.takeMsg() self.failIf(m is not None, m) def testRegisterUnregister(self): self.prefix = self.prefix1 self.assertNotError('register foo bar') self.assertError('register foo baz') self.failUnless(ircdb.users.getUserId('foo')) self.assertError('unregister foo') self.assertNotError('unregister foo bar') self.assertRaises(KeyError, ircdb.users.getUserId, 'foo') def testDisallowedUnregistration(self): self.prefix = self.prefix1 self.assertNotError('register foo bar') orig = conf.supybot.databases.users.allowUnregistration() conf.supybot.databases.users.allowUnregistration.setValue(False) try: self.assertError('unregister foo') m = self.irc.takeMsg() self.failIf(m is not None, m) self.failUnless(ircdb.users.getUserId('foo')) finally: conf.supybot.databases.users.allowUnregistration.setValue(orig) def testList(self): self.prefix = self.prefix1 self.assertNotError('register foo bar') self.assertResponse('user list', 'foo') self.prefix = self.prefix2 self.assertNotError('register biff quux') self.assertResponse('user list', 'biff and foo') self.assertRegexp('user list --capability testcap', 'no matching') self.assertNotError('admin capability add biff testcap') self.assertResponse('user list --capability testcap', 'biff') self.assertNotError('config capabilities.private testcap') self.assertRegexp('user list --capability testcap', 'Error:.*private') self.assertNotError('admin capability add biff admin') self.assertResponse('user list --capability testcap', 'biff') self.assertNotError('admin capability remove biff admin') self.assertRegexp('user list --capability testcap', 'Error:.*private') self.assertNotError('config capabilities.private ""') self.assertResponse('user list --capability testcap', 'biff') self.assertNotError('admin capability remove biff testcap') self.assertRegexp('user list --capability testcap', 'no matching') self.assertResponse('user list f', 'biff and foo') self.assertResponse('user list f*', 'foo') self.assertResponse('user list *f', 'biff') self.assertNotError('unregister biff quux') self.assertResponse('user list', 'foo') self.assertNotError('unregister foo bar') self.assertRegexp('user list', 'no registered users') self.assertRegexp('user list asdlfkjasldkj', 'no matching registered') def testListHandlesCaps(self): self.prefix = self.prefix1 self.assertNotError('register Foo bar') self.assertResponse('user list', 'Foo') self.assertResponse('user list f*', 'Foo') def testChangeUsername(self): self.prefix = self.prefix1 self.assertNotError('register foo bar') self.prefix = self.prefix2 self.assertNotError('register bar baz') self.prefix = self.prefix1 self.assertError('changename foo bar') self.assertNotError('changename foo baz') def testSetpassword(self): self.prefix = self.prefix1 self.assertNotError('register foo bar') password = ircdb.users.getUser(self.prefix).password self.assertNotEqual(password, 'bar') self.assertNotError('set password foo bar baz') self.assertNotEqual(ircdb.users.getUser(self.prefix).password,password) self.assertNotEqual(ircdb.users.getUser(self.prefix).password, 'baz') def testStats(self): self.assertNotError('user stats') self.assertNotError('load Lart') self.assertNotError('user stats') def testUserPluginAndUserList(self): self.prefix = self.prefix1 self.assertNotError('register Foo bar') self.assertResponse('user list', 'Foo') self.assertNotError('load Seen') self.assertResponse('user list', 'Foo') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/User/plugin.py0000644000175000017500000005355513233426066017603 0ustar valval00000000000000### # 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. ### import re import sys import fnmatch import supybot.conf as conf import supybot.gpg as gpg import supybot.utils as utils import supybot.ircdb as ircdb from supybot.commands import * import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('User') class User(callbacks.Plugin): """Provides commands for dealing with users, such as registration and authentication to the bot. This is a core Supybot plugin that should not be removed!""" def _checkNotChannel(self, irc, msg, password=' '): if password and irc.isChannel(msg.args[0]): raise callbacks.Error(conf.supybot.replies.requiresPrivacy()) @internationalizeDocstring def list(self, irc, msg, args, optlist, glob): """[--capability=] [] Returns the valid registered usernames matching . If is not given, returns all registered usernames. """ predicates = [] for (option, arg) in optlist: if option == 'capability': if arg in conf.supybot.capabilities.private(): try: u = ircdb.users.getUser(msg.prefix) if not u._checkCapability('admin'): raise KeyError except KeyError: # Note that it may be raised by checkCapability too. irc.error(_('This is a private capability. Only admins ' 'can see who has it.'), Raise=True) def p(u, cap=arg): try: return u._checkCapability(cap) except KeyError: return False predicates.append(p) if glob: r = re.compile(fnmatch.translate(glob), re.I) def p(u): return r.match(u.name) is not None predicates.append(p) users = [] for u in ircdb.users.values(): for predicate in predicates: if not predicate(u): break else: users.append(u.name) if users: utils.sortBy(str.lower, users) private = self.registryValue("listInPrivate", msg.args[0]) irc.reply(format('%L', users), private=private) else: if predicates: irc.reply(_('There are no matching registered users.')) else: irc.reply(_('There are no registered users.')) list = wrap(list, [getopts({'capability':'capability'}), additional('glob')]) @internationalizeDocstring def register(self, irc, msg, args, name, password): """ Registers with the given password and the current hostmask of the person registering. You shouldn't register twice; if you're not recognized as a user but you've already registered, use the hostmask add command to add another hostmask to your already-registered user, or use the identify command to identify just for a session. This command (and all other commands that include a password) must be sent to the bot privately, not in a channel. """ addHostmask = True try: ircdb.users.getUserId(name) irc.error(_('That name is already assigned to someone.'), Raise=True) except KeyError: pass if ircutils.isUserHostmask(name): irc.errorInvalid(_('username'), name, _('Hostmasks are not valid usernames.'), Raise=True) try: u = ircdb.users.getUser(msg.prefix) if u._checkCapability('owner'): addHostmask = False else: irc.error(_('Your hostmask is already registered to %s') % u.name) return except KeyError: pass user = ircdb.users.newUser() user.name = name user.setPassword(password) if addHostmask: user.addHostmask(msg.prefix) ircdb.users.setUser(user) irc.replySuccess() register = wrap(register, ['private', 'something', 'something']) @internationalizeDocstring def unregister(self, irc, msg, args, user, password): """ [] Unregisters from the user database. If the user giving this command is an owner user, the password is not necessary. """ try: caller = ircdb.users.getUser(msg.prefix) isOwner = caller._checkCapability('owner') except KeyError: caller = None isOwner = False if not conf.supybot.databases.users.allowUnregistration(): if not caller or not isOwner: self.log.warning('%s tried to unregister user %s.', msg.prefix, user.name) irc.error(_('This command has been disabled. You\'ll have to ' 'ask the owner of this bot to unregister your ' 'user.'), Raise=True) if isOwner or user.checkPassword(password): ircdb.users.delUser(user.id) irc.replySuccess() else: irc.error(conf.supybot.replies.incorrectAuthentication()) unregister = wrap(unregister, ['private', 'otherUser', additional('anything')]) @internationalizeDocstring def changename(self, irc, msg, args, user, newname, password): """ [] Changes your current user database name to the new name given. is only necessary if the user isn't recognized by hostmask. This message must be sent to the bot privately (not on a channel) since it may contain a password. """ try: id = ircdb.users.getUserId(newname) irc.error(format(_('%q is already registered.'), newname)) return except KeyError: pass if user.checkHostmask(msg.prefix) or user.checkPassword(password): user.name = newname ircdb.users.setUser(user) irc.replySuccess() changename = wrap(changename, ['private', 'otherUser', 'something', additional('something', '')]) class set(callbacks.Commands): @internationalizeDocstring def password(self, irc, msg, args, user, password, newpassword): """[] Sets the new password for the user specified by to . Obviously this message must be sent to the bot privately (not in a channel). If the requesting user is an owner user, then needn't be correct. """ try: u = ircdb.users.getUser(msg.prefix) except KeyError: u = None if user is None: if u is None: irc.errorNotRegistered(Raise=True) user = u if user.checkPassword(password) or \ (u and u._checkCapability('owner')): user.setPassword(newpassword) ircdb.users.setUser(user) irc.replySuccess() else: irc.error(conf.supybot.replies.incorrectAuthentication()) password = wrap(password, ['private', optional('otherUser'), 'something', 'something']) @internationalizeDocstring def secure(self, irc, msg, args, user, password, value): """ [] Sets the secure flag on the user of the person sending the message. Requires that the person's hostmask be in the list of hostmasks for that user in addition to the password being correct. When the secure flag is set, the user *must* identify before they can be recognized. If a specific True/False value is not given, it inverts the current value. """ if value is None: value = not user.secure if user.checkPassword(password) and \ user.checkHostmask(msg.prefix, useAuth=False): user.secure = value ircdb.users.setUser(user) irc.reply(_('Secure flag set to %s') % value) else: irc.error(conf.supybot.replies.incorrectAuthentication()) secure = wrap(secure, ['private', 'user', 'something', additional('boolean')]) @internationalizeDocstring def username(self, irc, msg, args, hostmask): """ Returns the username of the user specified by or if the user is registered. """ if ircutils.isNick(hostmask): try: hostmask = irc.state.nickToHostmask(hostmask) except KeyError: irc.error(_('I haven\'t seen %s.') % hostmask, Raise=True) try: user = ircdb.users.getUser(hostmask) irc.reply(user.name) except KeyError: irc.error(_('I don\'t know who that is.')) username = wrap(username, [first('nick', 'hostmask')]) class hostmask(callbacks.Commands): @internationalizeDocstring def hostmask(self, irc, msg, args, nick): """[] Returns the hostmask of . If isn't given, return the hostmask of the person giving the command. """ if not nick: nick = msg.nick irc.reply(irc.state.nickToHostmask(nick)) hostmask = wrap(hostmask, [additional('seenNick')]) @internationalizeDocstring def list(self, irc, msg, args, name): """[] Returns the hostmasks of the user specified by ; if isn't specified, returns the hostmasks of the user calling the command. """ def getHostmasks(user): hostmasks = list(map(repr, user.hostmasks)) if hostmasks: hostmasks.sort() return format('%L', hostmasks) else: return format(_('%s has no registered hostmasks.'), user.name) try: user = ircdb.users.getUser(msg.prefix) if name: if name != user.name and \ not ircdb.checkCapability(msg.prefix, 'owner'): irc.error(_('You may only retrieve your own ' 'hostmasks.'), Raise=True) else: try: user = ircdb.users.getUser(name) irc.reply(getHostmasks(user), private=True) except KeyError: irc.errorNoUser() else: irc.reply(getHostmasks(user), private=True) except KeyError: irc.errorNotRegistered() list = wrap(list, [additional('something')]) @internationalizeDocstring def add(self, irc, msg, args, user, hostmask, password): """[] [] [] Adds the hostmask to the user specified by . The may only be required if the user is not recognized by hostmask. is also not required if an owner user is giving the command on behalf of some other user. If is not given, it defaults to your current hostmask. If is not given, it defaults to your currently identified name. This message must be sent to the bot privately (not on a channel) since it may contain a password. """ caller_is_owner = ircdb.checkCapability(msg.prefix, 'owner') if not hostmask: hostmask = msg.prefix if not ircutils.isUserHostmask(hostmask): irc.errorInvalid(_('hostmask'), hostmask, _('Make sure your hostmask includes a nick, ' 'then an exclamation point (!), then a user, ' 'then an at symbol (@), then a host. Feel ' 'free to use wildcards (* and ?, which work ' 'just like they do on the command line) in ' 'any of these parts.'), Raise=True) try: otherId = ircdb.users.getUserId(hostmask) if otherId != user.id: if caller_is_owner: err = _('That hostmask is already registered to %s.') err %= otherId else: err = _('That hostmask is already registered.') irc.error(err, Raise=True) except KeyError: pass if not user.checkPassword(password) and \ not user.checkHostmask(msg.prefix) and \ not caller_is_owner: irc.error(conf.supybot.replies.incorrectAuthentication(), Raise=True) try: user.addHostmask(hostmask) except ValueError as e: irc.error(str(e), Raise=True) try: ircdb.users.setUser(user) except ircdb.DuplicateHostmask as e: user.removeHostmask(hostmask) if caller_is_owner: err = _('That hostmask is already registered to %s.') \ % e.args[0] else: err = _('That hostmask is already registered.') irc.error(err, Raise=True) except ValueError as e: irc.error(str(e), Raise=True) irc.replySuccess() add = wrap(add, ['private', first('otherUser', 'user'), optional('something'), additional('something', '')]) @internationalizeDocstring def remove(self, irc, msg, args, user, hostmask, password): """[] [] [] Removes the hostmask from the record of the user specified by . If the hostmask given is 'all' then all hostmasks will be removed. The may only be required if the user is not recognized by their hostmask. This message must be sent to the bot privately (not on a channel) since it may contain a password. If is not given, it defaults to your current hostmask. If is not given, it defaults to your currently identified name. """ if not hostmask: hostmask = msg.prefix if not user.checkPassword(password) and \ not user.checkHostmask(msg.prefix): if not ircdb.checkCapability(msg.prefix, 'owner'): irc.error(conf.supybot.replies.incorrectAuthentication()) return try: s = '' if hostmask == 'all': user.hostmasks.clear() s = _('All hostmasks removed.') else: user.removeHostmask(hostmask) except KeyError: irc.error(_('There was no such hostmask.')) return ircdb.users.setUser(user) irc.replySuccess(s) remove = wrap(remove, ['private', first('otherUser', 'user'), optional('something'), additional('something', '')]) def callCommand(self, command, irc, msg, *args, **kwargs): if command[0] != 'gpg' or \ (gpg.available and self.registryValue('gpg.enable')): return super(User, self) \ .callCommand(command, irc, msg, *args, **kwargs) else: irc.error(_('GPG features are not enabled.')) @internationalizeDocstring def capabilities(self, irc, msg, args, user): """[] Returns the capabilities of the user specified by ; if isn't specified, returns the capabilities of the user calling the command. """ try: u = ircdb.users.getUser(msg.prefix) except KeyError: irc.errorNotRegistered() else: if u == user or u._checkCapability('admin'): irc.reply('[%s]' % '; '.join(user.capabilities), private=True) else: irc.error(conf.supybot.replies.incorrectAuthentication(), Raise=True) capabilities = wrap(capabilities, [first('otherUser', 'user')]) @internationalizeDocstring def identify(self, irc, msg, args, user, password): """ Identifies the user as . This command (and all other commands that include a password) must be sent to the bot privately, not in a channel. """ if user.checkPassword(password): try: user.addAuth(msg.prefix) ircdb.users.setUser(user, flush=False) irc.replySuccess() except ValueError: irc.error(_('Your secure flag is true and your hostmask ' 'doesn\'t match any of your known hostmasks.')) else: self.log.warning('Failed identification attempt by %s (password ' 'did not match for %s).', msg.prefix, user.name) irc.error(conf.supybot.replies.incorrectAuthentication()) identify = wrap(identify, ['private', 'otherUser', 'something']) @internationalizeDocstring def unidentify(self, irc, msg, args, user): """takes no arguments Un-identifies you. Note that this may not result in the desired effect of causing the bot not to recognize you anymore, since you may have added hostmasks to your user that can cause the bot to continue to recognize you. """ user.clearAuth() ircdb.users.setUser(user) irc.replySuccess(_('If you remain recognized after giving this command, ' 'you\'re being recognized by hostmask, rather than ' 'by password. You must remove whatever hostmask is ' 'causing you to be recognized in order not to be ' 'recognized.')) unidentify = wrap(unidentify, ['user']) @internationalizeDocstring def whoami(self, irc, msg, args): """takes no arguments Returns the name of the user calling the command. """ try: user = ircdb.users.getUser(msg.prefix) irc.reply(user.name) except KeyError: error = self.registryValue('customWhoamiError') or \ _('I don\'t recognize you. You can message me either of these two commands: "user identify " to log in or "user register " to register.') irc.reply(error) whoami = wrap(whoami) @internationalizeDocstring def stats(self, irc, msg, args): """takes no arguments Returns some statistics on the user database. """ users = 0 owners = 0 admins = 0 hostmasks = 0 for user in ircdb.users.values(): users += 1 hostmasks += len(user.hostmasks) try: if user._checkCapability('owner'): owners += 1 elif user._checkCapability('admin'): admins += 1 except KeyError: pass irc.reply(format(_('I have %s registered users ' 'with %s registered hostmasks; ' '%n and %n.'), users, hostmasks, (owners, 'owner'), (admins, 'admin'))) stats = wrap(stats) Class = User # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/User/config.py0000644000175000017500000000517413233426066017544 0ustar valval00000000000000### # 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('User') 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('User', True) User = conf.registerPlugin('User') conf.registerChannelValue(User, 'listInPrivate', registry.Boolean(True, _("""Determines whether the output of 'user list' will be sent in private. This prevents mass-highlights of people who use their nick as their bot username."""))) conf.registerGlobalValue(User, 'customWhoamiError', registry.String("", _("""Determines what message the bot sends when a user isn't identified or recognized."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/User/__init__.py0000644000175000017500000000432213233426066020030 0ustar valval00000000000000### # 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. ### """ Provides commands useful to users in general. This plugin is loaded by default. """ 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__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from imp import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure limnoria-2018.01.25/plugins/User/locales/0000755000175000017500000000000013233426077017342 5ustar valval00000000000000limnoria-2018.01.25/plugins/User/locales/it.po0000644000175000017500000003310013233426066020311 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2014-07-05 00:10+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: plugin.py:49 #, docstring msgid "" "[--capability=] []\n" "\n" " Returns the valid registered usernames matching . If is\n" " not given, returns all registered usernames.\n" " " msgstr "" "[--capability=] []\n" "\n" " Restituisce i nomi utente registrati che corrispondono a ; se\n" " quest'ultimo non è specificato, riporta i nomi di tutti gli utenti registrati.\n" " " #: plugin.py:80 msgid "There are no matching registered users." msgstr "Non ci sono utenti registrati corrispondenti." #: plugin.py:82 msgid "There are no registered users." msgstr "Non ci sono utenti registrati." #: plugin.py:88 #, docstring msgid "" " \n" "\n" " Registers with the given password and the current\n" " hostmask of the person registering. You shouldn't register twice; if\n" " you're not recognized as a user but you've already registered, use the\n" " hostmask add command to add another hostmask to your already-registered\n" " user, or use the identify command to identify just for a session.\n" " This command (and all other commands that include a password) must be\n" " sent to the bot privately, not in a channel.\n" " " msgstr "" " \n" "\n" " Registra con la data e l'hostmask attuale dell'utente.\n" " Non bisogna registrarsi due volte; se non si è riconosciuti come utenti ma\n" " ci si è gia registrati, utilizzare il comando \"hostmask add\" per aggiungere\n" " un'altra hostmask per lo stesso utente oppure utilizzare il comando \"identify\"\n" " per identificarsi solo per la sessione in corso. Questo comando (e tutti quelli\n" " che includono una password) devono essere inviati al bot privatamente, mai in canale.\n" " " #: plugin.py:101 msgid "That name is already assigned to someone." msgstr "Questo nome è già assegnato a qualcuno." #: plugin.py:106 msgid "username" msgstr "nome utente" #: plugin.py:107 msgid "Hostmasks are not valid usernames." msgstr "Le hostmask non sono nomi utente validi." #: plugin.py:114 msgid "Your hostmask is already registered to %s" msgstr "La tua hostmask è già registrata a %s" #: plugin.py:130 #, docstring msgid "" " []\n" "\n" " Unregisters from the user database. If the user giving this\n" " command is an owner user, the password is not necessary.\n" " " msgstr "" " []\n" "\n" " Elimina dal database degli utenti. Se l'utente che usa questo\n" " comando è un owner (proprietario), la password non è necessaria.\n" " " #: plugin.py:145 msgid "This command has been disabled. You'll have to ask the owner of this bot to unregister your user." msgstr "Questo comando è stato disabilitato. Contatta l'owner del bot per de-registrarti." #: plugin.py:158 #, docstring msgid "" " []\n" "\n" " Changes your current user database name to the new name given.\n" " is only necessary if the user isn't recognized by hostmask.\n" " This message must be sent to the bot privately (not on a channel) since\n" " it may contain a password.\n" " " msgstr "" " []\n" "\n" " Cambia l'attuale nome nel database degli utenti con .\n" " è necessaria solo se l'utente non è riconosciuto tramite\n" " l'hostmask. Questo messaggio va inviato al bot privatamente (non in\n" " canale) in quanto può contenere una password.\n" " " #: plugin.py:167 msgid "%q is already registered." msgstr "%q è già registrato." #: plugin.py:181 #, docstring msgid "" "[] \n" "\n" " Sets the new password for the user specified by to . Obviously this message must be sent to the bot\n" " privately (not in a channel). If the requesting user is an owner\n" " user (and the user whose password is being changed isn't that same\n" " owner user), then needn't be correct.\n" " " msgstr "" "[] \n" "\n" " Imposta una nuova password per l'utente specificato da . Il\n" " messaggio va ovviamente inviato privatamente (non in canale). Se\n" " l'utente è l'owner (e l'utente di cui si cambia la password non è lo\n" " stesso proprietario), non necessita di essere corretta.\n" " " #: plugin.py:209 #, docstring msgid "" " []\n" "\n" " Sets the secure flag on the user of the person sending the message.\n" " Requires that the person's hostmask be in the list of hostmasks for\n" " that user in addition to the password being correct. When the\n" " secure flag is set, the user *must* identify before they can be\n" " recognized. If a specific True/False value is not given, it\n" " inverts the current value.\n" " " msgstr "" " []\n" "\n" " Imposta il flag \"secure\" per l'utente che invia il messaggio.\n" " Richiede che la sua hostmask sia presente nell'elenco di quelle\n" " per quell'utente e di una password corretta. Quando questo flag è\n" " impostato, l'utente *deve* identificarsi prima di essere riconosciuto.\n" " Se l'argomento True/False non è specificato, il valore corrente viene invertito.\n" " " #: plugin.py:224 msgid "Secure flag set to %s" msgstr "Flag secure impostato a %s" #: plugin.py:232 #, docstring msgid "" "\n" "\n" " Returns the username of the user specified by or if\n" " the user is registered.\n" " " msgstr "" "\n" "\n" " Se l'utente è registrato, restituisce il nome utente di quello specificato da o .\n" " " #: plugin.py:241 msgid "I haven't seen %s." msgstr "Non ho mai visto %s." #: plugin.py:246 msgid "I don't know who that is." msgstr "Non so chi sia." #: plugin.py:252 #, docstring msgid "" "[]\n" "\n" " Returns the hostmask of . If isn't given, return the\n" " hostmask of the person giving the command.\n" " " msgstr "" "[]\n" "\n" " Restituisce l'hostmask di . Se non è specificato,\n" " riporta l'hostmask di chi ha dato il comando.\n" " " #: plugin.py:264 #, docstring msgid "" "[]\n" "\n" " Returns the hostmasks of the user specified by ; if \n" " isn't specified, returns the hostmasks of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" " Restituisce l'hostmask dell'utente specificato da ; se \n" " non è definito, riporta l'hostmask di chi ha dato il comando.\n" " " #: plugin.py:276 msgid "%s has no registered hostmasks." msgstr "%s non ha hostmask registrate." #: plugin.py:283 msgid "You may only retrieve your own hostmasks." msgstr "Puoi recuperare solo le tue hostmask." #: plugin.py:299 #, docstring msgid "" "[] [] []\n" "\n" " Adds the hostmask to the user specified by . The\n" " may only be required if the user is not recognized by\n" " hostmask. is also not required if an owner user is\n" " giving the command on behalf of some other user. If is\n" " not given, it defaults to your current hostmask. If is not\n" " given, it defaults to your currently identified name. This message\n" " must be sent to the bot privately (not on a channel) since it may\n" " contain a password.\n" " " msgstr "" "[] [] []\n" "\n" " Aggiunge all'utente specificato da . \n" " è richiesta solo se l'utente non viene riconosciuto tramite l'hostmask\n" " e può essere omessa se un owner sta dando il comando a nome di qualcun\n" " altro. Se non è fornita utilizza quella attualmente in uso.\n" " Se non è specificato utilizza quello attualmente identificato.\n" " Questo messaggio va inviato al bot privatamente (non in canale) in\n" " quanto può contenere una password.\n" " " #: plugin.py:313 msgid "hostmask" msgstr "hostmask" #: plugin.py:314 msgid "Make sure your hostmask includes a nick, then an exclamation point (!), then a user, then an at symbol (@), then a host. Feel free to use wildcards (* and ?, which work just like they do on the command line) in any of these parts." msgstr "Assicurati che la tua hostmask includa un nick, un punto esclamativo, un utente, un at (@) e un host. Puoi usare wildcard (* e ?, che funzionano come da riga di comando) in qualsiasi punto." #: plugin.py:324 plugin.py:347 msgid "That hostmask is already registered." msgstr "Questa hostmask è già registrata." #: plugin.py:355 #, docstring msgid "" " []\n" "\n" " Removes the hostmask from the record of the user\n" " specified by . If the hostmask given is 'all' then all\n" " hostmasks will be removed. The may only be required if\n" " the user is not recognized by their hostmask. This message must be\n" " sent to the bot privately (not on a channel) since it may contain a\n" " password.\n" " " msgstr "" " []\n" "\n" " Rimuove dalla lista dell'utente specificato da . Se\n" " l'hostmask fornita è \"all\" verranno rimosse tutte. è richiesta\n" " solo se l'utente non viene riconosciuto tramite l'hostmask. Questo messaggio\n" " va inviato al bot privatamente (non in canale) in quanto può contenere una password.\n" " " #: plugin.py:374 msgid "All hostmasks removed." msgstr "Tutte le hostmask sono state rimosse." #: plugin.py:378 msgid "There was no such hostmask." msgstr "Non c'è nessuna hostmask." #: plugin.py:387 #, docstring msgid "" "[]\n" "\n" " Returns the capabilities of the user specified by ; if \n" " isn't specified, returns the capabilities of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" " Restituisce le capacità dell'utente specificato da ; se \n" " non è fornito riporta le capacità dell'utente che ha usato il comando.\n" " " #: plugin.py:407 #, docstring msgid "" " \n" "\n" " Identifies the user as . This command (and all other\n" " commands that include a password) must be sent to the bot privately,\n" " not in a channel.\n" " " msgstr "" " \n" "\n" " Identifica l'utente come . Questo comando (e tutti quelli che\n" " includono una password) devono essere inviati al bot privatamente, mai in canale.\n" " " #: plugin.py:419 msgid "Your secure flag is true and your hostmask doesn't match any of your known hostmasks." msgstr "Il tuo flag secure è impostato a True e la tua hostmask non corrisponde a nessuna di quelle conosciute." #: plugin.py:429 #, docstring msgid "" "takes no arguments\n" "\n" " Un-identifies you. Note that this may not result in the desired\n" " effect of causing the bot not to recognize you anymore, since you may\n" " have added hostmasks to your user that can cause the bot to continue to\n" " recognize you.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Ti de-identifica. Nota che questo potrebbe non sortire l'effetto desiderato,\n" " il bot potrebbe continuare a riconoscerti in quanto sono state eventualmente\n" " aggiunta altre hostmask per il tuo utente.\n" " " #: plugin.py:438 msgid "If you remain recognized after giving this command, you're being recognized by hostmask, rather than by password. You must remove whatever hostmask is causing you to be recognized in order not to be recognized." msgstr "Se dopo aver utilizzato questo comando si è ancora riconosciuti, ciò avviene tramite l'hostmask piuttosto che la password. È necessario rimuovere qualsiasi hostmask che causa il riconoscimento." #: plugin.py:447 #, docstring msgid "" "takes no arguments\n" "\n" " Returns the name of the user calling the command.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Restituisce il nome dell'utente che usa questo comando.\n" " " #: plugin.py:455 msgid "I don't recognize you." msgstr "Non ti riconosco." #: plugin.py:460 #, docstring msgid "" "takes no arguments\n" "\n" " Returns some statistics on the user database.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta alcune statistiche a proposito del database degli utenti.\n" " " #: plugin.py:478 msgid "I have %s registered users with %s registered hostmasks; %n and %n." msgstr "Ho %s utenti registrati con %s hostmask registrate; %n e %n." limnoria-2018.01.25/plugins/User/locales/hu.po0000644000175000017500000002212413233426066020315 0ustar valval00000000000000# Limnoria User plugin # Copyright (C) 2011 Limnoria # nyuszika7h , 2011. # msgid "" msgstr "" "Project-Id-Version: Limnoria User\n" "POT-Creation-Date: 2011-12-23 13:11+CET\n" "PO-Revision-Date: 2014-07-05 00:10+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: \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" #: plugin.py:49 msgid "" "[--capability=] []\n" "\n" " Returns the valid registered usernames matching . If is\n" " not given, returns all registered usernames.\n" " " msgstr "" #: plugin.py:80 msgid "There are no matching registered users." msgstr "Nincsenek egyező regisztrált felhasználók." #: plugin.py:82 msgid "There are no registered users." msgstr "Nincsenek regisztrált felhasználók." #: plugin.py:88 msgid "" " \n" "\n" " Registers with the given password and the current\n" " hostmask of the person registering. You shouldn't register twice; if\n" " you're not recognized as a user but you've already registered, use the\n" " hostmask add command to add another hostmask to your already-registered\n" " user, or use the identify command to identify just for a session.\n" " This command (and all other commands that include a password) must be\n" " sent to the bot privately, not in a channel.\n" " " msgstr "" #: plugin.py:101 msgid "That name is already assigned to someone." msgstr "Az a név már hozzá van rendelve valakihez." #: plugin.py:106 msgid "username" msgstr "felhasználónév" #: plugin.py:107 msgid "Hostmasks are not valid usernames." msgstr "A hosztmaszkok nem érvényes felhasználónevek." #: plugin.py:114 msgid "Your hostmask is already registered to %s" msgstr "A hosztmaszkod már regisztrálva van %s-hoz." #: plugin.py:130 msgid "" " []\n" "\n" " Unregisters from the user database. If the user giving this\n" " command is an owner user, the password is not necessary.\n" " " msgstr "" #: plugin.py:145 msgid "This command has been disabled. You'll have to ask the owner of this bot to unregister your user." msgstr "Ez a parancs le lett tiltva. Meg kell kérned a bot tulajdonosát, hogy törölje a regisztrációdat." #: plugin.py:158 msgid "" " []\n" "\n" " Changes your current user database name to the new name given.\n" " is only necessary if the user isn't recognized by hostmask.\n" " This message must be sent to the bot privately (not on a channel) since\n" " it may contain a password.\n" " " msgstr "" #: plugin.py:167 msgid "%q is already registered." msgstr "%q már regisztrálva van." #: plugin.py:181 msgid "" "[] \n" "\n" " Sets the new password for the user specified by to . Obviously this message must be sent to the bot\n" " privately (not in a channel). If the requesting user is an owner\n" " user (and the user whose password is being changed isn't that same\n" " owner user), then needn't be correct.\n" " " msgstr "" #: plugin.py:209 msgid "" " []\n" "\n" " Sets the secure flag on the user of the person sending the message.\n" " Requires that the person's hostmask be in the list of hostmasks for\n" " that user in addition to the password being correct. When the\n" " secure flag is set, the user *must* identify before they can be\n" " recognized. If a specific True/False value is not given, it\n" " inverts the current value.\n" " " msgstr "" #: plugin.py:224 msgid "Secure flag set to %s" msgstr "A secure jelző be lett állítva %s-ra" #: plugin.py:232 msgid "" "\n" "\n" " Returns the username of the user specified by or if\n" " the user is registered.\n" " " msgstr "" #: plugin.py:241 msgid "I haven't seen %s." msgstr "Nem láttam %s-t." #: plugin.py:246 msgid "I don't know who that is." msgstr "Nem tudom, ki az." #: plugin.py:252 msgid "" "[]\n" "\n" " Returns the hostmask of . If isn't given, return the\n" " hostmask of the person giving the command.\n" " " msgstr "" #: plugin.py:264 msgid "" "[]\n" "\n" " Returns the hostmasks of the user specified by ; if \n" " isn't specified, returns the hostmasks of the user calling the\n" " command.\n" " " msgstr "" #: plugin.py:276 msgid "%s has no registered hostmasks." msgstr "%s-nak nincsenek regisztrált hosztmaszkjai." #: plugin.py:283 msgid "You may only retrieve your own hostmasks." msgstr "Csak a saját hosztmaszkjaidat szerezheted meg." #: plugin.py:299 msgid "" "[] [] []\n" "\n" " Adds the hostmask to the user specified by . The\n" " may only be required if the user is not recognized by\n" " hostmask. is also not required if an owner user is\n" " giving the command on behalf of some other user. If is\n" " not given, it defaults to your current hostmask. If is not\n" " given, it defaults to your currently identified name. This message\n" " must be sent to the bot privately (not on a channel) since it may\n" " contain a password.\n" " " msgstr "" #: plugin.py:313 msgid "hostmask" msgstr "hosztmaszk" #: plugin.py:314 msgid "Make sure your hostmask includes a nick, then an exclamation point (!), then a user, then an at symbol (@), then a host. Feel free to use wildcards (* and ?, which work just like they do on the command line) in any of these parts." msgstr "Győződj meg róla, hogy a hosztmaszkod tartalmaz egy nevet, aztán egy felkiáltójelet (!), aztán egy felhasználót, aztán egy kukac jelet (@), aztán egy hosztot. Nyugodtan használj helyettesítő karaktereket (* és ?, amelyek úgy működnek, mint a parancssorban) ezeknek a részeknek bármelyikében." #: plugin.py:324 #: plugin.py:347 msgid "That hostmask is already registered." msgstr "Ez a hosztmaszk már regisztrálva van." #: plugin.py:355 msgid "" " []\n" "\n" " Removes the hostmask from the record of the user\n" " specified by . If the hostmask given is 'all' then all\n" " hostmasks will be removed. The may only be required if\n" " the user is not recognized by their hostmask. This message must be\n" " sent to the bot privately (not on a channel) since it may contain a\n" " password.\n" " " msgstr "" #: plugin.py:374 msgid "All hostmasks removed." msgstr "Minden hosztmaszk eltávolítva." #: plugin.py:378 msgid "There was no such hostmask." msgstr "Nem volt ilyen hosztmaszk." #: plugin.py:387 msgid "" "[]\n" "\n" " Returns the capabilities of the user specified by ; if \n" " isn't specified, returns the capabilities of the user calling the\n" " command.\n" " " msgstr "" #: plugin.py:407 msgid "" " \n" "\n" " Identifies the user as . This command (and all other\n" " commands that include a password) must be sent to the bot privately,\n" " not in a channel.\n" " " msgstr "" #: plugin.py:419 msgid "Your secure flag is true and your hostmask doesn't match any of your known hostmasks." msgstr "A secure jelződ igaz és a hosztmaszkod nem egyezik egy ismert hosztmaszkoddal sem." #: plugin.py:429 msgid "" "takes no arguments\n" "\n" " Un-identifies you. Note that this may not result in the desired\n" " effect of causing the bot not to recognize you anymore, since you may\n" " have added hostmasks to your user that can cause the bot to continue to\n" " recognize you.\n" " " msgstr "" #: plugin.py:438 msgid "If you remain recognized after giving this command, you're being recognized by hostmask, rather than by password. You must remove whatever hostmask is causing you to be recognized in order not to be recognized." msgstr "Ha felismerve maradsz e parancs kiadása után, hosztmaszk alapján vagy felismerve, jelszó helyett. El kell távolítanod akármilyen hosztmaszkot, amely a fekusmerésedet okozza, hogy ne legyél felismerve." #: plugin.py:447 msgid "" "takes no arguments\n" "\n" " Returns the name of the user calling the command.\n" " " msgstr "" #: plugin.py:455 msgid "I don't recognize you." msgstr "Nem ismerlek fel." #: plugin.py:460 msgid "" "takes no arguments\n" "\n" " Returns some statistics on the user database.\n" " " msgstr "" #: plugin.py:478 msgid "I have %s registered users with %s registered hostmasks; %n and %n." msgstr "%s regisztrált felhasználóm van %s regisztrált hosztmaszkkal; %n és %n." limnoria-2018.01.25/plugins/User/locales/fr.po0000644000175000017500000004436013233426066020316 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2014-01-22 07:53+CET\n" "PO-Revision-Date: 2014-07-05 00:10+0200\n" "Last-Translator: \n" "Language-Team: Limnoria \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:52 msgid "" "[--capability=] []\n" "\n" " Returns the valid registered usernames matching . If " "is\n" " not given, returns all registered usernames.\n" " " msgstr "" "[--capacility=] []\n" "\n" "Retourne les utilisateurs enregistrés correspondant au . Si " "n'est pas donné, retourne tous les noms d'utilisateur." #: plugin.py:67 msgid "This is a private capability. Only admins can see who has it." msgstr "" "C'est une capacité privée. Seuls les admins peuvent voir qui la possède." #: plugin.py:92 msgid "There are no matching registered users." msgstr "Il n'y a pas d'utilisateur enregistré correspondant." #: plugin.py:94 msgid "There are no registered users." msgstr "Il n'y a pas d'utilisateur enregistré." #: plugin.py:100 msgid "" " \n" "\n" " Registers with the given password and the current\n" " hostmask of the person registering. You shouldn't register twice; " "if\n" " you're not recognized as a user but you've already registered, use " "the\n" " hostmask add command to add another hostmask to your already-" "registered\n" " user, or use the identify command to identify just for a session.\n" " This command (and all other commands that include a password) must " "be\n" " sent to the bot privately, not in a channel.\n" " " msgstr "" " \n" "\n" "Enregistre avec le mot de passe donné, et le masque d'hôte actuel de " "la personne s'inscrivant. Vous ne devez pas vous enregistrer deux fois ; si " "vous n'êtes pas reconnu(e) comme un utilisateur(trice) alors que vous êtes " "déjà enregistré(e), utilisez la commande 'hostmask add' pour ajouter un " "masque d'hôte à votre compte, ou utilisez la commande 'identify' pour vous " "identifier, juste le temps d'une session. Cette commande (et toutes les " "autres commandes qui nécessitent un mot de passe) doivent être envoyées en " "privé, et non sur un canal." #: plugin.py:113 msgid "That name is already assigned to someone." msgstr "Ce nom est déjà utilisé par quelqu'un." #: plugin.py:118 msgid "username" msgstr "nom d'utilisateur" #: plugin.py:119 msgid "Hostmasks are not valid usernames." msgstr "Les masques d'hôte ne sont pas des noms d'utilisateur valides." #: plugin.py:126 msgid "Your hostmask is already registered to %s" msgstr "Votre masque d'hôte est déjà enregistré à %s" #: plugin.py:142 msgid "" " []\n" "\n" " Unregisters from the user database. If the user giving this\n" " command is an owner user, the password is not necessary.\n" " " msgstr "" " []\n" "\n" "Supprime de la base de données des utilisateurs. Si l'utilisateur " "appellant cette commande est le propriétaire, le mot de passe n'est pas " "requis." #: plugin.py:157 msgid "" "This command has been disabled. You'll have to ask the owner of this bot to " "unregister your user." msgstr "" "Cette commande a été désactivée. Vous devez contacter le propriétaire du bot " "pour vous désenregistrer." #: plugin.py:170 msgid "" " []\n" "\n" " Changes your current user database name to the new name given.\n" " is only necessary if the user isn't recognized by " "hostmask.\n" " This message must be sent to the bot privately (not on a channel) " "since\n" " it may contain a password.\n" " " msgstr "" " []\n" "\n" "Change votre nom actuel dans la base de données des utilisateurs. n'est requis que si vous n'êtes pas actuellement reconnu(e) par " "masque d'hôte. Si vous avez le paramètre le message doit être " "envoyé en privé (et non sur un canal)." #: plugin.py:179 msgid "%q is already registered." msgstr "%q est déjà enregistré." #: plugin.py:193 msgid "" "[] \n" "\n" " Sets the new password for the user specified by to . Obviously this message must be sent to the bot\n" " privately (not in a channel). If the requesting user is an " "owner\n" " user (and the user whose password is being changed isn't that " "same\n" " owner user), then needn't be correct.\n" " " msgstr "" " \n" "\n" "Définit le nouveau mot de passe pour l'utilisateur spécifié par . " "Évidemment, ce message doit être envoyé au bot en privé (= pas sur un " "canal). Si l'utilisateur est le propriétaire (et si l'utilisateur dont on " "change le mot de passe n'est pas le même propriétaire), l' ne requiert pas d'être correct." #: plugin.py:221 msgid "" " []\n" "\n" " Sets the secure flag on the user of the person sending the " "message.\n" " Requires that the person's hostmask be in the list of hostmasks " "for\n" " that user in addition to the password being correct. When the\n" " secure flag is set, the user *must* identify before they " "can be\n" " recognized. If a specific True/False value is not given, it\n" " inverts the current value.\n" " " msgstr "" " []\n" "\n" "Défini le flag 'secure' sur l'utilisateur envoyant le message. Requiert que " "la personne soit reconnue par masque d'hôte, en plus du fait que le mot de " "passe doit être correct. Lorsque le flag 'secure' est défini, l'utilisateur " "*doit* être identifié avant d'être reconnu. Si l'argument on/off n'est pas " "donné, l'état courant est inversé." #: plugin.py:236 msgid "Secure flag set to %s" msgstr "Flag secure déjà défini à %s" #: plugin.py:244 msgid "" "\n" "\n" " Returns the username of the user specified by or " "if\n" " the user is registered.\n" " " msgstr "" "\n" "\n" "Retourne le nom d'utilisateur de la personne spécifiée par " "ou par , si cette personne est enregistrée." #: plugin.py:253 msgid "I haven't seen %s." msgstr "Je n'ai pas vu %s." #: plugin.py:258 msgid "I don't know who that is." msgstr "Je ne sais pas qui c'est." #: plugin.py:264 msgid "" "[]\n" "\n" " Returns the hostmask of . If isn't given, return " "the\n" " hostmask of the person giving the command.\n" " " msgstr "" "[]\n" "\n" "Retourne le masque d'hôte de . Si n'est pas donné, retourne le " "masque d'hôte de la personne envoyant la commande." #: plugin.py:276 msgid "" "[]\n" "\n" " Returns the hostmasks of the user specified by ; if " "\n" " isn't specified, returns the hostmasks of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" "Retourne les masques d'hôte de l'utilisateur spécifié par ; si " "n'est pas spécifié, retourne le masque d'hôte de la personne appelant la " "commande." #: plugin.py:288 msgid "%s has no registered hostmasks." msgstr "%s n'a pas de masque d'hôte enregistré." #: plugin.py:295 msgid "You may only retrieve your own hostmasks." msgstr "Vous ne pouvez récupérer que vos propres masques d'hôte." #: plugin.py:311 msgid "" "[] [] []\n" "\n" " Adds the hostmask to the user specified by . " "The\n" " may only be required if the user is not recognized " "by\n" " hostmask. is also not required if an owner user is\n" " giving the command on behalf of some other user. If " "is\n" " not given, it defaults to your current hostmask. If is " "not\n" " given, it defaults to your currently identified name. This " "message\n" " must be sent to the bot privately (not on a channel) since it " "may\n" " contain a password.\n" " " msgstr "" "[] [] []\n" "\n" "Ajoute le à l'utilisateur . Le n'est " "requis que si l'utilisateur n'est pas reconnu par masque d'hôte. peut également être omis si l'expéditeur est le propriétaire, et " "envoie la commande au nom de quelqu'un d'autre. Si le n'est " "pas donné, ce sera le masque d'hôte de l'expéditeur. Si n'est pas " "donné, il s'agit par défaut de votre nom d'utilisateur. Ce message doit être " "envoyé au bot en privé (pas sur un canal) si il contient un mot de passe." #: plugin.py:325 msgid "hostmask" msgstr "masque d'hôte" #: plugin.py:326 msgid "" "Make sure your hostmask includes a nick, then an exclamation point (!), then " "a user, then an at symbol (@), then a host. Feel free to use wildcards (* " "and ?, which work just like they do on the command line) in any of these " "parts." msgstr "" "Assurez-vous que votre masque d'hôte inclue un nick, un point d'exclamation, " "une ident, un arobase, puis un hôte. Sentez-vous libre d'utiliser des jokers " "(* et ?, qui fonctionnent comme dans la ligne de commande), dans n'importe " "lequel de ces termes." #: plugin.py:336 plugin.py:359 msgid "That hostmask is already registered." msgstr "Ce masque d'hôte est déjà enregistré." #: plugin.py:367 msgid "" "[] [] []\n" "\n" " Removes the hostmask from the record of the user\n" " specified by . If the hostmask given is 'all' then all\n" " hostmasks will be removed. The may only be required " "if\n" " the user is not recognized by their hostmask. This message must " "be\n" " sent to the bot privately (not on a channel) since it may " "contain a\n" " password. If is\n" " not given, it defaults to your current hostmask. If is " "not\n" " given, it defaults to your currently identified name. This " "message\n" " must be sent to the bot privately (not on a channel) since it " "may\n" " contain a password.\n" "\n" " " msgstr "" "[] [] []\n" "\n" "Supprime le de l'utilisateur . Si le masque d’hôte " "donné est « all », alors tous les masques d’hôte seront supprimés. Le n'est requis que si l'utilisateur n'est pas reconnu par masque " "d'hôte. peut également être omis si l'expéditeur est le " "propriétaire, et envoie la commande au nom de quelqu'un d'autre. Si le " " n'est pas donné, ce sera le masque d'hôte de l'expéditeur. " "Si n'est pas donné, il s'agit par défaut de votre nom d'utilisateur. " "Ce message doit être envoyé au bot en privé (pas sur un canal) si il " "contient un mot de passe." #: plugin.py:393 msgid "All hostmasks removed." msgstr "Tous les masques d'hôte ont été supprimés." #: plugin.py:397 msgid "There was no such hostmask." msgstr "Il n'y avait pas ce masque d'hôte." #: plugin.py:414 msgid "GPG features are not enabled." msgstr "Les fonctionnalités liées à GPG ne sont pas activées." #: plugin.py:423 msgid "" " \n" "\n" " Add a GPG key to your account." msgstr "" " \n" "\n" "Ajoute une clé GPG à votre compte." #: plugin.py:427 msgid "This key is already associated with your account." msgstr "Cette clé est déjà associée à votre compte." #: plugin.py:431 msgid "%n imported, %i unchanged, %i not imported." msgstr "%n importée(s), %i inchangée(s), %i non importée(s)" #: plugin.py:432 msgid "key" msgstr "clé" #: plugin.py:443 msgid "You must give a valid key id" msgstr "Vous devez donner un identifiant de clé valide" #: plugin.py:445 msgid "You must give a valid key server" msgstr "Vous devez donner un serveur de clé valide" #: plugin.py:449 msgid "" "\n" "\n" " Remove a GPG key from your account." msgstr "" "\n" "\n" "Supprime une clé GPG de votre compte." #: plugin.py:462 msgid "GPG key not associated with your account." msgstr "La clé GPG n’est pas associée à votre compte." #: plugin.py:467 msgid "" "takes no arguments\n" "\n" " Send you a token that you'll have to sign with your key." msgstr "" "ne prend pas d’argument\n" "\n" "Vous donne un jeton que vous devez signer avec votre clé." #: plugin.py:474 msgid "" "Your token is: %s. Please sign it with your GPG key, paste it somewhere, and " "call the 'auth' command with the URL to the (raw) file containing the " "signature." msgstr "" "Votre jeton est : %s. Veuillez le signer avec votre clé GPG, le paster " "quelque part, et appeler la commande « auth » avec l’URL du fichier (brut) " "contenant la signature." #: plugin.py:488 msgid "" "\n" "\n" " Check the GPG signature at the and authenticates you if\n" " the key used is associated to a user." msgstr "" "\n" "\n" "Vérifie que la signature GPG à l’ et vous authentifie si la clé est " "associée à un utilisateur." #: plugin.py:495 msgid "Signature or token not found." msgstr "Signature ou jeton non trouvé." #: plugin.py:499 msgid "Unknown token. It may have expired before you submit it." msgstr "" "Jeton inconnu. Il se peut qu’il ait expiré avant que vous ne l’ayez soumis." #: plugin.py:502 msgid "Your hostname/nick changed in the process. Authentication aborted." msgstr "" "Votre nom d’hôte/nick a changé durant le processus. Authentification annulée." #: plugin.py:513 msgid "You are now authenticated as %s." msgstr "Vous êtes maintenant identifié(e) en tant que %s." #: plugin.py:516 msgid "Unknown GPG key." msgstr "Clé GPG inconnue." #: plugin.py:518 msgid "" "Signature could not be verified. Make sure this is a valid GPG signature and " "the URL is valid." msgstr "" "La signature n’a pas pu être vérifiée. Assurez vous que c’est une signature " "GPG valide et que l’URL est valide." #: plugin.py:524 msgid "" "[]\n" "\n" " Returns the capabilities of the user specified by ; if \n" " isn't specified, returns the capabilities of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" "Retourne la liste des capacités de l'utilisateur spécifié par ; si " " n'est pas spécifié, retourne les capacités de l'utilisateur appelant " "la commande." #: plugin.py:544 msgid "" " \n" "\n" " Identifies the user as . This command (and all other\n" " commands that include a password) must be sent to the bot " "privately,\n" " not in a channel.\n" " " msgstr "" " \n" "\n" "Identifie l'utilisateur en tant que . Cette commande (et toutes cellent " "qui requierent un mot de passe) doivent être envoyées en privé au bot, et " "non sur un canal." #: plugin.py:556 msgid "" "Your secure flag is true and your hostmask doesn't match any of your known " "hostmasks." msgstr "" "Votre flag secure est True, et votre masque d'hôte ne correspond à aucune de " "vos masques d'hôte connus." #: plugin.py:566 msgid "" "takes no arguments\n" "\n" " Un-identifies you. Note that this may not result in the desired\n" " effect of causing the bot not to recognize you anymore, since you " "may\n" " have added hostmasks to your user that can cause the bot to continue " "to\n" " recognize you.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Vous dé-indentifie. Notez que cela ne marche pas forcément, par exemple si " "le bot vous reconnait grâce au masque d'hôte ; il continuera alors à vous " "reconnaître." #: plugin.py:575 msgid "" "If you remain recognized after giving this command, you're being recognized " "by hostmask, rather than by password. You must remove whatever hostmask is " "causing you to be recognized in order not to be recognized." msgstr "" "Si vous êtes encore reconnu(e) après avoir envoyé cette commande, c'est que " "vous êtes reconnu(e) par masque d'hôte et non par mot de passe. Vous devez " "supprimer tout masque d'hôte susceptible de vous reconnaitre, dans le but de " "ne pas être reconnu(e)." #: plugin.py:584 msgid "" "takes no arguments\n" "\n" " Returns the name of the user calling the command.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le nom de l'utilisateur appellant la commande." #: plugin.py:592 msgid "I don't recognize you." msgstr "Je ne vous reconnais pas." #: plugin.py:597 msgid "" "takes no arguments\n" "\n" " Returns some statistics on the user database.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne des statistiques sur la base de données." #: plugin.py:615 msgid "I have %s registered users with %s registered hostmasks; %n and %n." msgstr "" "J'ai %s utilisateurs enregistrés, avec %s masques d'hôte enregistrés ; %n et " "%n." #~ msgid "" #~ " []\n" #~ "\n" #~ " Removes the hostmask from the record of the user\n" #~ " specified by . If the hostmask given is 'all' then " #~ "all\n" #~ " hostmasks will be removed. The may only be " #~ "required if\n" #~ " the user is not recognized by their hostmask. This message " #~ "must be\n" #~ " sent to the bot privately (not on a channel) since it may " #~ "contain a\n" #~ " password.\n" #~ " " #~ msgstr "" #~ " []\n" #~ "\n" #~ "Retire le de la liste de ceux de l'utilisateur spécifié " #~ "par . Si le masque d'hôte est 'all', alors tous les masques d'hôtes " #~ "seront supprimés. Le n'est requis que si vous n'êtes pas " #~ "reconnu(e) par masque d'hôte. Ce message doit être envoyé en privé (=pas " #~ "sur un canal) car il peut contenir un mot de passe." limnoria-2018.01.25/plugins/User/locales/fi.po0000644000175000017500000005000613233426066020277 0ustar valval00000000000000# User plugin in Limnoria. # Copyright (C) 2011- 2014 Limnoria # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: User plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 11:59+EET\n" "PO-Revision-Date: 2014-12-20 12:00+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: suomi <>\n" "Language: 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" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: plugin.py:47 msgid "" "Provides commands for dealing with users, such as registration and\n" " authentication to the bot. This is a core Supybot plugin that should\n" " not be removed!" msgstr "" "Sisältää komennot käyttäjien hallinnointiin, kuten rekisteröimiseen ja " "botille tunnistautumiseen.\n" " Tämä on Supybotin ydin plugin, jota ei pitäisi poistaa!" #: plugin.py:56 msgid "" "[--capability=] []\n" "\n" " Returns the valid registered usernames matching . If " "is\n" " not given, returns all registered usernames.\n" " " msgstr "" "[--capability=] []\n" "\n" " Palauttaa kelvolliset rekisteröidyt käyttäjätunnukset, jotka " "täsmäävät . Jos ei\n" " ole annettu, palauttaa kaikki rekisteröidyt käyttäjätunnukset.\n" " " #: plugin.py:71 msgid "This is a private capability. Only admins can see who has it." msgstr "" "Tämä on yksityinen valtuus. Vain ylläpitäjät voivat nähdä keillä on se." #: plugin.py:96 msgid "There are no matching registered users." msgstr "Täsmääviä rekisteröityjä käyttäjiä ei ole." #: plugin.py:98 msgid "There are no registered users." msgstr "Rekisteröityneitä käyttäjiä ei ole." #: plugin.py:104 msgid "" " \n" "\n" " Registers with the given password and the current\n" " hostmask of the person registering. You shouldn't register twice; " "if\n" " you're not recognized as a user but you've already registered, use " "the\n" " hostmask add command to add another hostmask to your already-" "registered\n" " user, or use the identify command to identify just for a session.\n" " This command (and all other commands that include a password) must " "be\n" " sent to the bot privately, not in a channel.\n" " " msgstr "" " \n" "\n" " Rekisteröi annetulla ja rekisteröityvän " "henkilön nykyisellä hostmaskilla.\n" " Sinun ei pitäisi rekisteröityä kahdesti; jos\n" " sinua ei tunnisteta käyttäjäksi, käytä komentoa\n" " 'hostmask add' lisätäksesi hostmaskin valmiiksi rekisteröidylle\n" " käyttäjällesi, tai käytä komentoa 'identify' tunnistautuaksesi vain " "istunnon ajaksi.\n" " Tämä komento (ja kaikki muut komennot, jotka sisältävät salasanan) " "täytyy lähettää\n" " botille yksityisesti, ei kanavalla.\n" " " #: plugin.py:117 msgid "That name is already assigned to someone." msgstr "Tuo nimi on jo liitetty johonkuhun." #: plugin.py:122 msgid "username" msgstr "käyttäjänimi" #: plugin.py:123 msgid "Hostmasks are not valid usernames." msgstr "Hostmaskit eivät ole kelvollisia käyttäjänimiä" #: plugin.py:130 msgid "Your hostmask is already registered to %s" msgstr "Sinun hostmaskisi on jo rekisteröity käyttäjälle %s." #: plugin.py:146 msgid "" " []\n" "\n" " Unregisters from the user database. If the user giving this\n" " command is an owner user, the password is not necessary.\n" " " msgstr "" " []\n" "\n" " Poistaa käyttäjätietokannasta. Mikäli käyttäjä, joka antaa " "tämän komennon omaa 'owner' valtuuden,\n" " salasana ei ole vaadittu.\n" " " #: plugin.py:161 msgid "" "This command has been disabled. You'll have to ask the owner of this bot to " "unregister your user." msgstr "" "Tämä komento on poistettu käytöstä. Sinun täytyy pyytää tämän botin " "omistajaa poistaaksesi käyttäjätunnuksesi." #: plugin.py:174 msgid "" " []\n" "\n" " Changes your current user database name to the new name given.\n" " is only necessary if the user isn't recognized by " "hostmask.\n" " This message must be sent to the bot privately (not on a channel) " "since\n" " it may contain a password.\n" " " msgstr "" " []\n" "\n" " Vaihtaa nykyisen nimesi käyttäjätietokannassa annetuksi .\n" " on vaadittu vain, jos käyttäjä ei ole tunnistettu " "hostmaskilla.\n" " Tämä viesti täytyy lähettää botille yksityisesti (ei kanavalla), " "koska \n" " se saattaa sisältää salasanan.\n" " " #: plugin.py:183 msgid "%q is already registered." msgstr "%q on jo rekisteröitynyt." #: plugin.py:197 msgid "" "[] \n" "\n" " Sets the new password for the user specified by to . Obviously this message must be sent to the bot\n" " privately (not in a channel). If the requesting user is an " "owner\n" " user (and the user whose password is being changed isn't that " "same\n" " owner user), then needn't be correct.\n" " " msgstr "" "[] \n" "\n" " Asettaa määrittämän käyttäjätunnuksen salasanan " ". Ilmiselvästi tämä viesti täytyy lähettää botille " "yksityisesti\n" " (ei kanavalla). Jos pyytävä käyttäjä omaa valtuuden 'owner'\n" " (ja käyttäjä, jonka salasanaa vaihdetaan ei ole sama \n" " 'owner' valtuuden omaava käyttäjä), silloin " "ei tarvitse olla oikein.\n" " " #: plugin.py:225 msgid "" " []\n" "\n" " Sets the secure flag on the user of the person sending the " "message.\n" " Requires that the person's hostmask be in the list of hostmasks " "for\n" " that user in addition to the password being correct. When the\n" " secure flag is set, the user *must* identify before they can be\n" " recognized. If a specific True/False value is not given, it\n" " inverts the current value.\n" " " msgstr "" " []\n" "\n" " Asettaa 'secure' lipun käyttäjään, joka lähettää viestin.\n" " Vaatii, että henkilön hostmask on hostmaskien listassa oikean " "salasanan.\n" " lisäksi.\n" " Kun 'secure' lippu on asetettu, käyttäjän *täytyy* tunnistautua, " "ennen kuin hänet voidaan tunnistaa.\n" " Jos True/False arvoa ei ole annettu, \n" " nykyinen arvo käännetään.\n" " " #: plugin.py:240 msgid "Secure flag set to %s" msgstr "'Secure' lippu on asetettu arvoon %s" #: plugin.py:248 msgid "" "\n" "\n" " Returns the username of the user specified by or " "if\n" " the user is registered.\n" " " msgstr "" "\n" "\n" " Palauttaa käyttäjätunnuksen, jonka määrittää tai " ", mikäli\n" " käyttäjä on rekisteröitynyt.\n" " " #: plugin.py:257 msgid "I haven't seen %s." msgstr "En ole nähnyt käyttäjää %s." #: plugin.py:262 msgid "I don't know who that is." msgstr "En tiedä kuka tuo on." #: plugin.py:268 msgid "" "[]\n" "\n" " Returns the hostmask of . If isn't given, return " "the\n" " hostmask of the person giving the command.\n" " " msgstr "" "[]\n" "\n" " Palauttaa hostmaskin. Jos ei ole " "annettu, palauttaa\n" " komennon antavan nimimerkin hostmaskin\n" " " #: plugin.py:280 msgid "" "[]\n" "\n" " Returns the hostmasks of the user specified by ; if " "\n" " isn't specified, returns the hostmasks of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" " Palauttaa käyttäjän, jonka määrittää , hostmaskit; jos " "\n" " ei ole määritetty, palauttaa käyttäjän, joka antaa komennon\n" " hostmaskit.\n" " " #: plugin.py:292 msgid "%s has no registered hostmasks." msgstr "%s ei ole rekisteröinyt hostmaskeja." #: plugin.py:299 msgid "You may only retrieve your own hostmasks." msgstr "Voit saada vain omat hostmaskisi." #: plugin.py:315 msgid "" "[] [] []\n" "\n" " Adds the hostmask to the user specified by . " "The\n" " may only be required if the user is not recognized " "by\n" " hostmask. is also not required if an owner user is\n" " giving the command on behalf of some other user. If " "is\n" " not given, it defaults to your current hostmask. If is " "not\n" " given, it defaults to your currently identified name. This " "message\n" " must be sent to the bot privately (not on a channel) since it " "may\n" " contain a password.\n" " " msgstr "" "[] [] []\n" "\n" " Lisää , käyttäjälle jonka määrittää . \n" " on vaadittu vain, jos käyttäjää ei tunnisteta\n" " hostmaskilla. ei myöskään ole vaadittu, mikäli " "omistaja\n" " käyttäjä tekee komennon toiselle käyttäjälle. Jos " "ei\n" " ole annettu, se on oletuksena nykyinen hostmaskisi. " "Jos ei ole\n" " annettu, se on oletuksena nykyinen tunnistettu käyttäjänimesi. " "Tämä komento\n" " täytyy lähettää botille yksityisesti (ei kanavalla), koska se " "saattaa\n" " sisältää salasanan.\n" " " #: plugin.py:329 msgid "hostmask" msgstr "hostmask" #: plugin.py:330 msgid "" "Make sure your hostmask includes a nick, then an exclamation point (!), then " "a user, then an at symbol (@), then a host. Feel free to use wildcards (* " "and ?, which work just like they do on the command line) in any of these " "parts." msgstr "" "Varmista, että hostmaskisi sisältää nimimerkin, sitten eroitus kohdan, p(!), " "sitten käyttäjän, sitten ät symboolin (@), sitten isännän. Käytä " "jokerimerkkejä vapaasti (* ja ?, jotka toimivat samalla tavalla, kuin " "komentorivillä) missä tahansa näistä osista." #: plugin.py:340 plugin.py:361 msgid "That hostmask is already registered." msgstr "Tuo hostmaski on jo rekisteröity." #: plugin.py:371 #, fuzzy msgid "" "[] [] []\n" "\n" " Removes the hostmask from the record of the user\n" " specified by . If the hostmask given is 'all' then all\n" " hostmasks will be removed. The may only be required " "if\n" " the user is not recognized by their hostmask. This message must " "be\n" " sent to the bot privately (not on a channel) since it may " "contain a\n" " password. If is\n" " not given, it defaults to your current hostmask. If is " "not\n" " given, it defaults to your currently identified name.\n" " " msgstr "" "[] [] []\n" "\n" " Poistaa määrittämän käyttäjän tiedoista.\n" " Joa annettu hostmask on 'all', kaikki hostmaskit poistetaan.\n" " voidaan vaatia, mikäli käyttäjää ei ole tunnistettu hostmaskin " "perusteella.\n" " Viesti täytyy lähettää botille yksityisesti (ei kanavalla), koska se voi " "sisältää salasanan.\n" " Jos hostmaskia ei ole annettu, se on oletuksena nykyinen tunnistetu " "hostmask.\n" " Jos nimeä ei ole annettu, se on oletuksena nykyinen tunnistettu nimi.\n" " " #: plugin.py:394 msgid "All hostmasks removed." msgstr "Kaikki hostmaskit poistettu." #: plugin.py:398 msgid "There was no such hostmask." msgstr "Tuollaista hostmaskia ei ollut." #: plugin.py:411 msgid "GPG features are not enabled." msgstr "GPG toiminnot eivät ole käytössä." #: plugin.py:425 msgid "" " \n" "\n" " Add a GPG key to your account." msgstr "" " \n" "\n" " Lisää GPG-avaimen tunnuksellesi." #: plugin.py:429 msgid "This key is already associated with your account." msgstr "Tämä avain on jo liitetty tunnukseesi." #: plugin.py:433 msgid "%n imported, %i unchanged, %i not imported." msgstr "%n tuotu, %i muuttumaton, %i ei tuotu." #: plugin.py:434 msgid "key" msgstr "avain" #: plugin.py:445 msgid "You must give a valid key id" msgstr "Kelvollinen avain-id vaaditaan" #: plugin.py:447 msgid "You must give a valid key server" msgstr "Kelvollinen avainpalvelin vaaditaan" #: plugin.py:451 msgid "" "\n" "\n" " Remove a GPG key from your account." msgstr "" "\n" "\n" " Poistaa GPG-avaimen tunnukseltasi." #: plugin.py:467 msgid "GPG key not associated with your account." msgstr "GPG-avainta ei ole liitetty tunnukseesi." #: plugin.py:472 msgid "" "takes no arguments\n" "\n" " List your GPG keys." msgstr "" "ei ota parametrejä\n" "\n" " Antaa luettelon GPG-avaimistasi." #: plugin.py:477 msgid "No key is associated with your account." msgstr "Yhtään avainta ei ole liitetty tunnukseesi." #: plugin.py:484 msgid "" "takes no arguments\n" "\n" " Send you a token that you'll have to sign with your key." msgstr "" "ei ota parametrejä\n" "\n" " Lähettää merkin, jonka allekirjoitat avaimellasi." #: plugin.py:491 msgid "" "Your token is: %s. Please sign it with your GPG key, paste it somewhere, and " "call the 'auth' command with the URL to the (raw) file containing the " "signature." msgstr "" "Avaimesi on: %s. Allekirjoita se GPG-avaimellasi, liitä jonnekin ja käytä " "'auth'-komentoa URL-osoitteella (raakaan) tiedostoon, joka sisältää " "allekirjoituksen." #: plugin.py:505 msgid "" "\n" "\n" " Check the GPG signature at the and authenticates you if\n" " the key used is associated to a user." msgstr "" "\n" "\n" " Tarkistaa -osoitteessa olevan GPG-allekirjoituksen ja tunnistaa sinut, " "jos\n" " avain on liitetty käyttäjään." #: plugin.py:515 msgid "Signature or token not found." msgstr "Allekirjoitusta tai merkkiä ei löydy." #: plugin.py:519 msgid "Unknown token. It may have expired before you submit it." msgstr "Tuntematon merkki. Se on voinut vanhentua ennen, kuin lähetit sen." #: plugin.py:522 msgid "Your hostname/nick changed in the process. Authentication aborted." msgstr "" "Isäntänimesi/nimimerkkisi vaihtui prosessissa. Tunnistautuminen keskeytetty." #: plugin.py:534 plugin.py:581 msgid "" "Your secure flag is true and your hostmask doesn't match any of your known " "hostmasks." msgstr "" "Sinun 'secure' lippusi on 'true' ja sinun hostmaskisi ei täsmää yhteenkään " "sinun tunnettuun hostmaskiisi." #: plugin.py:538 msgid "You are now authenticated as %s." msgstr "Olet nyt tunnistautunut käyttäjäksi %s." #: plugin.py:541 msgid "Unknown GPG key." msgstr "Tuntematon GPG-avain." #: plugin.py:543 msgid "" "Signature could not be verified. Make sure this is a valid GPG signature and " "the URL is valid." msgstr "" "Allekirjoitusta ei voitu varmistaa. Varmista, että tämä on kelvollinen GPG-" "allekirjoitus ja URL-osoite on kelvollinen." #: plugin.py:549 msgid "" "[]\n" "\n" " Returns the capabilities of the user specified by ; if \n" " isn't specified, returns the capabilities of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" " Palauttaa käyttäjän, jonka määrittää valtuudet; jos \n" " ei ole määritetty, palauttaa käyttäjän, joka kutsuu komennonn " "valtuudet.\n" " " #: plugin.py:569 msgid "" " \n" "\n" " Identifies the user as . This command (and all other\n" " commands that include a password) must be sent to the bot " "privately,\n" " not in a channel.\n" " " msgstr "" " \n" "\n" " Tunnistaa käyttäjän . Tämä komento (ja kaikki muut " "komennot, \n" " jotka sisältävät salasanan) täytyy lähettää botille yksityisesti, \n" " ei kanavalla.\n" " " #: plugin.py:591 msgid "" "takes no arguments\n" "\n" " Un-identifies you. Note that this may not result in the desired\n" " effect of causing the bot not to recognize you anymore, since you " "may\n" " have added hostmasks to your user that can cause the bot to continue " "to\n" " recognize you.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Kirjaa sinut ulos. Huomaa, ettei tällä välltämättä ole haluttua\n" " vaikutusta, että botti lopettaa sinun tuntemisen, koska olet " "saattanut\n" " lisätä hostmaskin käyttäjälle. Tämä voi aihettaa sen, että botti\n" " tunnistaa sinut yhä.\n" " " #: plugin.py:600 msgid "" "If you remain recognized after giving this command, you're being recognized " "by hostmask, rather than by password. You must remove whatever hostmask is " "causing you to be recognized in order not to be recognized." msgstr "" "Jos pysyt tunnistettuna tämän komennon antamisen jälkeen, sinut tunnistetaan " "hostmaskin, eikä salasanan perusteella. Sinun täytyy poistaa mikä tahansa " "hostmaski, joka aiheuttaa sinun tunnistamisesi, tullaksesi " "tunnistamattomaksi." #: plugin.py:609 msgid "" "takes no arguments\n" "\n" " Returns the name of the user calling the command.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa komennon antaneen käyttäjän tunnuksen.\n" " " #: plugin.py:617 #, fuzzy msgid "" "I don't recognize you. You can message me either of these two commands: " "\"user identify \" to log in or \"user register " " \" to register." msgstr "" "Et ole tunnistautunut. Voit lähettää minulle kumman tahansa näistä kahdesta " "komennoista: \"user identify \" kirjautuaksesi sisään " "tai rekisteröityäksesi \"user register \"." #: plugin.py:622 msgid "" "takes no arguments\n" "\n" " Returns some statistics on the user database.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa joitakin tilastotietoja käyttäjä tietokannasta.\n" " " #: plugin.py:640 msgid "I have %s registered users with %s registered hostmasks; %n and %n." msgstr "" "Minulla on %s rekisteröityä käyttäjää %s rekisteröidyllä hostmaskilla; %n ja " "%n." #~ msgid "I don't recognize you." #~ msgstr "Minä en tunnista sinua." #~ msgid "" #~ " []\n" #~ "\n" #~ " Removes the hostmask from the record of the user\n" #~ " specified by . If the hostmask given is 'all' then " #~ "all\n" #~ " hostmasks will be removed. The may only be " #~ "required if\n" #~ " the user is not recognized by their hostmask. This message " #~ "must be\n" #~ " sent to the bot privately (not on a channel) since it may " #~ "contain a\n" #~ " password.\n" #~ " " #~ msgstr "" #~ " []\n" #~ "\n" #~ " Poistaa käyttäjältä, jonka määrittää\n" #~ " . Jos annettu hostmask on 'all' (kaikki) niin, kaikki\n" #~ " hostmaskit poistetaan. on vaadittu\n" #~ " vain, jos käyttäjää ei tunnisteta hostmaskin perusteella. " #~ "Tämä viesti täytyy\n" #~ " lähettää botille yksisyisesti (ei kanavalla), koska se voi " #~ "sisältää\n" #~ " salasanan.\n" #~ " " limnoria-2018.01.25/plugins/User/locales/de.po0000644000175000017500000003260513233426066020276 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2011-12-23 13:11+CET\n" "PO-Revision-Date: 2012-04-27 15:48+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: German \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" #: plugin.py:49 msgid "" "[--capability=] []\n" "\n" " Returns the valid registered usernames matching . If is\n" " not given, returns all registered usernames.\n" " " msgstr "" "[--capability=] []\n" "\n" "Gibt die zulässigen registrierten Nutzernamen aus, die auf zutreffen. Fall nicht angegeben wird, werden alle registrierten Benutzernamen ausgegeben." #: plugin.py:80 msgid "There are no matching registered users." msgstr "Kein passender registrierter Benutzer gefunden." #: plugin.py:82 msgid "There are no registered users." msgstr "Es gibt keine registrierten Benutzer." #: plugin.py:88 msgid "" " \n" "\n" " Registers with the given password and the current\n" " hostmask of the person registering. You shouldn't register twice; if\n" " you're not recognized as a user but you've already registered, use the\n" " hostmask add command to add another hostmask to your already-registered\n" " user, or use the identify command to identify just for a session.\n" " This command (and all other commands that include a password) must be\n" " sent to the bot privately, not in a channel.\n" " " msgstr "" " \n" "\n" "Registiert mit dem angegeben Passwort und der momentanen Hostmaske der registierenden Person. Du solltest dich nicht doppelt registrieren; falls der Bot dich nicht als Benutzer anerkennt, obwohl du bereits registriert bist, benutze den Befehl \"hostmask add\" um eine andere Hostmaske zu deinem existierendem Benutzer hinzuzufügen, oder benutze den Befehl \"identify\" um dich nur für diese Sitzung zu identifizieren. Dieser Befehl (und alle anderen Befehle, die ein Passwort beinhalten) müssen dem Bot privat gesendet werden, nicht in einem Kanal." #: plugin.py:101 msgid "That name is already assigned to someone." msgstr "Dieser Name ist schon an jemanden anders vergeben." #: plugin.py:106 msgid "username" msgstr "Benutzername" #: plugin.py:107 msgid "Hostmasks are not valid usernames." msgstr "Hostmasken sind keine gültigen Benutzernamen." #: plugin.py:114 msgid "Your hostmask is already registered to %s" msgstr "Deine Hostmaske ist schon registiert für %s." #: plugin.py:130 msgid "" " []\n" "\n" " Unregisters from the user database. If the user giving this\n" " command is an owner user, the password is not necessary.\n" " " msgstr "" " []\n" "\n" "Entfernt die Registierung von aus der Benutzerdatenbank. Falls der Benutzer, der diesen Befehl aufruft ein Besitzer ist, wird kein Passwort benötigt." #: plugin.py:145 msgid "This command has been disabled. You'll have to ask the owner of this bot to unregister your user." msgstr "Dieser Befehl wurde abgeschaltet. Du musst den Besiter des Bots fragen um dich vom Bot zu entfernen." #: plugin.py:158 msgid "" " []\n" "\n" " Changes your current user database name to the new name given.\n" " is only necessary if the user isn't recognized by hostmask.\n" " This message must be sent to the bot privately (not on a channel) since\n" " it may contain a password.\n" " " msgstr "" " []\n" "\n" "Ändert deinen momentanen Namen in der Benutzerdatenbank in den neuen Namen. ist nur nötig, falls der Benutzer anhand der Hostmaske erkannt wird. Diese Nachricht muss dem Bot privat übermittelt werden (nicht in einem Kanal), da sie möglicherweise ein Passwort enthält." #: plugin.py:167 msgid "%q is already registered." msgstr "%q ist schon registriert." #: plugin.py:181 msgid "" "[] \n" "\n" " Sets the new password for the user specified by to . Obviously this message must be sent to the bot\n" " privately (not in a channel). If the requesting user is an owner\n" " user (and the user whose password is being changed isn't that same\n" " owner user), then needn't be correct.\n" " " msgstr "" "[] \n" "\n" "Setzt das Passwort für den Benutzer auf . Es ist ja wohl klar, dass das dem Bot privat gesendet werden muss (nicht in einem Kanal). Fall der anfordernde Benutzer ein Besitzer ist (und der Benutzer, dessen Passwort geändert wird nicht der gleiche Besitzer ist), muss nicht korrekt sein." #: plugin.py:209 #, fuzzy msgid "" " []\n" "\n" " Sets the secure flag on the user of the person sending the message.\n" " Requires that the person's hostmask be in the list of hostmasks for\n" " that user in addition to the password being correct. When the\n" " secure flag is set, the user *must* identify before they can be\n" " recognized. If a specific True/False value is not given, it\n" " inverts the current value.\n" " " msgstr "" " []\n" "\n" "Setzt die Sicherheitsflagge für die Person, die diese Nachricht sendet. Setzt vorraus das die Hostmaske der person in der Liste von Hostmask für diesen Benutzer ist und außerdem muss das Passwort richtig sein. Wenn die Sicherheitsflagge gesetzt ist, *muss* der Benutzer sich identifizieren bevor er erkannt wird. Falls kein True/False Wert angegeben wird, wird der momentane Wert invertiert." #: plugin.py:224 msgid "Secure flag set to %s" msgstr "Sicherheits Flagge ist gesetzt auf %s" #: plugin.py:232 msgid "" "\n" "\n" " Returns the username of the user specified by or if\n" " the user is registered.\n" " " msgstr "" "\n" "\n" "Gibt den Benutzernamen des Benutzers aus, der durch oder angegeben wird. Falls der Benutzer registrier ist." #: plugin.py:241 msgid "I haven't seen %s." msgstr "Ich habe %s nicht gesehen." #: plugin.py:246 msgid "I don't know who that is." msgstr "Ich weiß nicht wer das ist." #: plugin.py:252 msgid "" "[]\n" "\n" " Returns the hostmask of . If isn't given, return the\n" " hostmask of the person giving the command.\n" " " msgstr "" "[]\n" "\n" "Gibt die Hostmaske von aus. Falls nicht angegeben wird, wird die Hostmaske der Person ausgegeben, die den Befehl gegeben hat." #: plugin.py:264 msgid "" "[]\n" "\n" " Returns the hostmasks of the user specified by ; if \n" " isn't specified, returns the hostmasks of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" "Gibt die Hostmaske des Benutzers aus; falls nicht angegeben ist, wird die Hostmaskes des Benutzer ausgegeben, der den Befehl aufgerufen hat." #: plugin.py:276 msgid "%s has no registered hostmasks." msgstr "%s hat keine Hostmasken gesetzt." #: plugin.py:283 msgid "You may only retrieve your own hostmasks." msgstr "Du darfst nur deine eigene Hostmaske abfragen" #: plugin.py:299 msgid "" "[] [] []\n" "\n" " Adds the hostmask to the user specified by . The\n" " may only be required if the user is not recognized by\n" " hostmask. is also not required if an owner user is\n" " giving the command on behalf of some other user. If is\n" " not given, it defaults to your current hostmask. If is not\n" " given, it defaults to your currently identified name. This message\n" " must be sent to the bot privately (not on a channel) since it may\n" " contain a password.\n" " " msgstr "" "[] [] []\n" "\n" "Fühgt die Hostmaske dem Benutzer hinzu. Das wird nur benötigt, falls der Benutzer nicht anhand seiner Hostmaske erkannt wird. wird auch nicht benötigt wenn ein Besitzer diesen Befehl für einen anderen Benutzer ausführt. Falls nicht angegben wird, wird deine momentane Hostmaske benutzt. Falls nicht angegeben ist, wird der Name benutzt über den du momentan identifiziert bist. Diese Nachricht muss dem Bot privat gesendet werden (nicht in einem Kanal), da sie ein Passwort enhalten könnte." #: plugin.py:313 msgid "hostmask" msgstr "Hostmaske" #: plugin.py:314 msgid "Make sure your hostmask includes a nick, then an exclamation point (!), then a user, then an at symbol (@), then a host. Feel free to use wildcards (* and ?, which work just like they do on the command line) in any of these parts." msgstr "Stelle sicher das deine Hostmaske einen Nicknamen, dann ein Ausrufezeichen (!), dann einen User, dann ein at Symbol (@) und dann den Host beinhaltet. Du kannst Wildcards verwenden ( * und ?, welche funktionrien wie in der Befehlszeile), in jedem dieser Abschnitte" #: plugin.py:324 #: plugin.py:347 msgid "That hostmask is already registered." msgstr "Diese Hostmaske ist schon registriert." #: plugin.py:355 msgid "" " []\n" "\n" " Removes the hostmask from the record of the user\n" " specified by . If the hostmask given is 'all' then all\n" " hostmasks will be removed. The may only be required if\n" " the user is not recognized by their hostmask. This message must be\n" " sent to the bot privately (not on a channel) since it may contain a\n" " password.\n" " " msgstr "" " []\n" "\n" "Entfernt die Hostmaske vom Benutzereintrag von . Falls die angegebene Hostmaske 'all' ist, werden alle Hostmasken entfernt. Das wird nur benötigt, falls der Benutzer nicht über seine Hostmaske erkannt wird. Diese Nachricht muss dem Bot privat gesendet werden (nicht in einem Kanal), da sie ein Passwort enhalten könnte." #: plugin.py:374 msgid "All hostmasks removed." msgstr "Alle Hostmasken entfernt." #: plugin.py:378 msgid "There was no such hostmask." msgstr "Es gibt keine solche Hostmaske." #: plugin.py:387 msgid "" "[]\n" "\n" " Returns the capabilities of the user specified by ; if \n" " isn't specified, returns the capabilities of the user calling the\n" " command.\n" " " msgstr "" "[]\n" "\n" "Gibt die Fähigkeiten des Benutzers aus; falls nicht angegeben wird, werden die Fähigkeiten des Benutzer ausgegeben, der diesen Befehl ausführt." #: plugin.py:407 msgid "" " \n" "\n" " Identifies the user as . This command (and all other\n" " commands that include a password) must be sent to the bot privately,\n" " not in a channel.\n" " " msgstr "" " \n" "\n" "identifiziert den Benutzer als . Dieser Befehl (und alle anderen Befehle die ein Passwort beinhalten) muss an den Bot privat gesendet werden, nicht in einem Kanal." #: plugin.py:419 msgid "Your secure flag is true and your hostmask doesn't match any of your known hostmasks." msgstr "Dein Sicherheitsflag ist auf wahr gesetzt und deine Hostmaske passt zu keiner Hostmaske die zu deinen passt." #: plugin.py:429 msgid "" "takes no arguments\n" "\n" " Un-identifies you. Note that this may not result in the desired\n" " effect of causing the bot not to recognize you anymore, since you may\n" " have added hostmasks to your user that can cause the bot to continue to\n" " recognize you.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Hebt deine Identifizierung auf. Beachte das dies eventuell nicht den geschwünschten Effekt erzielt, dass dich der Bot nichtmehr erkennt. Da du du möglicherweise Hostmasken zu deinem Bot hinzugefügt hast, die dazu führen das der Bot dich weitehrin erkennt." #: plugin.py:438 msgid "If you remain recognized after giving this command, you're being recognized by hostmask, rather than by password. You must remove whatever hostmask is causing you to be recognized in order not to be recognized." msgstr "Falls du weiterhin beachtet wirst nach diesem Befehl, wirst du durch deine Hostmaske beachtet, anstatt deines Passworts. Du musst die Hostmaske entfernen die dazu führt du das beachtet wirst um nicht mehr beachtet zu werden." #: plugin.py:447 msgid "" "takes no arguments\n" "\n" " Returns the name of the user calling the command.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt den Namen den Benutzers aus, der den Befehl aufruft." #: plugin.py:455 msgid "I don't recognize you." msgstr "Ich erkenne dich nicht" #: plugin.py:460 msgid "" "takes no arguments\n" "\n" " Returns some statistics on the user database.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt ein paar Statistiken über die Benutzerdatenbank zurück." #: plugin.py:478 msgid "I have %s registered users with %s registered hostmasks; %n and %n." msgstr "Ich habe %s registierte Benutzer mit %s registrierten Hostmasken; %n und %n." limnoria-2018.01.25/plugins/Unix/0000755000175000017500000000000013233426077015725 5ustar valval00000000000000limnoria-2018.01.25/plugins/Unix/test.py0000644000175000017500000001701413233426066017257 0ustar valval00000000000000### # 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. ### import os import socket from supybot.test import * try: from unittest import skip, skipIf except ImportError: def skipUnlessSpell(f): return None def skipUnlessFortune(f): return None def skipUnlessPing(f): return None def skipUnlessPing6(f): return None else: skipUnlessSpell = skipIf(utils.findBinaryInPath('aspell') is None and \ utils.findBinaryInPath('ispell') is None, 'aspell/ispell not available.') skipUnlessFortune = skipIf(utils.findBinaryInPath('fortune') is None, 'fortune not available.') if network: skipUnlessPing = skipIf( utils.findBinaryInPath('ping') is None or not setuid, 'ping not available.') if socket.has_ipv6: skipUnlessPing6 = skipIf( utils.findBinaryInPath('ping6') is None or not setuid, 'ping6 not available.') else: skipUnlessPing6 = skip( 'IPv6 not available.') else: skipUnlessPing = skip( 'network not available.') skipUnlessPing6 = skip( 'network not available.') class UnixConfigTestCase(ChannelPluginTestCase): plugins = ('Unix',) def testFortuneFiles(self): self.assertNotError('config channel plugins.Unix.fortune.files ' 'foo bar') self.assertRegexp('config channel plugins.Unix.fortune.files ' '"-foo bar"', 'Error:.*dash.*not u?\'-foo\'') # The u is for Python 2 self.assertNotError('config channel plugins.Unix.fortune.files ""') if os.name == 'posix': class UnixTestCase(PluginTestCase): plugins = ('Unix',) @skipUnlessSpell def testSpell(self): self.assertRegexp('spell Strike', '(correctly|Possible spellings)') # ispell won't find any results. aspell will make some # suggestions. self.assertRegexp('spell z0opadfnaf83nflafl230kasdf023hflasdf', 'not find|Possible spellings') self.assertNotError('spell Strizzike') self.assertError('spell foo bar baz') self.assertError('spell -') self.assertError('spell .') self.assertError('spell ?') self.assertNotError('spell whereever') self.assertNotRegexp('spell foo', 'whatever') def testErrno(self): self.assertRegexp('errno 12', '^ENOMEM') self.assertRegexp('errno ENOMEM', '#12') def testProgstats(self): self.assertNotError('progstats') def testCrypt(self): self.assertNotError('crypt jemfinch') @skipUnlessFortune def testFortune(self): self.assertNotError('fortune') @skipUnlessPing def testPing(self): self.assertNotError('unix ping 127.0.0.1') self.assertError('unix ping') self.assertError('unix ping -localhost') self.assertError('unix ping local%host') @skipUnlessPing def testPingCount(self): self.assertNotError('unix ping --c 1 127.0.0.1') self.assertError('unix ping --c a 127.0.0.1') self.assertRegexp('unix ping --c 11 127.0.0.1','10 packets') self.assertRegexp('unix ping 127.0.0.1','5 packets') @skipUnlessPing def testPingInterval(self): self.assertNotError('unix ping --i 1 --c 1 127.0.0.1') self.assertError('unix ping --i a --c 1 127.0.0.1') # Super-user privileged interval setting self.assertError('unix ping --i 0.1 --c 1 127.0.0.1') @skipUnlessPing def testPingTtl(self): self.assertNotError('unix ping --t 64 --c 1 127.0.0.1') self.assertError('unix ping --t a --c 1 127.0.0.1') @skipUnlessPing def testPingWait(self): self.assertNotError('unix ping --W 1 --c 1 127.0.0.1') self.assertError('unix ping --W a --c 1 127.0.0.1') @skipUnlessPing6 def testPing6(self): self.assertNotError('unix ping6 ::1') self.assertError('unix ping6') self.assertError('unix ping6 -localhost') self.assertError('unix ping6 local%host') @skipUnlessPing6 def testPing6Count(self): self.assertNotError('unix ping6 --c 1 ::1') self.assertError('unix ping6 --c a ::1') self.assertRegexp('unix ping6 --c 11 ::1','10 packets', timeout=12) self.assertRegexp('unix ping6 ::1','5 packets') @skipUnlessPing6 def testPing6Interval(self): self.assertNotError('unix ping6 --i 1 --c 1 ::1') self.assertError('unix ping6 --i a --c 1 ::1') # Super-user privileged interval setting self.assertError('unix ping6 --i 0.1 --c 1 ::1') @skipUnlessPing6 def testPing6Ttl(self): self.assertNotError('unix ping6 --t 64 --c 1 ::1') self.assertError('unix ping6 --t a --c 1 ::1') @skipUnlessPing6 def testPing6Wait(self): self.assertNotError('unix ping6 --W 1 --c 1 ::1') self.assertError('unix ping6 --W a --c 1 ::1') def testCall(self): self.assertNotError('unix call /bin/ls /') self.assertRegexp('unix call /bin/ls /', 'boot, .*dev, ') self.assertError('unix call /usr/bin/nosuchcommandaoeuaoeu') def testShellForbidden(self): self.assertNotError('unix call /bin/ls /') with conf.supybot.commands.allowShell.context(False): self.assertRegexp('unix call /bin/ls /', 'Error:.*not available.*supybot.commands.allowShell') def testUptime(self): self.assertNotError('unix sysuptime') def testUname(self): self.assertNotError('unix sysuname') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Unix/plugin.py0000644000175000017500000004674413233426066017612 0ustar valval00000000000000### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008-2010, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import re import pwd import sys import crypt import errno import random import select import struct import subprocess import shlex import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.utils.minisix as minisix import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Unix') def checkAllowShell(irc): if not conf.supybot.commands.allowShell(): irc.error(_('This command is not available, because ' 'supybot.commands.allowShell is False.'), Raise=True) _progstats_endline_remover = utils.str.MultipleRemover('\r\n') def progstats(): pw = pwd.getpwuid(os.getuid()) response = format('Process ID %i running as user %q and as group %q ' 'from directory %q with the command line %q. ' 'Running on Python %s.', os.getpid(), pw[0], pw[3], os.getcwd(), ' '.join(sys.argv), _progstats_endline_remover(sys.version)) return response class TimeoutError(IOError): pass def pipeReadline(fd, timeout=2): (r, _, _) = select.select([fd], [], [], timeout) if r: return r[0].readline() else: raise TimeoutError class Unix(callbacks.Plugin): """Provides Utilities for Unix-like systems.""" threaded = True @internationalizeDocstring def errno(self, irc, msg, args, s): """ Returns the number of an errno code, or the errno code of a number. """ try: i = int(s) name = errno.errorcode[i] except ValueError: name = s.upper() try: i = getattr(errno, name) except AttributeError: irc.reply(_('I can\'t find the errno number for that code.')) return except KeyError: name = _('(unknown)') irc.reply(format(_('%s (#%i): %s'), name, i, os.strerror(i))) errno = wrap(errno, ['something']) @internationalizeDocstring def progstats(self, irc, msg, args): """takes no arguments Returns various unix-y information on the running supybot process. """ irc.reply(progstats()) @internationalizeDocstring def pid(self, irc, msg, args): """takes no arguments Returns the current pid of the process for this Supybot. """ irc.reply(format('%i', os.getpid()), private=True) pid = wrap(pid, [('checkCapability', 'owner')]) _cryptre = re.compile(b'[./0-9A-Za-z]') @internationalizeDocstring def crypt(self, irc, msg, args, password, salt): """ [] Returns the resulting of doing a crypt() on . If is not given, uses a random salt. If running on a glibc2 system, prepending '$1$' to your salt will cause crypt to return an MD5sum based crypt rather than the standard DES based crypt. """ def makeSalt(): s = b'\x00' while self._cryptre.sub(b'', s) != b'': s = struct.pack(' Returns the result of passing to aspell/ispell. The results shown are sorted from best to worst in terms of being a likely match for the spelling of . """ # We are only checking the first word spellCmd = self.registryValue('spell.command') if not spellCmd: irc.error(_('The spell checking command is not configured. If one ' 'is installed, reconfigure ' 'supybot.plugins.Unix.spell.command appropriately.'), Raise=True) spellLang = self.registryValue('spell.language') or 'en' if word and not word[0].isalpha(): irc.error(_(' must begin with an alphabet character.')) return try: inst = subprocess.Popen([spellCmd, '-l', spellLang, '-a'], close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) except OSError as e: irc.error(e, Raise=True) ret = inst.poll() if ret is not None: s = inst.stderr.readline().decode('utf8') if not s: s = inst.stdout.readline().decode('utf8') s = s.rstrip('\r\n') s = s.lstrip('Error: ') irc.error(s, Raise=True) (out, err) = inst.communicate(word.encode()) inst.wait() lines = [x.decode('utf8') for x in out.splitlines() if x] lines.pop(0) # Banner if not lines: irc.error(_('No results found.'), Raise=True) line = lines.pop(0) line2 = '' if lines: line2 = lines.pop(0) # parse the output # aspell will sometimes list spelling suggestions after a '*' or '+' # line for complex words. if line[0] in '*+' and line2: line = line2 if line[0] in '*+': resp = format(_('%q may be spelled correctly.'), word) elif line[0] == '#': resp = format(_('I could not find an alternate spelling for %q'), word) elif line[0] == '&': matches = line.split(':')[1].strip() resp = format(_('Possible spellings for %q: %L.'), word, matches.split(', ')) else: resp = _('Something unexpected was seen in the [ai]spell output.') irc.reply(resp) spell = thread(wrap(spell, ['something'])) @internationalizeDocstring def fortune(self, irc, msg, args): """takes no arguments Returns a fortune from the *nix fortune program. """ channel = msg.args[0] fortuneCmd = self.registryValue('fortune.command') if fortuneCmd: args = [fortuneCmd] if self.registryValue('fortune.short', channel): args.append('-s') if self.registryValue('fortune.equal', channel): args.append('-e') if self.registryValue('fortune.offensive', channel): args.append('-a') args.extend(self.registryValue('fortune.files', channel)) try: with open(os.devnull) as null: inst = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=null) except OSError as e: irc.error(_('It seems the configured fortune command was ' 'not available.'), Raise=True) (out, err) = inst.communicate() inst.wait() if minisix.PY3: lines = [i.decode('utf-8').rstrip() for i in out.splitlines()] lines = list(map(str, lines)) else: lines = out.splitlines() lines = list(map(str.rstrip, lines)) lines = filter(None, lines) irc.replies(lines, joiner=' ') else: irc.error(_('The fortune command is not configured. If fortune is ' 'installed on this system, reconfigure the ' 'supybot.plugins.Unix.fortune.command configuration ' 'variable appropriately.')) @internationalizeDocstring def wtf(self, irc, msg, args, foo, something): """[is] Returns wtf is. 'wtf' is a *nix command that first appeared in NetBSD 1.5. In most *nices, it's available in some sort of 'bsdgames' package. """ wtfCmd = self.registryValue('wtf.command') if wtfCmd: something = something.rstrip('?') try: with open(os.devnull, 'r+') as null: inst = subprocess.Popen([wtfCmd, something], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=null) except OSError: irc.error(_('It seems the configured wtf command was not ' 'available.'), Raise=True) (out, foo) = inst.communicate() inst.wait() if out: response = out.decode('utf8').splitlines()[0].strip() response = utils.str.normalizeWhitespace(response) irc.reply(response) else: irc.error(_('The wtf command is not configured. If it is installed ' 'on this system, reconfigure the ' 'supybot.plugins.Unix.wtf.command configuration ' 'variable appropriately.')) wtf = thread(wrap(wtf, [optional(('literal', ['is'])), 'something'])) def _make_ping(command): def f(self, irc, msg, args, optlist, host): """[--c ] [--i ] [--t ] [--W ] [--4|--6] Sends an ICMP echo request to the specified host. The arguments correspond with those listed in ping(8). --c is limited to 10 packets or less (default is 5). --i is limited to 5 or less. --W is limited to 10 or less. --4 and --6 can be used if and only if the system has a unified ping command. """ pingCmd = self.registryValue(registry.join([command, 'command'])) if not pingCmd: irc.error('The ping command is not configured. If one ' 'is installed, reconfigure ' 'supybot.plugins.Unix.%s.command appropriately.' % command, Raise=True) else: try: host = host.group(0) except AttributeError: pass args = [pingCmd] for opt, val in optlist: if opt == 'c' and val > 10: val = 10 if opt == 'i' and val > 5: val = 5 if opt == 'W' and val > 10: val = 10 args.append('-%s' % opt) if opt not in ('4', '6'): args.append(str(val)) if '-c' not in args: args.append('-c') args.append(str(self.registryValue('ping.defaultCount'))) args.append(host) try: with open(os.devnull) as null: inst = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=null) except OSError as e: irc.error('It seems the configured ping command was ' 'not available (%s).' % e, Raise=True) result = inst.communicate() if result[1]: # stderr irc.error(' '.join(result[1].decode('utf8').split())) else: response = result[0].decode('utf8').split("\n"); if response[1]: irc.reply(' '.join(response[1].split()[3:5]).split(':')[0] + ': ' + ' '.join(response[-3:])) else: irc.reply(' '.join(response[0].split()[1:3]) + ': ' + ' '.join(response[-3:])) f.__name__ = command _hostExpr = re.compile(r'^[a-z0-9][a-z0-9\.-]*[a-z0-9]$', re.I) return thread(wrap(f, [getopts({'c':'positiveInt','i':'float', 't':'positiveInt','W':'positiveInt', '4':'', '6':''}), first('ip', ('matches', _hostExpr, 'Invalid hostname'))])) ping = _make_ping('ping') ping6 = _make_ping('ping6') def sysuptime(self, irc, msg, args): """takes no arguments Returns the uptime from the system the bot is runnning on. """ uptimeCmd = self.registryValue('sysuptime.command') if uptimeCmd: args = [uptimeCmd] try: with open(os.devnull) as null: inst = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=null) except OSError as e: irc.error('It seems the configured uptime command was ' 'not available.', Raise=True) (out, err) = inst.communicate() inst.wait() lines = out.splitlines() lines = [x.decode('utf8').rstrip() for x in lines] lines = filter(None, lines) irc.replies(lines, joiner=' ') else: irc.error('The uptime command is not configured. If uptime is ' 'installed on this system, reconfigure the ' 'supybot.plugins.Unix.sysuptime.command configuration ' 'variable appropriately.') def sysuname(self, irc, msg, args): """takes no arguments Returns the uname -a from the system the bot is runnning on. """ unameCmd = self.registryValue('sysuname.command') if unameCmd: args = [unameCmd, '-a'] try: with open(os.devnull) as null: inst = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=null) except OSError as e: irc.error('It seems the configured uptime command was ' 'not available.', Raise=True) (out, err) = inst.communicate() inst.wait() lines = out.splitlines() lines = [x.decode('utf8').rstrip() for x in lines] lines = filter(None, lines) irc.replies(lines, joiner=' ') else: irc.error('The uname command is not configured. If uname is ' 'installed on this system, reconfigure the ' 'supybot.plugins.Unix.sysuname.command configuration ' 'variable appropriately.') def call(self, irc, msg, args, text): """ Calls any command available on the system, and returns its output. Requires owner capability. Note that being restricted to owner, this command does not do any sanity checking on input/output. So it is up to you to make sure you don't run anything that will spamify your channel or that will bring your machine to its knees. """ checkAllowShell(irc) self.log.info('Unix: running command "%s" for %s/%s', text, msg.nick, irc.network) args = shlex.split(text) try: with open(os.devnull) as null: inst = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=null) except OSError as e: irc.error('It seems the requested command was ' 'not available (%s).' % e, Raise=True) result = inst.communicate() if result[1]: # stderr irc.error(' '.join(result[1].decode('utf8').split())) if result[0]: # stdout response = result[0].decode('utf8').splitlines() response = [l for l in response if l] irc.replies(response) call = thread(wrap(call, ["owner", "text"])) def shell(self, irc, msg, args, text): """ Calls any command available on the system using the shell specified by the SHELL environment variable, and returns its output. Requires owner capability. Note that being restricted to owner, this command does not do any sanity checking on input/output. So it is up to you to make sure you don't run anything that will spamify your channel or that will bring your machine to its knees. """ checkAllowShell(irc) self.log.info('Unix: running command "%s" for %s/%s', text, msg.nick, irc.network) try: with open(os.devnull) as null: inst = subprocess.Popen(text, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=null) except OSError as e: irc.error('It seems the shell (%s) was not available (%s)' % (os.getenv('SHELL'), e), Raise=True) result = inst.communicate() if result[1]: # stderr irc.error(' '.join(result[1].decode('utf8').split())) if result[0]: # stdout response = result[0].decode('utf8').splitlines() response = [l for l in response if l] irc.replies(response) shell = thread(wrap(shell, ["owner", "text"])) Class = Unix # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Unix/config.py0000644000175000017500000001410013233426066017536 0ustar valval00000000000000### # 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. ### import supybot.conf as conf import supybot.utils as utils import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Unix') from . import plugin progstats = plugin.progstats 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 output, expect, anything, something, yn conf.registerPlugin('Unix', True) output(_("""The "progstats" command can reveal potentially sensitive information about your machine. Here's an example of its output: %s\n""") % progstats()) if yn(_('Would you like to disable this command for non-owner users?'), default=True): conf.supybot.commands.disabled().add('Unix.progstats') class NonOptionString(registry.String): errormsg = _('Value must be a string not starting with a dash (-), not %r.') def __init__(self, *args, **kwargs): self.__parent = super(NonOptionString, self) self.__parent.__init__(*args, **kwargs) def setValue(self, v): if v.startswith('-'): self.error(v) else: self.__parent.setValue(v) class SpaceSeparatedListOfNonOptionStrings(registry.SpaceSeparatedListOfStrings): Value = NonOptionString Unix = conf.registerPlugin('Unix') conf.registerGroup(Unix, 'fortune') conf.registerGlobalValue(Unix.fortune, 'command', registry.String(utils.findBinaryInPath('fortune') or '', _("""Determines what command will be called for the fortune command."""))) conf.registerChannelValue(Unix.fortune, 'short', registry.Boolean(True, _("""Determines whether only short fortunes will be used if possible. This sends the -s option to the fortune program."""))) conf.registerChannelValue(Unix.fortune, 'equal', registry.Boolean(True, _("""Determines whether fortune will give equal weight to the different fortune databases. If false, then larger databases will be given more weight. This sends the -e option to the fortune program."""))) conf.registerChannelValue(Unix.fortune, 'offensive', registry.Boolean(False, _("""Determines whether fortune will retrieve offensive fortunes along with the normal fortunes. This sends the -a option to the fortune program."""))) conf.registerChannelValue(Unix.fortune, 'files', SpaceSeparatedListOfNonOptionStrings([], _("""Determines what specific file (if any) will be used with the fortune command; if none is given, the system-wide default will be used. Do note that this fortune file must be placed with the rest of your system's fortune files."""))) conf.registerGroup(Unix, 'spell') conf.registerGlobalValue(Unix.spell, 'command', registry.String(utils.findBinaryInPath('aspell') or utils.findBinaryInPath('ispell') or '', _("""Determines what command will be called for the spell command."""))) conf.registerGlobalValue(Unix.spell, 'language', registry.String('en', _("""Determines what aspell dictionary will be used for spell checking."""))) conf.registerGroup(Unix, 'wtf') conf.registerGlobalValue(Unix.wtf, 'command', registry.String(utils.findBinaryInPath('wtf') or '', _("""Determines what command will be called for the wtf command."""))) conf.registerGroup(Unix, 'ping') conf.registerGlobalValue(Unix.ping, 'command', registry.String(utils.findBinaryInPath('ping') or '', """Determines what command will be called for the ping command.""")) conf.registerGlobalValue(Unix.ping, 'defaultCount', registry.PositiveInteger(5, """Determines what ping and ping6 counts (-c) will default to.""")) conf.registerGroup(Unix, 'ping6') conf.registerGlobalValue(Unix.ping6, 'command', registry.String(utils.findBinaryInPath('ping6') or '', """Determines what command will be called for the ping6 command.""")) conf.registerGroup(Unix, 'sysuptime') conf.registerGlobalValue(Unix.sysuptime, 'command', registry.String(utils.findBinaryInPath('uptime') or '', """Determines what command will be called for the uptime command.""")) conf.registerGroup(Unix, 'sysuname') conf.registerGlobalValue(Unix.sysuname, 'command', registry.String(utils.findBinaryInPath('uname') or '', """Determines what command will be called for the uname command.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Unix/__init__.py0000644000175000017500000000501413233426066020034 0ustar valval00000000000000### # 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. ### """ Provides commands available only on Unix. """ 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__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch # 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/Unix/download' from . import config from . import plugin from imp import reload 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! if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Unix/locales/0000755000175000017500000000000013233426077017347 5ustar valval00000000000000limnoria-2018.01.25/plugins/Unix/locales/it.po0000644000175000017500000002465613233426066020336 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2012-01-02 20:09+0100\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: config.py:47 msgid "" "The \"progstats\" command can reveal potentially sensitive\n" " information about your machine. Here's an example of its output:\n" "\n" " %s\n" msgstr "" "Il comando \"progstats\" può potenzialmente rivelare informazioni sensibili\n" " riguardo la tua macchina. Ecco un esempio del suo output:\n" "\n" " %s\n" #: config.py:51 msgid "Would you like to disable this command for non-owner users?" msgstr "Vuoi disabilitare questo comando per gli utenti non proprietari (owner)?" #: config.py:59 msgid "" "Determines\n" " what command will be called for the fortune command." msgstr "Determina quale comando verrà richiamato tramite \"fortune\"." #: config.py:62 msgid "" "Determines whether only short fortunes will be\n" " used if possible. This sends the -s option to the fortune program." msgstr "Determina se verranno utilizzati solo fortune brevi. Questo invia l'opzione -s al programma fortune." #: config.py:65 msgid "" "Determines whether fortune will give equal\n" " weight to the different fortune databases. If false, then larger\n" " databases will be given more weight. This sends the -e option to the\n" " fortune program." msgstr "" "Determines se fortune darà uguale peso ai diversi database; se impostato a False,\n" " quelli più grossi avranno la priorità. Questo invia l'opzione -e al programma fortune." #: config.py:70 msgid "" "Determines whether fortune will retrieve\n" " offensive fortunes along with the normal fortunes. This sends the -a\n" " option to the fortune program." msgstr "" "Determina se fortune recupererà citazioni offensive insieme alle normali.\n" " Questo invia l'opzione -a al programma fortune." #: config.py:74 msgid "" "Determines what specific file\n" " (if any) will be used with the fortune command; if none is given, the\n" " system-wide default will be used. Do note that this fortune file must be\n" " placed with the rest of your system's fortune files." msgstr "" "Determina quale file specifico (eventuale) verrà usato con il comando fortune,\n" " se non è specificato utilizzerà il predefinito a livello di sistema. Nota che\n" " questo file va collocato insieme agli altri file di fortune presenti nel sistema.\n" #: config.py:82 msgid "" "Determines\n" " what command will be called for the spell command." msgstr "Determina quale comando verrà richiamato tramite \"spell\"." #: config.py:85 msgid "" "Determines what aspell dictionary will be used\n" " for spell checking." msgstr "" "Determina quale dizionario di aspell verrà utilizzato per il controllo ortografico." #: config.py:90 msgid "" "Determines what\n" " command will be called for the wtf command." msgstr "Determina quale comando verrà richiamato tramite \"wtf\"." #: plugin.py:75 #, docstring msgid "" "\n" "\n" " Returns the number of an errno code, or the errno code of a number.\n" " " msgstr "" "\n" "\n" " Restituisce il numero di un codice di errore (errno) o il codice di errore di un numero.\n" " " #: plugin.py:87 msgid "I can't find the errno number for that code." msgstr "Non trovo un numero di errore per quel codice." #: plugin.py:90 msgid "(unknown)" msgstr "(sconosciuto)" #: plugin.py:91 msgid "%s (#%i): %s" msgstr "%s (#%i): %s" #: plugin.py:96 #, docstring msgid "" "takes no arguments\n" "\n" " Returns various unix-y information on the running supybot process.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta varie informazioni unix-y sul processo supybot in esecuzione.\n" " " #: plugin.py:104 #, docstring msgid "" "takes no arguments\n" "\n" " Returns the current pid of the process for this Supybot.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta l'attuale PID del processo per questo Supybot.\n" " " #: plugin.py:114 #, docstring msgid "" " []\n" "\n" " Returns the resulting of doing a crypt() on . If is\n" " not given, uses a random salt. If running on a glibc2 system,\n" " prepending '$1$' to your salt will cause crypt to return an MD5sum\n" " based crypt rather than the standard DES based crypt.\n" " " msgstr "" " []\n" "\n" " Riporta il risultato di crypt() su . Se non è specificato,\n" " ne utilizza uno casuale. Se eseguito su un sistema glibc2, preporre '$1$' al\n" " sale restituirà una cifratura basata su MD5sum piuttosto che la standard DES.\n" " " #: plugin.py:133 #, docstring msgid "" "\n" "\n" " Returns the result of passing to aspell/ispell. The results\n" " shown are sorted from best to worst in terms of being a likely match\n" " for the spelling of .\n" " " msgstr "" "\n" "\n" " Riporta il risultato di passare ad aspell/ispell. I risultati\n" " mostrati sono ordinati dal migliore al peggiore in termini di essere una\n" " potenziale ortografia corretta di .\n" " " #: plugin.py:142 msgid "The spell checking command is not configured. If one is installed, reconfigure supybot.plugins.Unix.spell.command appropriately." msgstr "Il comando di correzione ortografica non è configurato. Se ve n'è uno installato, configurare la variabile supybot.plugins.Unix.spell.command in modo appropriato." #: plugin.py:148 msgid " must begin with an alphabet character." msgstr " deve iniziare con un carattere dell'alfabeto." #: plugin.py:170 msgid "No results found." msgstr "Nessun risultato trovato." #: plugin.py:181 msgid "%q may be spelled correctly." msgstr "%q sembra scritto correttamente." #: plugin.py:183 msgid "I could not find an alternate spelling for %q" msgstr "Impossibile trovare un'ortografia alternativa per %q" #: plugin.py:187 msgid "Possible spellings for %q: %L." msgstr "Ortografia possibile per %q: %L." #: plugin.py:190 msgid "Something unexpected was seen in the [ai]spell output." msgstr "È stato trovato qualcosa di inaspettato nell'output di [ai]spell." #: plugin.py:196 #, docstring msgid "" "takes no arguments\n" "\n" " Returns a fortune from the *nix fortune program.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Restituisce un biscottino della fortuna dal programma *nix fortune.\n" " " #: plugin.py:217 msgid "It seems the configured fortune command was not available." msgstr "Sembra che il comando fortune configurato non sia disponibile." #: plugin.py:226 msgid "The fortune command is not configured. If fortune is installed on this system, reconfigure the supybot.plugins.Unix.fortune.command configuration variable appropriately." msgstr "Il comando fortune non è configurato. Se ve n'è uno installato, configurare la variabile supybot.plugins.Unix.fortune.command in modo appropriato." #: plugin.py:233 #, docstring msgid "" "[is] \n" "\n" " Returns wtf is. 'wtf' is a *nix command that first\n" " appeared in NetBSD 1.5. In most *nices, it's available in some sort\n" " of 'bsdgames' package.\n" " " msgstr "" "[is] \n" "\n" " Restituisce il significato di . \"wtf\" è un comando *nix apparso la prima volta\n" " in NetBSD 1.5. Nella maggior parte delle macchine *nix è disponibile nel pacchetto \"bsdgames\".\n" " " #: plugin.py:248 msgid "It seems the configured wtf command was not available." msgstr "Sembra che il comando wtf configurato non sia disponibile." #: plugin.py:257 msgid "The wtf command is not configured. If it is installed on this system, reconfigure the supybot.plugins.Unix.wtf.command configuration variable appropriately." msgstr "Il comando wtf non è configurato. Se ve n'è uno installato, configurare la variabile supybot.plugins.Unix.wtf.command in modo appropriato." #: plugin.py:265 #, docstring msgid "" "[--c ] [--i ] [--t ] [--W ] \n" " Sends an ICMP echo request to the specified host.\n" " The arguments correspond with those listed in ping(8). --c is\n" " limited to 10 packets or less (default is 5). --i is limited to 5\n" " or less. --W is limited to 10 or less.\n" " " msgstr "" "[--c ] [--i ] [--t ] [--W ] \n" " Invia una pacchetto ICMP di tipo echo request all'host specificato. Gli\n" " argomenti corrispondono a quelli del comando ping(8). --c è limitato a 10\n" " pacchetti (il default è 5). --i è limitato a 5. --W è limitato a 10.\n" " " #: plugin.py:317 #, docstring msgid "" "takes no arguments\n" "\n" " Returns the uptime from the system the bot is runnning on.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta l'uptime del sistema su cui è in esecuzione il bot.\n" " " #: plugin.py:345 #, docstring msgid "" "takes no arguments\n" "\n" " Returns the uname -a from the system the bot is runnning on.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta il risultato di 'uname -a' dal sistema su cui è in esecuzione il bot.\n" " " #: plugin.py:373 #, docstring msgid "" " \n" " Calls any command available on the system, and returns its output.\n" " Requires owner capability.\n" " Note that being restricted to owner, this command does not do any\n" " sanity checking on input/output. So it is up to you to make sure\n" " you don't run anything that will spamify your channel or that \n" " will bring your machine to its knees. \n" " " msgstr "" " \n" " Richiama qualsiai comando disponibile sul sistema e restituisce il suo\n" " output; richiede la capacità owner. Essendo limitato al proprietario,\n" " questo comando non fa alcun controllo su input e output; per cui sta a\n" " te assicurarti di non eseguire nulla che possa inviare molto testo in\n" " canale (flood) o che metta in ginocchio la tua macchina.\n" " " limnoria-2018.01.25/plugins/Unix/locales/fr.po0000644000175000017500000002432513233426066020322 0ustar valval00000000000000# Valentin Lorentz , 2012. msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2012-04-15 18:47+EEST\n" "PO-Revision-Date: 2012-07-29 11:58+0100\n" "Last-Translator: Valentin Lorentz \n" "Language-Team: French \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Lokalize 1.2\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: config.py:47 msgid "" "The \"progstats\" command can reveal potentially sensitive\n" " information about your machine. Here's an example of its output:\n" "\n" " %s\n" msgstr "La commande \"progstats\" peut révéler des informations potentiellement sensibles à propos de votre machine. Voici un exemple de sa sortie : %s" #: config.py:51 msgid "Would you like to disable this command for non-owner users?" msgstr "Voulez-vous désactiver cette commande pour les utilisateurs non-owners ?" #: config.py:59 msgid "" "Determines\n" " what command will be called for the fortune command." msgstr "Détermine quelle commande doit être appelée par la commande 'fortune'" #: config.py:62 msgid "" "Determines whether only short fortunes will be\n" " used if possible. This sends the -s option to the fortune program." msgstr "Détermine si seules les fortunes courtes seront utilisées si possible. Ceci envoie l'option -s au programme fortune." #: config.py:65 msgid "" "Determines whether fortune will give equal\n" " weight to the different fortune databases. If false, then larger\n" " databases will be given more weight. This sends the -e option to the\n" " fortune program." msgstr "Détermine si les fortunes auront le même poids quel que soit la base de données. Si c'est False, alors les plus grandes bases de données auront plus de poids. Ceci envoie l'option -e au programme fortune." #: config.py:70 msgid "" "Determines whether fortune will retrieve\n" " offensive fortunes along with the normal fortunes. This sends the -a\n" " option to the fortune program." msgstr "Détermine si 'fortune' récupérera des fortunes offensibles, en plus des fortunes normales. Ceci envoie l'option -a au programme 'fortune'." #: config.py:74 msgid "" "Determines what specific file\n" " (if any) will be used with the fortune command; if none is given, the\n" " system-wide default will be used. Do note that this fortune file must be\n" " placed with the rest of your system's fortune files." msgstr "Détermine quel fichier spécifique (si il y en a un) est utilisé par la commande 'fortune' ; si aucune n'est donné, le réglage par défaut du système sera utilisé. Notez que ce fichier doit être placé avec le reste de vos fichiers de fortunes." #: config.py:82 msgid "" "Determines\n" " what command will be called for the spell command." msgstr "Détermine quelle commande sera appelée par la commande 'spell'." #: config.py:85 msgid "" "Determines what aspell dictionary will be used\n" " for spell checking." msgstr "Détermine quelle commande sera appelée par la commande 'spell'." #: config.py:90 msgid "" "Determines what\n" " command will be called for the wtf command." msgstr "Détermine quelle commande sera appelée par la commande 'wtf'." #: plugin.py:75 msgid "" "\n" "\n" " Returns the number of an errno code, or the errno code of a number.\n" " " msgstr "" "\n" "\n" "Retourne le code du numéro d'erreur, ou le numéro d'erreur du code." #: plugin.py:87 msgid "I can't find the errno number for that code." msgstr "Je ne peux trouver le numéro d'erreur pour ce code." #: plugin.py:90 msgid "(unknown)" msgstr "(inconnu)" #: plugin.py:91 msgid "%s (#%i): %s" msgstr "%s (#%i) : %s" #: plugin.py:96 msgid "" "takes no arguments\n" "\n" " Returns various unix-y information on the running supybot process.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourn différentes informations unix-y à propos du processus de supybot." #: plugin.py:104 msgid "" "takes no arguments\n" "\n" " Returns the current pid of the process for this Supybot.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le pid du processus de Supybot." #: plugin.py:114 msgid "" " []\n" "\n" " Returns the resulting of doing a crypt() on . If is\n" " not given, uses a random salt. If running on a glibc2 system,\n" " prepending '$1$' to your salt will cause crypt to return an MD5sum\n" " based crypt rather than the standard DES based crypt.\n" " " msgstr "" " []\n" "\n" "Retourne le résultat d'un crypt() sur le . Si le n'est pas donné, utilise un sel aléatoire. Si on est sur un système glibc2, ajouter '$1$' au début de votre sel fera que crypt retournera un crypt basé sur MD5sum plutôt que sur le crypt basé sur DES standard." #: plugin.py:133 msgid "" "\n" "\n" " Returns the result of passing to aspell/ispell. The results\n" " shown are sorted from best to worst in terms of being a likely match\n" " for the spelling of .\n" " " msgstr "" "\n" "\n" "Retourne le résultat du parsage du avec aspell/ispell. Les résultats affichés sont triés du meilleur au pire en fonction de comment ils correspondent au ." #: plugin.py:142 msgid "The spell checking command is not configured. If one is installed, reconfigure supybot.plugins.Unix.spell.command appropriately." msgstr "La commande de vérification orthographique ne semble pas être installée. Si il y en a une, configurez supybot.plugins.Unix.spell.command de façon appropriée." #: plugin.py:148 msgid " must begin with an alphabet character." msgstr " doit commencer par une lettre de l'alphabet." #: plugin.py:170 msgid "No results found." msgstr "Aucun résultat trouvé." #: plugin.py:181 msgid "%q may be spelled correctly." msgstr "%q semble être orthographié correctement." #: plugin.py:183 msgid "I could not find an alternate spelling for %q" msgstr "Je ne peux pas trouver d'orthographe alternative pour %q" #: plugin.py:187 msgid "Possible spellings for %q: %L." msgstr "Orthographes possibles pour %q : %L" #: plugin.py:190 msgid "Something unexpected was seen in the [ai]spell output." msgstr "Quelque chose d'imprévu a été trouvé dans la sortie de [ai]spell." #: plugin.py:196 msgid "" "takes no arguments\n" "\n" " Returns a fortune from the *nix fortune program.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne une fortune depuis le programme fortune de *nix." #: plugin.py:217 msgid "It seems the configured fortune command was not available." msgstr "Il semble que la commande fortune configurée ne soit pas disponible." #: plugin.py:226 msgid "The fortune command is not configured. If fortune is installed on this system, reconfigure the supybot.plugins.Unix.fortune.command configuration variable appropriately." msgstr "La commande fortune n'est pas configurée. Si fortune est installé sur ce système, configurez supybot.plugins.Unix.fortune.command de façon appropriée." #: plugin.py:233 msgid "" "[is] \n" "\n" " Returns wtf is. 'wtf' is a *nix command that first\n" " appeared in NetBSD 1.5. In most *nices, it's available in some sort\n" " of 'bsdgames' package.\n" " " msgstr "" "[is] \n" "\n" "Retourne ce que l' pourrait signifier. 'wtf' est une commande *nix qui est apparue dans NetBSD 1.5. Dans la plupart des machines unices, elle fait partie du paquet 'bsdgames'." #: plugin.py:248 msgid "It seems the configured wtf command was not available." msgstr "Il semble que la commande wtf ne soit pas disponible." #: plugin.py:257 msgid "The wtf command is not configured. If it is installed on this system, reconfigure the supybot.plugins.Unix.wtf.command configuration variable appropriately." msgstr "La commande wtf n'est pas configurée. Si elle est installée sur ce système, veuillez configurer supybot.plugins.Unix.wtf.command de façon appropriée." #: plugin.py:265 msgid "" "[--c ] [--i ] [--t ] [--W ] \n" " Sends an ICMP echo request to the specified host.\n" " The arguments correspond with those listed in ping(8). --c is\n" " limited to 10 packets or less (default is 5). --i is limited to 5\n" " or less. --W is limited to 10 or less.\n" " " msgstr "" "[--c ] [--i ] [--t ] [--W ] \n" "Envoie une requête ICMP echo à l' spécifié. Les arguments correspondent à ceux listés dans ping(8).--c est limité à 10 paquets (5 par défaut).--i est limité à 5 (5 par défaut).--W est limité à 10." #: plugin.py:317 msgid "" "takes no arguments\n" "\n" " Returns the uptime from the system the bot is runnning on.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne l'uptime du système sur lequel fonctionne le bot." #: plugin.py:345 msgid "" "takes no arguments\n" "\n" " Returns the uname -a from the system the bot is runnning on.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le \"uname -a\" du système sur lequel le bot fonctionne." #: plugin.py:373 msgid "" " \n" " Calls any command available on the system, and returns its output.\n" " Requires owner capability.\n" " Note that being restricted to owner, this command does not do any\n" " sanity checking on input/output. So it is up to you to make sure\n" " you don't run anything that will spamify your channel or that \n" " will bring your machine to its knees. \n" " " msgstr "" "\n" "\n" "Appelle n'importe quelle commande disponible sur le système, et retourne sa sortie. Requiert la capacité owner.Notez que comme elle est restreinte au propriétaire, cette commande n'effectue aucune vérification sur les entrées/sorties. Donc, c'est à vous de vous assurez que ça ne fera rien qui spammera le canal ou que ça ne va pas faire tomber votre machine à genoux." limnoria-2018.01.25/plugins/Unix/locales/fi.po0000644000175000017500000003220713233426066020307 0ustar valval00000000000000# Unix plugin in Limnoria # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Unix plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 11:59+EET\n" "PO-Revision-Date: 2014-12-20 13:10+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: 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.6.10\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: config.py:47 msgid "" "The \"progstats\" command can reveal potentially sensitive\n" " information about your machine. Here's an example of its " "output:\n" "\n" " %s\n" msgstr "" "\"Progstats\" komento voi paljastaa mahdollisesti henkilökohtaista tietoa " "tietokoneestasi.\n" " Tässä on näyte sen ulostulosta:\n" "\n" " %s\n" #: config.py:51 msgid "Would you like to disable this command for non-owner users?" msgstr "" "Haluaisitko poistaa tämän komennon käytöstä muille käyttäjille, kuin " "omistajille?" #: config.py:59 msgid "" "Determines\n" " what command will be called for the fortune command." msgstr "" "Määrittää minkä komennon 'fortune'\n" " komento kutsuu." #: config.py:62 msgid "" "Determines whether only short fortunes will be\n" " used if possible. This sends the -s option to the fortune program." msgstr "" "Määrittää käytetäänkö vain lyhyitä ennustuksia, jos se on mahdollista.\n" " Tämä lähettää fortune ohjelmalle -s asetuksen." #: config.py:65 msgid "" "Determines whether fortune will give equal\n" " weight to the different fortune databases. If false, then larger\n" " databases will be given more weight. This sends the -e option to the\n" " fortune program." msgstr "" "Määrittää antaako 'fortune'\n" " yhtäpaljon painoa erilaisille ennustustietokannoille. Jos tämä asetus " "on 'false', niin\n" " suuremmille tietokannoille annetaan enemmän painoa. Tämä lähettää -e " "asetuksen\n" " ennustus ohjelmalle." #: config.py:70 msgid "" "Determines whether fortune will retrieve\n" " offensive fortunes along with the normal fortunes. This sends the -a\n" " option to the fortune program." msgstr "" "Määrittää hakeeko 'fortune' myös loukkaavia ennustuksia tavallisten\n" " ennustusten lisäksi. Tämä lähettää -a\n" " asetuksen ennustus ohjelmalle." #: config.py:74 msgid "" "Determines what specific file\n" " (if any) will be used with the fortune command; if none is given, the\n" " system-wide default will be used. Do note that this fortune file must " "be\n" " placed with the rest of your system's fortune files." msgstr "" "Määrittää mitä tiettyä tietokantaa\n" " (jos mitään) 'fortune' käyttää; jos yhtään ei ole käytetty, \n" " järjestelmän laajuista oletusta käytetään. Huomaa, että tämän " "ennustustiedoston täytyy olla\n" " sijoitettuna muiden järjestelmän ennustustiedostojen kanssa." #: config.py:82 msgid "" "Determines\n" " what command will be called for the spell command." msgstr "" "Määrittää minkä komennon 'spell'\n" " komento kutsuu." #: config.py:85 msgid "" "Determines what aspell dictionary will be used\n" " for spell checking." msgstr "" "Määrittää mitä aspell sanakirjaa käytetään\n" " oikeinkirjoituksen tarkistukseen." #: config.py:90 msgid "" "Determines what\n" " command will be called for the wtf command." msgstr "" "Määrittää minkä komennon\n" " 'wtf' komento kutsuu." #: plugin.py:74 msgid "Provides Utilities for Unix-like systems." msgstr "Tarjoaa työkaluja Unixin kaltaisille järjestelmille." #: plugin.py:78 msgid "" "\n" "\n" " Returns the number of an errno code, or the errno code of a number.\n" " " msgstr "" "\n" "\n" " Palauttaa virhenumeron , tai virhekoodin virhenumeron.\n" " " #: plugin.py:90 msgid "I can't find the errno number for that code." msgstr "En voi löytää virhenumeroa tuolle koodille." #: plugin.py:93 msgid "(unknown)" msgstr "(tuntematon)" #: plugin.py:94 msgid "%s (#%i): %s" msgstr "%s (#%i): %s" #: plugin.py:99 msgid "" "takes no arguments\n" "\n" " Returns various unix-y information on the running supybot process.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa muutamia unixmaisia tietoja suoritettavasta supybot " "prosessista.\n" " " #: plugin.py:107 msgid "" "takes no arguments\n" "\n" " Returns the current pid of the process for this Supybot.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa tämän Supybot prosessin nykyisen pidin.\n" " " #: plugin.py:117 msgid "" " []\n" "\n" " Returns the resulting of doing a crypt() on . If " "is\n" " not given, uses a random salt. If running on a glibc2 system,\n" " prepending '$1$' to your salt will cause crypt to return an MD5sum\n" " based crypt rather than the standard DES based crypt.\n" " " msgstr "" " []\n" "\n" " Palauttaa crypt():in tuloksen . Jos ei ole\n" " annettu, satunnaista suolaa käytetään. Jos suoritetaan glibc2 " "järjestelmällä,\n" " '$1$' lisääminen kryptaukseesi aiheuttaa MD5 summaan perustuvan " "kryptauksen, mielummin kuin\n" " normaalin DES pohjaisen kryptin.\n" " " #: plugin.py:136 msgid "" "\n" "\n" " Returns the result of passing to aspell/ispell. The results\n" " shown are sorted from best to worst in terms of being a likely " "match\n" " for the spelling of .\n" " " msgstr "" "\n" "\n" " Palauttaa lähetyksen aspell/ispell ohjelmaan. Palautuvat " "tulokset\n" " näytetään järjestyksessä parhaasta huonompaan sillä perusteella, " "kuinka todennäköisesti ne ovat oikein kirjoitettuja\n" " .\n" " " #: plugin.py:145 msgid "" "The spell checking command is not configured. If one is installed, " "reconfigure supybot.plugins.Unix.spell.command appropriately." msgstr "" "Oikeinkirjoituksen tarkistusohjelma ei ole säädetty. Jos sellainen on " "asennttu, säädä supybot.plugins.Unix.spell.command sopivaksi." #: plugin.py:151 msgid " must begin with an alphabet character." msgstr " täytyy alkaa aakkosellisella merkillä." #: plugin.py:173 msgid "No results found." msgstr "Tuloksia ei löytynyt." #: plugin.py:184 msgid "%q may be spelled correctly." msgstr "%q saattaa olla kirjoitettu oikein." #: plugin.py:186 msgid "I could not find an alternate spelling for %q" msgstr "En löytänyt vaihtoehtoista kirjoitustapaa sanalle %q" #: plugin.py:190 msgid "Possible spellings for %q: %L." msgstr "Mahdolliset kirjoitustavat sanalle %q ovat: %L." #: plugin.py:193 msgid "Something unexpected was seen in the [ai]spell output." msgstr "Jotakin odottamatonta nähtiin [ai]spellin ulostulossa." #: plugin.py:199 msgid "" "takes no arguments\n" "\n" " Returns a fortune from the *nix fortune program.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa ennustuksen *nix ennustusohjelmalta.\n" " " #: plugin.py:221 msgid "It seems the configured fortune command was not available." msgstr "Näyttää siltä, että määritetty ennustusohjelma ei ollut saatavilla." #: plugin.py:234 msgid "" "The fortune command is not configured. If fortune is installed on this " "system, reconfigure the supybot.plugins.Unix.fortune.command configuration " "variable appropriately." msgstr "" "Ennustuskomento ei ole määritetty. Jos fortune on asennettu tähän " "järjestelmään, määritä uudelleen asetusarvo supybot.plugins.Unix.fortune." "command oikein." #: plugin.py:241 msgid "" "[is] \n" "\n" " Returns wtf is. 'wtf' is a *nix command that first\n" " appeared in NetBSD 1.5. In most *nices, it's available in some " "sort\n" " of 'bsdgames' package.\n" " " msgstr "" "[is] \n" "\n" " Palauttaa mikä ihme on. 'wtf' on *nix komento, joka " "ilmestyi ensin\n" " NetBSD 1.5 käyttöjärjestelmässä. Suurimmassa osassa *nixeistä, se " "on saatavilla jonkinlaisessa\n" " 'bsdgames' paketissa.\n" " " #: plugin.py:257 msgid "It seems the configured wtf command was not available." msgstr "Vaikuttaa siltä, ettei määritetty wtf komento ollut saatavilla." #: plugin.py:266 msgid "" "The wtf command is not configured. If it is installed on this system, " "reconfigure the supybot.plugins.Unix.wtf.command configuration variable " "appropriately." msgstr "" "Wtf komento ei ole määritetty. Jos se on asennettu tähän järjestelmään, " "määritä supybot.plugins.Unix.wtf.command asetusarvo oikein." #: plugin.py:332 msgid "" "takes no arguments\n" "\n" " Returns the uptime from the system the bot is runnning on.\n" " " msgstr "" "ei ota parametrejän\n" " Palauttaa järjestelmän, jolla botti on ylläoloajan.\n" " " #: plugin.py:361 msgid "" "takes no arguments\n" "\n" " Returns the uname -a from the system the bot is runnning on.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa komennon \"uname -a\" ulostulon järjestelmästä, jossa botti " "on.\n" " " #: plugin.py:390 msgid "" "\n" " Calls any command available on the system, and returns its output.\n" " Requires owner capability.\n" " Note that being restricted to owner, this command does not do any\n" " sanity checking on input/output. So it is up to you to make sure\n" " you don't run anything that will spamify your channel or that\n" " will bring your machine to its knees.\n" " " msgstr "" " \n" " Kutsuu minkä tahansa komennon, joka on saatavilla järjestelmässä " "palauttaen sen ulostulon.\n" " Vaatii owner-valtuuden.\n" " Huomaa että, koska tämä komento on rajoitettu omistajalle, se ei " "tee\n" " minkäänlaista järjellisyystarkistusta sisäänmenoon/ulostuloon. Joten " "on oma tehtäväsi varmistaa, ettet suorita mitään, mikä sotkee kanavaasi, tai " "laittaa koneesi polvilleen. \n" " " #: plugin.py:420 msgid "" "\n" " Calls any command available on the system using the shell\n" " specified by the SHELL environment variable, and returns its\n" " output.\n" " Requires owner capability.\n" " Note that being restricted to owner, this command does not do any\n" " sanity checking on input/output. So it is up to you to make sure\n" " you don't run anything that will spamify your channel or that\n" " will bring your machine to its knees.\n" " " msgstr "" " \n" " Kutsuu minkä tahansa komennon, joka on saatavilla järjestelmässä " "käyttäen SHELL ympäristömuuttujaa, ja palauttaa sen ulostulon.\n" " Vaatii owner-valtuuden.\n" " Huomaa, että, koska tämä komento on rajoitettu omistajalle, se ei " "tee\n" " minkäänlaista järjellisyystarkistusta sisäänmenoon/ulostuloon. Joten " "on oma tehtäväsi varmistaa, ettet suorita mitään, mikä sotkee kanavaasi tai " "joka\n" " laittaa koneesi\n" " polvilleen. \n" " " #~ msgid "" #~ "[--c ] [--i ] [--t ] [--W ] \n" #~ " Sends an ICMP echo request to the specified host.\n" #~ " The arguments correspond with those listed in ping(8). --c is\n" #~ " limited to 10 packets or less (default is 5). --i is limited to " #~ "5\n" #~ " or less. --W is limited to 10 or less.\n" #~ " " #~ msgstr "" #~ "[--c ] [--i ] [--t ] [--W ] \n" #~ " Lähettää ICMP kaiutuspyynnön määritettyyn isäntään.\n" #~ " Parametrin täsmäävät niihin, jotka on määritetty ohjekirjasivulla " #~ "ping(8). --c on\n" #~ " rajoitettu kymmeneen tai vähempään (oletus on 5). --i on " #~ "rajoitettu viiteen\n" #~ " tai vähempään. --W on rajoitettu kymmeneen tai vähempään.\n" #~ " " #~ msgid "" #~ "[--c ] [--i ] [--t ] [--W ] \n" #~ " Sends an ICMP echo request to the specified host.\n" #~ " The arguments correspond with those listed in ping6(8). --c is\n" #~ " limited to 10 packets or less (default is 5). --i is limited to " #~ "5\n" #~ " or less. --W is limited to 10 or less.\n" #~ " " #~ msgstr "" #~ "[--c ] [--i ] [--t ] [--W ] \n" #~ " Lähettää ICMP kaiutuspyynnön määritettyyn isäntään.\n" #~ " Parametrin täsmäävät niihin, jotka on määritetty ohjekirjasivulla " #~ "ping(8). --c on\n" #~ " rajoitettu kymmeneen tai vähempään (oletus on 5). --i on " #~ "rajoitettu viiteen\n" #~ " tai vähempään. --W on rajoitettu kymmeneen tai vähempään.\n" #~ " " limnoria-2018.01.25/plugins/URL/0000755000175000017500000000000013233426077015444 5ustar valval00000000000000limnoria-2018.01.25/plugins/URL/test.py0000644000175000017500000001014513233426066016774 0ustar valval00000000000000### # 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 * urls = """ http://www.ureg.ohio-state.edu/courses/book3.asp http://wwwsearch.sourceforge.net/ClientForm/ http://slashdot.org/comments.pl?sid=75443&cid=6747654 http://baseball-almanac.com/rb_menu.shtml http://www.linuxquestions.org/questions/showthread.php?postid=442905#post442905 http://games.slashdot.org/comments.pl?sid=76027&cid=6785588' http://games.slashdot.org/comments.pl?sid=76027&cid=6785588 http://www.census.gov/ftp/pub/tiger/tms/gazetteer/zcta5.zip http://slashdot.org/~Strike http://lambda.weblogs.com/xml/rss.xml' http://lambda.weblogs.com/xml/rss.xml http://www.sourcereview.net/forum/index.php?showforum=8 http://www.sourcereview.net/forum/index.php?showtopic=291 http://www.sourcereview.net/forum/index.php?showtopic=291&st=0&#entry1778 http://dhcp065-024-059-168.columbus.rr.com:81/~jfincher/old-supybot.tar.gz http://www.sourcereview.net/forum/index.php? http://www.joelonsoftware.com/articles/BuildingCommunitieswithSo.html http://gameknot.com/stats.pl?ddipaolo http://slashdot.org/slashdot.rss http://gameknot.com/chess.pl?bd=1038943 http://codecentral.sleepwalkers.org/ http://gameknot.com/chess.pl?bd=1037471&r=327 http://dhcp065-024-059-168.columbus.rr.com:81/~jfincher/angryman.py https://sourceforge.net/projects/pyrelaychecker/ http://gameknot.com/tsignup.pl """.strip().splitlines() class URLTestCase(ChannelPluginTestCase): plugins = ('URL',) def test(self): counter = 0 #self.assertNotError('url random') for url in urls: self.assertRegexp('url stats', str(counter)) self.feedMsg(url) counter += 1 self.assertRegexp('url stats', str(counter)) self.assertRegexp('url last', re.escape(urls[-1])) self.assertRegexp('url last --proto https', re.escape(urls[-2])) self.assertRegexp('url last --with gameknot.com', re.escape(urls[-1])) self.assertRegexp('url last --with dhcp', re.escape(urls[-3])) self.assertRegexp('url last --from alsdkjf', '^No') self.assertRegexp('url last --without game', 'sourceforge') #self.assertNotError('url random') def testDefaultNotFancy(self): self.feedMsg(urls[0]) self.assertResponse('url last', urls[0]) def testStripsColors(self): self.feedMsg('\x031foo \x034' + urls[0]) self.assertResponse('url last', urls[0]) def testAction(self): self.irc.feedMsg(ircmsgs.action(self.channel, urls[1])) self.assertNotRegexp('url last', '\\x01') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/URL/plugin.py0000644000175000017500000001473113233426066017320 0ustar valval00000000000000### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010, James McCoy # 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.dbi as dbi import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.utils.minisix as minisix import supybot.plugins as plugins import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('URL') class UrlRecord(dbi.Record): __fields__ = [ ('url', eval), ('by', eval), ('near', eval), ('at', eval), ] class DbiUrlDB(plugins.DbiChannelDB): class DB(dbi.DB): Record = UrlRecord def add(self, url, msg): record = self.Record(url=url, by=msg.nick, near=msg.args[1], at=msg.receivedAt) super(self.__class__, self).add(record) def urls(self, p): L = list(self.select(p)) L.reverse() return L URLDB = plugins.DB('URL', {'flat': DbiUrlDB}) class URL(callbacks.Plugin): """This plugin records how many URLs have been mentioned in a channel and what the last URL was.""" def __init__(self, irc): self.__parent = super(URL, self) self.__parent.__init__(irc) self.db = URLDB() def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return channel = msg.args[0] if irc.isChannel(channel): if ircmsgs.isAction(msg): text = ircmsgs.unAction(msg) else: text = msg.args[1] for url in utils.web.urlRe.findall(text): r = self.registryValue('nonSnarfingRegexp', channel) if r and r.search(url): self.log.debug('Skipping adding %u to db.', url) continue self.log.debug('Adding %u to db.', url) self.db.add(channel, url, msg) @internationalizeDocstring def stats(self, irc, msg, args, channel): """[] Returns the number of URLs in the URL database. is only required if the message isn't sent in the channel itself. """ self.db.vacuum(channel) count = self.db.size(channel) irc.reply(format(_('I have %n in my database.'), (count, 'URL'))) stats = wrap(stats, ['channeldb']) @internationalizeDocstring def last(self, irc, msg, args, channel, optlist): """[] [--{from,with,without,near,proto} ] [--nolimit] Gives the last URL matching the given criteria. --from is from whom the URL came; --proto is the protocol the URL used; --with is something inside the URL; --without is something that should not be in the URL; --near is something in the same message as the URL. If --nolimit is given, returns all the URLs that are found to just the URL. is only necessary if the message isn't sent in the channel itself. """ predicates = [] f = None nolimit = False for (option, arg) in optlist: if isinstance(arg, minisix.string_types): arg = arg.lower() if option == 'nolimit': nolimit = True elif option == 'from': def f(record, arg=arg): return ircutils.strEqual(record.by, arg) elif option == 'with': def f(record, arg=arg): return arg in record.url.lower() elif option == 'without': def f(record, arg=arg): return arg not in record.url.lower() elif option == 'proto': def f(record, arg=arg): return record.url.lower().startswith(arg) elif option == 'near': def f(record, arg=arg): return arg in record.near.lower() if f is not None: predicates.append(f) def predicate(record): for predicate in predicates: if not predicate(record): return False return True urls = [record.url for record in self.db.urls(channel, predicate)] if not urls: irc.reply(_('No URLs matched that criteria.')) else: if nolimit: urls = [format('%u', url) for url in urls] s = ', '.join(urls) else: # We should optimize this with another URLDB method eventually. s = urls[0] irc.reply(s) last = wrap(last, ['channeldb', getopts({'from': 'something', 'with': 'something', 'near': 'something', 'proto': 'something', 'nolimit': '', 'without': 'something',})]) Class = URL # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/URL/config.py0000644000175000017500000000503113233426066017260 0ustar valval00000000000000### # 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('URL') 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('URL', True) URL = conf.registerPlugin('URL') conf.registerChannelValue(URL, 'nonSnarfingRegexp', registry.Regexp(None, _("""Determines what URLs are not to be snarfed and stored in the database for the channel; URLs matching the given regexp will not be snarfed. Give the empty string if you have no URLs that you'd like to exclude from being snarfed."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/URL/__init__.py0000644000175000017500000000500413233426066017552 0ustar valval00000000000000### # 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. ### """ Keeps track of URLs posted to a channel, along with relevant context. Allows searching for URLs and returning random URLs. Also provides statistics on the URLs in the database. """ 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__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from imp import reload 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! if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/URL/locales/0000755000175000017500000000000013233426077017066 5ustar valval00000000000000limnoria-2018.01.25/plugins/URL/locales/it.po0000644000175000017500000000520213233426066020037 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2011-08-10 02:43+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: config.py:45 msgid "" "Determines what URLs are not to be snarfed and\n" " stored in the database for the channel; URLs matching the given regexp will\n" " not be snarfed. Give the empty string if you have no URLs that you'd like\n" " to exclude from being snarfed." msgstr "" "Determina quali URL non vanno intercettati e memorizzati nel database del canale;\n" " quelli che corrispondono alla regexp fornita non verranno coinvolti.\n" " Se non vuoi escludere alcun URL, aggiungi una stringa vuota.\n" #: plugin.py:89 #, docstring msgid "" "[]\n" "\n" " Returns the number of URLs in the URL database. is only\n" " required if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta il numero di URL nel database. è richiesto\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:96 msgid "I have %n in my database." msgstr "Ho %n nel mio database." #: plugin.py:101 #, docstring msgid "" "[] [--{from,with,without,near,proto} ] [--nolimit]\n" "\n" " Gives the last URL matching the given criteria. --from is from whom\n" " the URL came; --proto is the protocol the URL used; --with is something\n" " inside the URL; --without is something that should not be in the URL;\n" " --near is something in the same message as the URL. If --nolimit is\n" " given, returns all the URLs that are found to just the URL.\n" " is only necessary if the message isn't sent in the channel\n" " itself.\n" " " msgstr "" "[] [--{from,with,without,near,proto} ] [--nolimit]\n" "\n" " Fornisce l'ultimo URL che corrisponde al criterio specificato. --from equivale\n" " a chi ha inserito l'URL; --proto è il protocollo dell'URL usato; --with è\n" " qualcosa all'interno dell'URL, mentre --without è qualcosa non presente;\n" " --near qualcosa nell'URL stesso. Se --nolimit è specificato, riporta\n" " tutti gli URL equivalenti a URL trovati. è necessario solo se il\n" " messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:143 msgid "No URLs matched that criteria." msgstr "Nessun URL corrisponde a questo criterio." limnoria-2018.01.25/plugins/URL/locales/fr.po0000644000175000017500000000522613233426066020040 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-08-10 11:27+CEST\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:45 msgid "" "Determines what URLs are not to be snarfed and\n" " stored in the database for the channel; URLs matching the given regexp will\n" " not be snarfed. Give the empty string if you have no URLs that you'd like\n" " to exclude from being snarfed." msgstr "Détermine quelles URLs sont écoutées et stockées dans la base de canal ; les URLs correspondant à l'expression régulière ne seront pas écoutées. Donnez une chaîne vide si vous ne voulez exclure aucune URL" #: plugin.py:89 msgid "" "[]\n" "\n" " Returns the number of URLs in the URL database. is only\n" " required if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne le nombre d'URLs dans ma base de données d'URLs. n'est nécessaire que si le message n'est pas envoyé sur la canal lui-même." #: plugin.py:96 msgid "I have %n in my database." msgstr "J'ai %n dans ma base de données." #: plugin.py:101 #, fuzzy msgid "" "[] [--{from,with,without,near,proto} ] [--nolimit]\n" "\n" " Gives the last URL matching the given criteria. --from is from whom\n" " the URL came; --proto is the protocol the URL used; --with is something\n" " inside the URL; --without is something that should not be in the URL;\n" " --near is something in the same message as the URL. If --nolimit is\n" " given, returns all the URLs that are found to just the URL.\n" " is only necessary if the message isn't sent in the channel\n" " itself.\n" " " msgstr "" "[] [--{from,with,without,near,proto} ] [--nolimit]\n" "\n" "Donne la dernière URL correspondant aux critères. --from correspond à l'utilisateur ayant posé l'URL ; --proto est le protocole utilisé pour l'URL ; --with est quelque chose dans l'URL ; --without est le contraire de --with --near est quelque chose dans le même message que l'URL ; si --nolimit est donné, retourne toutes les URLs trouvées, et non une seule. n'est nécessaire que si la commande n'est pas envoyée sur le canal lui-même." #: plugin.py:143 msgid "No URLs matched that criteria." msgstr "Aucune URL ne correspond à ces critères." limnoria-2018.01.25/plugins/URL/locales/fi.po0000644000175000017500000000653313233426066020031 0ustar valval00000000000000# URL plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: URL plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 14:04+EET\n" "PO-Revision-Date: 2014-12-20 14:41+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: 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" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: config.py:45 msgid "" "Determines what URLs are not to be snarfed and\n" " stored in the database for the channel; URLs matching the given regexp " "will\n" " not be snarfed. Give the empty string if you have no URLs that you'd " "like\n" " to exclude from being snarfed." msgstr "" "Määrittää, mitkä URL-osoitteet eivät tule kaapatuiksi eivätkä\n" " tallennetuiksi tietokantaan; URL-osoitteet, jotka täsmäävät annettuun " "säännölliseen lausekkeeseen eivät tule\n" " kaapatuiksi. Anna tyhjä säännöllinen lauseke, mikäli haluat ettei yhtään " "URL-osoitetta \n" " jätetä kaappaamatta." #: plugin.py:65 msgid "" "This plugin records how many URLs have been mentioned in\n" " a channel and what the last URL was." msgstr "" "Tämä plugini tallentaa kuinka monta URL-osoitetta on mainittu kanavalla\n" " ja mikä viimeisin URL oli." #: plugin.py:91 msgid "" "[]\n" "\n" " Returns the number of URLs in the URL database. is only\n" " required if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa tietokannassa olevien URL-osoitteiden määrän. on " "vaadittu vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:98 msgid "I have %n in my database." msgstr "Minulla on %n tietokannassani." #: plugin.py:103 msgid "" "[] [--{from,with,without,near,proto} ] [--nolimit]\n" "\n" " Gives the last URL matching the given criteria. --from is from " "whom\n" " the URL came; --proto is the protocol the URL used; --with is " "something\n" " inside the URL; --without is something that should not be in the " "URL;\n" " --near is something in the same message as the URL. If --nolimit " "is\n" " given, returns all the URLs that are found to just the URL.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--{from,with,without,near,proto} ] [--nolimit]\n" "\n" " Antaa viimeisen URL-osoitteen, joka täsmää annettuihin " "kriteereihin. --from on keneltä\n" " URL-osoite tuli; --proto on protokolla, jota URL-osoite käytti; --" "with on jokin\n" " URL-osoitteen sisällä; --without on jotakin, jonka ei pitäisi olla " "URL-osoitteessa;\n" " --near on jotakin samassa viestissä, kuin URL-osoite. Jos --nolimit " "on\n" " annettu, palauttaa kaikki täsmäävät URL-osoitteet, jotka löydetään.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:145 msgid "No URLs matched that criteria." msgstr "Yksikään URL-osoite ei täsmännyt noihin kriteereihin." limnoria-2018.01.25/plugins/Topic/0000755000175000017500000000000013233426077016060 5ustar valval00000000000000limnoria-2018.01.25/plugins/Topic/test.py0000644000175000017500000003135613233426066017417 0ustar valval00000000000000### # Copyright (c) 2002-2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from supybot.test import * class TopicTestCase(ChannelPluginTestCase): plugins = ('Topic','User',) def testRemove(self): self.assertError('topic remove 1') _ = self.getMsg('topic add foo') _ = self.getMsg('topic add bar') _ = self.getMsg('topic add baz') self.assertError('topic remove 0') self.assertNotError('topic remove 3') self.assertNotError('topic remove 2') self.assertNotError('topic remove 1') self.assertError('topic remove 1') def testRemoveMultiple(self): self.assertError('topic remove 1 2') _ = self.getMsg('topic add foo') _ = self.getMsg('topic add bar') _ = self.getMsg('topic add baz') _ = self.getMsg('topic add derp') _ = self.getMsg('topic add cheese') self.assertNotError('topic remove 1 2') self.assertNotError('topic remove -1 1') self.assertError('topic remove -99 1') def testReplace(self): _ = self.getMsg('topic add foo') _ = self.getMsg('topic add bar') _ = self.getMsg('topic add baz') self.assertRegexp('topic replace 1 oof', 'oof.*bar.*baz') self.assertRegexp('topic replace -1 zab', 'oof.*bar.*zab') self.assertRegexp('topic replace 2 lorem ipsum', 'oof.*lorem ipsum.*zab') self.assertRegexp('topic replace 2 rab', 'oof.*rab.*zab') def testGet(self): self.assertError('topic get 1') _ = self.getMsg('topic add foo') _ = self.getMsg('topic add bar') _ = self.getMsg('topic add baz') self.assertRegexp('topic get 1', '^foo') self.assertError('topic get 0') def testAdd(self): self.assertError('topic add #floorgle') m = self.getMsg('topic add foo') self.assertEqual(m.command, 'TOPIC') self.assertEqual(m.args[0], self.channel) self.assertEqual(m.args[1], 'foo') m = self.getMsg('topic add bar') self.assertEqual(m.command, 'TOPIC') self.assertEqual(m.args[0], self.channel) self.assertEqual(m.args[1], 'foo | bar') def testManageCapabilities(self): try: self.irc.feedMsg(ircmsgs.mode(self.channel, args=('+o', self.nick), prefix=self.prefix)) self.irc.feedMsg(ircmsgs.mode(self.channel, args=('+t'), prefix=self.prefix)) world.testing = False origuser = self.prefix self.prefix = 'stuff!stuff@stuff' self.assertNotError('register nottester stuff', private=True) self.assertError('topic add foo') origconf = conf.supybot.plugins.Topic.requireManageCapability() conf.supybot.plugins.Topic.requireManageCapability.setValue('') self.assertNotError('topic add foo') finally: world.testing = True self.prefix = origuser conf.supybot.plugins.Topic.requireManageCapability.setValue(origconf) def testInsert(self): m = self.getMsg('topic add foo') self.assertEqual(m.args[1], 'foo') m = self.getMsg('topic insert bar') self.assertEqual(m.args[1], 'bar | foo') def testChange(self): _ = self.getMsg('topic add foo') _ = self.getMsg('topic add bar') _ = self.getMsg('topic add baz') self.assertRegexp('topic change -1 s/baz/biff/', r'foo.*bar.*biff') self.assertRegexp('topic change 2 s/bar/baz/', r'foo.*baz.*biff') self.assertRegexp('topic change 1 s/foo/bar/', r'bar.*baz.*biff') self.assertRegexp('topic change -2 s/baz/bazz/', r'bar.*bazz.*biff') self.assertError('topic change 0 s/baz/biff/') def testConfig(self): try: original = conf.supybot.plugins.Topic.separator() conf.supybot.plugins.Topic.separator.setValue(' <==> ') _ = self.getMsg('topic add foo') m = self.getMsg('topic add bar') self.failUnless('<==>' in m.args[1]) finally: conf.supybot.plugins.Topic.separator.setValue(original) def testReorder(self): _ = self.getMsg('topic add foo') _ = self.getMsg('topic add bar') _ = self.getMsg('topic add baz') self.assertRegexp('topic reorder 2 1 3', r'bar.*foo.*baz') self.assertRegexp('topic reorder 3 -2 1', r'baz.*foo.*bar') self.assertError('topic reorder 0 1 2') self.assertError('topic reorder 1 -2 2') self.assertError('topic reorder 1 2') self.assertError('topic reorder 2 3 4') self.assertError('topic reorder 1 2 2') self.assertError('topic reorder 1 1 2 3') _ = self.getMsg('topic remove 1') _ = self.getMsg('topic remove 1') self.assertError('topic reorder 1') _ = self.getMsg('topic remove 1') self.assertError('topic reorder 0') def testList(self): _ = self.getMsg('topic add foo') self.assertRegexp('topic list', '1: foo') _ = self.getMsg('topic add bar') self.assertRegexp('topic list', '1: foo.*2: bar') _ = self.getMsg('topic add baz') self.assertRegexp('topic list', '1: foo.* 2: bar.* and 3: baz') def testSet(self): _ = self.getMsg('topic add foo') self.assertRegexp('topic set -1 bar', 'bar') self.assertNotRegexp('topic set -1 baz', 'bar') self.assertResponse('topic set foo bar baz', 'foo bar baz') # Catch a bug we had where setting topic 1 would reset the whole topic orig = conf.supybot.plugins.Topic.format() sep = conf.supybot.plugins.Topic.separator() try: conf.supybot.plugins.Topic.format.setValue('$topic') self.assertResponse('topic add baz', 'foo bar baz%sbaz' % sep) self.assertResponse('topic set 1 bar', 'bar%sbaz' % sep) finally: conf.supybot.plugins.Topic.format.setValue(orig) def testRestore(self): self.getMsg('topic set foo') self.assertResponse('topic restore', 'foo') self.getMsg('topic remove 1') restoreError = 'Error: I haven\'t yet set the topic in #test.' self.assertResponse('topic restore', restoreError) def testRefresh(self): self.getMsg('topic set foo') self.assertResponse('topic refresh', 'foo') self.getMsg('topic remove 1') refreshError = 'Error: I haven\'t yet set the topic in #test.' self.assertResponse('topic refresh', refreshError) def testUndo(self): try: original = conf.supybot.plugins.Topic.format() conf.supybot.plugins.Topic.format.setValue('$topic') self.assertResponse('topic set ""', '') self.assertResponse('topic add foo', 'foo') self.assertResponse('topic add bar', 'foo | bar') self.assertResponse('topic add baz', 'foo | bar | baz') self.assertResponse('topic undo', 'foo | bar') self.assertResponse('topic undo', 'foo') self.assertResponse('topic undo', '') finally: conf.supybot.plugins.Topic.format.setValue(original) def testUndoRedo(self): try: original = conf.supybot.plugins.Topic.format() conf.supybot.plugins.Topic.format.setValue('$topic') self.assertResponse('topic set ""', '') self.assertResponse('topic add foo', 'foo') self.assertResponse('topic add bar', 'foo | bar') self.assertResponse('topic add baz', 'foo | bar | baz') self.assertResponse('topic undo', 'foo | bar') self.assertResponse('topic undo', 'foo') self.assertResponse('topic undo', '') self.assertResponse('topic redo', 'foo') self.assertResponse('topic redo', 'foo | bar') self.assertResponse('topic redo', 'foo | bar | baz') self.assertResponse('topic undo', 'foo | bar') self.assertResponse('topic undo', 'foo') self.assertResponse('topic redo', 'foo | bar') self.assertResponse('topic undo', 'foo') self.assertResponse('topic redo', 'foo | bar') finally: conf.supybot.plugins.Topic.format.setValue(original) def testSwap(self): original = conf.supybot.plugins.Topic.format() try: conf.supybot.plugins.Topic.format.setValue('$topic') self.assertResponse('topic set ""', '') self.assertResponse('topic add foo', 'foo') self.assertResponse('topic add bar', 'foo | bar') self.assertResponse('topic add baz', 'foo | bar | baz') self.assertResponse('topic swap 1 2', 'bar | foo | baz') self.assertResponse('topic swap 1 -1', 'baz | foo | bar') self.assertError('topic swap -1 -1') self.assertError('topic swap 2 -2') self.assertError('topic swap 1 -3') self.assertError('topic swap -2 2') self.assertError('topic swap -3 1') finally: conf.supybot.plugins.Topic.format.setValue(original) def testDefault(self): self.assertError('topic default') try: original = conf.supybot.plugins.Topic.default() conf.supybot.plugins.Topic.default.setValue('foo bar baz') self.assertResponse('topic default', 'foo bar baz') finally: conf.supybot.plugins.Topic.default.setValue(original) def testTopic(self): original = conf.supybot.plugins.Topic.format() try: conf.supybot.plugins.Topic.format.setValue('$topic') self.assertError('topic addd') # Error to send too many args. self.assertResponse('topic add foo', 'foo') self.assertResponse('topic add bar', 'foo | bar') self.assertResponse('topic', 'foo | bar') finally: conf.supybot.plugins.Topic.format.setValue(original) def testSeparator(self): original = conf.supybot.plugins.Topic.format() try: conf.supybot.plugins.Topic.format.setValue('$topic') self.assertResponse('topic add foo', 'foo') self.assertResponse('topic add bar', 'foo | bar') self.assertResponse('topic add baz', 'foo | bar | baz') self.assertResponse('topic separator ::', 'foo :: bar :: baz') self.assertResponse('topic separator ||', 'foo || bar || baz') self.assertResponse('topic separator |', 'foo | bar | baz') finally: conf.supybot.plugins.Topic.format.setValue(original) def testFit(self): original = conf.supybot.plugins.Topic.format() try: conf.supybot.plugins.Topic.format.setValue('$topic') self.irc.state.supported['TOPICLEN'] = 20 self.assertResponse('topic fit foo', 'foo') self.assertResponse('topic fit bar', 'foo | bar') self.assertResponse('topic fit baz', 'foo | bar | baz') self.assertResponse('topic fit qux', 'bar | baz | qux') finally: conf.supybot.plugins.Topic.format.setValue(original) self.irc.state.supported.pop('TOPICLEN', None) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Topic/plugin.py0000644000175000017500000006065313233426066017740 0ustar valval00000000000000### # Copyright (c) 2002-2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import re import random import shutil import tempfile import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization _ = PluginInternationalization('Topic') import supybot.ircdb as ircdb import supybot.utils.minisix as minisix pickle = minisix.pickle def canChangeTopic(irc, msg, args, state): assert not state.channel callConverter('channel', irc, msg, args, state) callConverter('inChannel', irc, msg, args, state) if state.channel not in irc.state.channels: state.error(format(_('I\'m not currently in %s.'), state.channel), Raise=True) c = irc.state.channels[state.channel] if 't' in c.modes and not c.isHalfopPlus(irc.nick): state.error(format(_('I can\'t change the topic, I\'m not (half)opped ' 'and %s is +t.'), state.channel), Raise=True) def getTopic(irc, msg, args, state, format=True): separator = state.cb.registryValue('separator', state.channel) if separator in args[0] and not \ state.cb.registryValue('allowSeparatorinTopics', state.channel): state.errorInvalid('topic', args[0], format(_('The topic must not include %q.'), separator)) topic = args.pop(0) if format: env = {'topic': topic} formatter = state.cb.registryValue('format', state.channel) topic = ircutils.standardSubstitute(irc, msg, formatter, env) state.args.append(topic) def getTopicNumber(irc, msg, args, state): def error(s): state.errorInvalid(_('topic number'), s) try: n = int(args[0]) if not n: raise ValueError except ValueError: error(args[0]) if n > 0: n -= 1 topic = irc.state.getTopic(state.channel) separator = state.cb.registryValue('separator', state.channel) topics = splitTopic(topic, separator) if not topics: state.error(format(_('There are no topics in %s.'), state.channel), Raise=True) try: topics[n] except IndexError: error(args[0]) del args[0] while n < 0: n += len(topics) state.args.append(n) addConverter('topic', getTopic) addConverter('topicNumber', getTopicNumber) addConverter('canChangeTopic', canChangeTopic) def splitTopic(topic, separator): return list(filter(None, topic.split(separator))) datadir = conf.supybot.directories.data() filename = conf.supybot.directories.data.dirize('Topic.pickle') class Topic(callbacks.Plugin): """This plugin allows you to use many topic-related functions, such as Add, Undo, and Remove.""" def __init__(self, irc): self.__parent = super(Topic, self) self.__parent.__init__(irc) self.undos = ircutils.IrcDict() self.redos = ircutils.IrcDict() self.lastTopics = ircutils.IrcDict() self.watchingFor332 = ircutils.IrcSet() try: pkl = open(filename, 'rb') try: self.undos = pickle.load(pkl) self.redos = pickle.load(pkl) self.lastTopics = pickle.load(pkl) self.watchingFor332 = pickle.load(pkl) except Exception as e: self.log.debug('Unable to load pickled data: %s', e) pkl.close() except IOError as e: self.log.debug('Unable to open pickle file: %s', e) world.flushers.append(self._flush) def die(self): world.flushers.remove(self._flush) self.__parent.die() def _flush(self): try: pklfd, tempfn = tempfile.mkstemp(suffix='topic', dir=datadir) pkl = os.fdopen(pklfd, 'wb') try: pickle.dump(self.undos, pkl) pickle.dump(self.redos, pkl) pickle.dump(self.lastTopics, pkl) pickle.dump(self.watchingFor332, pkl) except Exception as e: self.log.warning('Unable to store pickled data: %s', e) pkl.close() shutil.move(tempfn, filename) except (IOError, shutil.Error) as e: self.log.warning('File error: %s', e) def _splitTopic(self, topic, channel): separator = self.registryValue('separator', channel) return splitTopic(topic, separator) def _joinTopic(self, channel, topics): separator = self.registryValue('separator', channel) return separator.join(topics) def _addUndo(self, channel, topics): stack = self.undos.setdefault(channel, []) stack.append(topics) maxLen = self.registryValue('undo.max', channel) del stack[:len(stack) - maxLen] def _addRedo(self, channel, topics): stack = self.redos.setdefault(channel, []) stack.append(topics) maxLen = self.registryValue('undo.max', channel) del stack[:len(stack) - maxLen] def _getUndo(self, channel): try: return self.undos[channel].pop() except (KeyError, IndexError): return None def _getRedo(self, channel): try: return self.redos[channel].pop() except (KeyError, IndexError): return None def _formatTopics(self, irc, channel, topics, fit=False): topics = [s for s in topics if s and not s.isspace()] self.lastTopics[channel] = topics newTopic = self._joinTopic(channel, topics) try: maxLen = irc.state.supported['topiclen'] if fit: while len(newTopic) > maxLen: topics.pop(0) self.lastTopics[channel] = topics newTopic = self._joinTopic(channel, topics) elif len(newTopic) > maxLen: if self.registryValue('recognizeTopiclen', channel): irc.error(format(_('That topic is too long for this ' 'server (maximum length: %i; this topic: ' '%i).'), maxLen, len(newTopic)), Raise=True) except KeyError: pass return newTopic def _sendTopics(self, irc, channel, topics=None, isDo=False, fit=False): if isinstance(topics, list) or isinstance(topics, tuple): assert topics is not None topics = self._formatTopics(irc, channel, topics, fit) self._addUndo(channel, topics) if not isDo and channel in self.redos: del self.redos[channel] irc.queueMsg(ircmsgs.topic(channel, topics)) irc.noReply() def _checkManageCapabilities(self, irc, msg, channel): """Check if the user has any of the required capabilities to manage the channel topic. The list of required capabilities is in requireManageCapability channel config. Also allow if the user is a chanop. Since they can change the topic manually anyway. """ c = irc.state.channels[channel] if msg.nick in c.ops or msg.nick in c.halfops or 't' not in c.modes: return True capabilities = self.registryValue('requireManageCapability', channel) if capabilities: for capability in re.split(r'\s*;\s*', capabilities): if capability.startswith('channel,'): capability = ircdb.makeChannelCapability( channel, capability[8:]) if capability and ircdb.checkCapability(msg.prefix, capability): return capabilities = self.registryValue('requireManageCapability', channel) irc.errorNoCapability(capabilities, Raise=True) else: return def doJoin(self, irc, msg): if ircutils.strEqual(msg.nick, irc.nick): # We're joining a channel, let's watch for the topic. self.watchingFor332.add(msg.args[0]) def do315(self, irc, msg): # Try to restore the topic when not set yet. channel = msg.args[1] c = irc.state.channels.get(channel) if c is None or not self.registryValue('setOnJoin', channel): return if irc.nick not in c.ops and 't' in c.modes: self.log.debug('Not trying to restore topic in %s. I\'m not opped ' 'and %s is +t.', channel, channel) return try: topics = self.lastTopics[channel] except KeyError: self.log.debug('No topic to auto-restore in %s.', channel) else: newTopic = self._formatTopics(irc, channel, topics) if c.topic == '' or (c.topic != newTopic and self.registryValue('alwaysSetOnJoin', channel)): self._sendTopics(irc, channel, newTopic) def do332(self, irc, msg): if msg.args[1] in self.watchingFor332: self.watchingFor332.remove(msg.args[1]) # Store an undo for the topic when we join a channel. This allows # us to undo the first topic change that takes place in a channel. self._addUndo(msg.args[1], [msg.args[2]]) def topic(self, irc, msg, args, channel): """[] Returns the topic for . is only necessary if the message isn't sent in the channel itself. """ topic = irc.state.channels[channel].topic irc.reply(topic) topic = wrap(topic, ['inChannel']) def add(self, irc, msg, args, channel, topic): """[] Adds to the topics for . is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) topics.append(topic) self._sendTopics(irc, channel, topics) add = wrap(add, ['canChangeTopic', rest('topic')]) def fit(self, irc, msg, args, channel, topic): """[] Adds to the topics for . If the topic is too long for the server, topics will be popped until there is enough room. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) topics.append(topic) self._sendTopics(irc, channel, topics, fit=True) fit = wrap(fit, ['canChangeTopic', rest('topic')]) def replace(self, irc, msg, args, channel, i, topic): """[] Replaces topic with . """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) topics[i] = topic self._sendTopics(irc, channel, topics) replace = wrap(replace, ['canChangeTopic', 'topicNumber', rest('topic')]) def insert(self, irc, msg, args, channel, topic): """[] Adds to the topics for at the beginning of the topics currently on . is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) topics.insert(0, topic) self._sendTopics(irc, channel, topics) insert = wrap(insert, ['canChangeTopic', rest('topic')]) def shuffle(self, irc, msg, args, channel): """[] Shuffles the topics in . is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) if len(topics) == 0 or len(topics) == 1: irc.error(_('I can\'t shuffle 1 or fewer topics.'), Raise=True) elif len(topics) == 2: topics.reverse() else: original = topics[:] while topics == original: random.shuffle(topics) self._sendTopics(irc, channel, topics) shuffle = wrap(shuffle, ['canChangeTopic']) def reorder(self, irc, msg, args, channel, numbers): """[] [ ...] Reorders the topics from in the order of the specified arguments. is a one-based index into the topics. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) num = len(topics) if num == 0 or num == 1: irc.error(_('I cannot reorder 1 or fewer topics.'), Raise=True) if len(numbers) != num: irc.error(_('All topic numbers must be specified.'), Raise=True) if sorted(numbers) != list(range(num)): irc.error(_('Duplicate topic numbers cannot be specified.')) return newtopics = [topics[i] for i in numbers] self._sendTopics(irc, channel, newtopics) reorder = wrap(reorder, ['canChangeTopic', many('topicNumber')]) def list(self, irc, msg, args, channel): """[] Returns a list of the topics in , prefixed by their indexes. Mostly useful for topic reordering. is only necessary if the message isn't sent in the channel itself. """ topics = self._splitTopic(irc.state.getTopic(channel), channel) L = [] for (i, t) in enumerate(topics): L.append(format(_('%i: %s'), i + 1, utils.str.ellipsisify(t, 30))) s = utils.str.commaAndify(L) irc.reply(s) list = wrap(list, ['inChannel']) def get(self, irc, msg, args, channel, number): """[] Returns topic number from . is a one-based index into the topics. is only necessary if the message isn't sent in the channel itself. """ topics = self._splitTopic(irc.state.getTopic(channel), channel) irc.reply(topics[number]) get = wrap(get, ['inChannel', 'topicNumber']) def change(self, irc, msg, args, channel, number, replacer): """[] Changes the topic number on according to the regular expression . is the one-based index into the topics; is a regular expression of the form s/regexp/replacement/flags. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) topics[number] = replacer(topics[number]) self._sendTopics(irc, channel, topics) change = wrap(change, ['canChangeTopic', 'topicNumber', 'regexpReplacer']) def set(self, irc, msg, args, channel, number, topic): """[] [] Sets the topic to be . If no is given, this sets the entire topic. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) if number is not None: topics = self._splitTopic(irc.state.getTopic(channel), channel) topics[number] = topic else: topics = [topic] self._sendTopics(irc, channel, topics) set = wrap(set, ['canChangeTopic', optional('topicNumber'), rest(('topic', False))]) def remove(self, irc, msg, args, channel, numbers): """[] [ ...] Removes topics from the topic for Topics are numbered starting from 1; you can also use negative indexes to refer to topics starting the from the end of the topic. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) numbers = set(numbers) for n in numbers: # Equivalent of marking the topic for deletion; there's no # simple, easy way of removing multiple items from a list. # pop() will shift the indices after every run. topics[n] = '' topics = [topic for topic in topics if topic != ''] self._sendTopics(irc, channel, topics) remove = wrap(remove, ['canChangeTopic', many('topicNumber')]) def lock(self, irc, msg, args, channel): """[] Locks the topic (sets the mode +t) in . is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) irc.queueMsg(ircmsgs.mode(channel, '+t')) irc.noReply() lock = wrap(lock, ['channel', ('haveHalfop+', _('lock the topic'))]) def unlock(self, irc, msg, args, channel): """[] Unlocks the topic (sets the mode -t) in . is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) irc.queueMsg(ircmsgs.mode(channel, '-t')) irc.noReply() unlock = wrap(unlock, ['channel', ('haveHalfop+', _('unlock the topic'))]) def restore(self, irc, msg, args, channel): """[] Restores the topic to the last topic set by the bot. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) try: topics = self.lastTopics[channel] if not topics: raise KeyError except KeyError: irc.error(format(_('I haven\'t yet set the topic in %s.'), channel)) return self._sendTopics(irc, channel, topics) restore = wrap(restore, ['canChangeTopic']) def refresh(self, irc, msg, args, channel): """[] Refreshes current topic set by anyone. Restores topic if empty. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topic = irc.state.channels[channel].topic if topic: self._sendTopics(irc, channel, topic) return try: topics = self.lastTopics[channel] if not topics: raise KeyError except KeyError: irc.error(format(_('I haven\'t yet set the topic in %s.'), channel)) return self._sendTopics(irc, channel, topics) refresh = wrap(refresh, ['canChangeTopic']) def undo(self, irc, msg, args, channel): """[] Restores the topic to the one previous to the last topic command that set it. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) self._addRedo(channel, self._getUndo(channel)) # current topic. topics = self._getUndo(channel) # This is the topic list we want. if topics is not None: self._sendTopics(irc, channel, topics, isDo=True) else: irc.error(format(_('There are no more undos for %s.'), channel)) undo = wrap(undo, ['canChangetopic']) def redo(self, irc, msg, args, channel): """[] Undoes the last undo. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._getRedo(channel) if topics is not None: self._sendTopics(irc, channel, topics, isDo=True) else: irc.error(format(_('There are no redos for %s.'), channel)) redo = wrap(redo, ['canChangeTopic']) def swap(self, irc, msg, args, channel, first, second): """[] Swaps the order of the first topic number and the second topic number. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) if first == second: irc.error(_('I refuse to swap the same topic with itself.')) return t = topics[first] topics[first] = topics[second] topics[second] = t self._sendTopics(irc, channel, topics) swap = wrap(swap, ['canChangeTopic', 'topicNumber', 'topicNumber']) def save(self, irc, msg, args, channel): """[] Saves the topic in to be restored with 'topic default' later. is only necessary if the message isn't sent in the channel itself. """ self._checkManageCapabilities(irc, msg, channel) topic = irc.state.getTopic(channel) if topic: self.setRegistryValue('default', value=topic, channel=channel) else: self.setRegistryValue('default', value='', channel=channel) irc.replySuccess() save = wrap(save, ['channel', 'inChannel']) def default(self, irc, msg, args, channel): """[] Sets the topic in to the default topic for . The default topic for a channel may be configured via the configuration variable supybot.plugins.Topic.default. """ self._checkManageCapabilities(irc, msg, channel) topic = self.registryValue('default', channel) if topic: self._sendTopics(irc, channel, [topic]) else: irc.error(format(_('There is no default topic configured for %s.'), channel)) default = wrap(default, ['canChangeTopic']) def separator(self, irc, msg, args, channel, separator): """[] Sets the topic separator for to Converts the current topic appropriately. """ self._checkManageCapabilities(irc, msg, channel) topics = self._splitTopic(irc.state.getTopic(channel), channel) self.setRegistryValue('separator', separator, channel) self._sendTopics(irc, channel, topics) separator = wrap(separator, ['canChangeTopic', 'something']) Class = Topic # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Topic/config.py0000644000175000017500000001117413233426066017701 0ustar valval00000000000000### # 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('Topic') 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('Topic', True) class TopicFormat(registry.TemplatedString): "Value must include $topic, otherwise the actual topic would be left out." requiredTemplates = ['topic'] Topic = conf.registerPlugin('Topic') conf.registerChannelValue(Topic, 'separator', registry.StringSurroundedBySpaces('|', _("""Determines what separator is used between individually added topics in the channel topic."""))) conf.registerChannelValue(Topic, 'format', TopicFormat('$topic', _("""Determines what format is used to add topics in the topic. All the standard substitutes apply, in addition to "$topic" for the topic itself."""))) conf.registerChannelValue(Topic, 'recognizeTopiclen', registry.Boolean(True, _("""Determines whether the bot will recognize the TOPICLEN value sent to it by the server and thus refuse to send TOPICs longer than the TOPICLEN. These topics are likely to be truncated by the server anyway, so this defaults to True."""))) conf.registerChannelValue(Topic, 'default', registry.String('', _("""Determines what the default topic for the channel is. This is used by the default command to set this topic."""))) conf.registerChannelValue(Topic, 'setOnJoin', registry.Boolean(True, _("""Determines whether the bot will automatically set the topic on join if it is empty."""))) conf.registerChannelValue(Topic, 'alwaysSetOnJoin', registry.Boolean(False, _("""Determines whether the bot will set the topic every time it joins, or only if the topic is empty. Requires 'config plugins.topic.setOnJoin' to be set to True."""))) conf.registerGroup(Topic, 'undo') conf.registerChannelValue(Topic.undo, 'max', registry.NonNegativeInteger(10, _("""Determines the number of previous topics to keep around in case the undo command is called."""))) conf.registerChannelValue(Topic, 'requireManageCapability', registry.String('channel,op; channel,halfop', _("""Determines the capabilities required (if any) to make any topic changes, (everything except for read-only operations). Use 'channel,capab' for channel-level capabilities. Note that absence of an explicit anticapability means user has capability."""))) conf.registerChannelValue(Topic, 'allowSeparatorinTopics', registry.Boolean(True, _("""Determines whether the bot will allow topics containing the defined separator to be used. You may want to disable this if you are signing all topics by nick (see the 'format' option for ways to adjust this)."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Topic/__init__.py0000644000175000017500000000466613233426066020203 0ustar valval00000000000000### # 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. ### """ Provides commands for manipulating channel topics. """ 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__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = { supybot.authors.stepnem: ['persistence support'] } from . import config from . import plugin from imp import reload 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! if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Topic/locales/0000755000175000017500000000000013233426077017502 5ustar valval00000000000000limnoria-2018.01.25/plugins/Topic/locales/it.po0000644000175000017500000003630413233426066020462 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2014-07-05 00:09+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: config.py:45 #, docstring msgid "Value must include $topic, otherwise the actual topic would be left out." msgstr "Il valore deve includere $topic, altrimenti l'attuale topic non sarà modificato." #: config.py:50 msgid "" "Determines what separator is\n" " used between individually added topics in the channel topic." msgstr "Determina quale separatore utilizzare tra i diversi argomenti del topic del canale." #: config.py:53 msgid "" "Determines what format is used to add\n" " topics in the topic. All the standard substitutes apply, in addition to\n" " \"$topic\" for the topic itself." msgstr "" "Determina quale formato utilizzare per aggiungere argomenti al topic. In aggiunta a\n" " \"$topic\" per il topic stesso, sono applicati tutti gli standard di sostituzione." #: config.py:57 msgid "" "Determines whether the bot will recognize the\n" " TOPICLEN value sent to it by the server and thus refuse to send TOPICs\n" " longer than the TOPICLEN. These topics are likely to be truncated by the\n" " server anyway, so this defaults to True." msgstr "" "Determina se il bot riconoscerà il valore di TOPICLEN inviato dal server e rifiuterà\n" " di inviare topic di lunghezza maggiore. I topic troppo lunghi saranno comunque\n" " troncati dal server, per cui è impostato a True in modo predefinito." #: config.py:62 msgid "" "Determines what the default topic for the channel\n" " is. This is used by the default command to set this topic." msgstr "" "Determina quale sia il topic predefinito del canale. È utilizzato dal comando \"default\"." #: config.py:66 msgid "" "Determines the number of previous\n" " topics to keep around in case the undo command is called." msgstr "" "Determina il numero di topic precedenti da ricordare in caso si utilizzi il comando \"undo\"." #: config.py:69 msgid "" "Determines the\n" " capabilities required (if any) to make any topic changes,\n" " (everything except for read-only operations). Use 'channel,capab' for\n" " channel-level capabilities.\n" " Note that absence of an explicit anticapability means user has\n" " capability." msgstr "" "Determina le capacità richieste (eventuali) per le modifiche al topic (qualsiasi\n" " operazione tranne la lettura). Utilizzare \"canale,capacità\" per le capacità del singolo\n" " canale. L'assenza di un'esplicita anti-capacità significa che l'utente può usare i comandi." #: plugin.py:57 msgid "I'm not currently in %s." msgstr "Attualmente non sono in %s." #: plugin.py:61 msgid "I can't change the topic, I'm not(half)opped and %s is +t." msgstr "Non posso cambiare il topic, non sono halfop e %s ha il mode +t." #: plugin.py:68 msgid "The topic must not include %q." msgstr "Il topic non deve includere %q." #: plugin.py:79 msgid "topic number" msgstr "numero dell'argomento" #: plugin.py:92 msgid "There are no topics in %s." msgstr "Non ci sono argomenti in %s." #: plugin.py:200 msgid "That topic is too long for this server (maximum length: %i; this topic: %i)." msgstr "Il topic è troppo lungo per il server (lunghezza massima: %i; questo: %i)." #: plugin.py:213 #, docstring msgid "" "Check if the user has any of the required capabilities to manage\n" " the channel topic.\n" "\n" " The list of required capabilities is in requireManageCapability\n" " channel config.\n" "\n" " Also allow if the user is a chanop. Since they can change the topic\n" " manually anyway.\n" " " msgstr "" "Controlla che l'utente abbia una delle capacità richieste per gestire il topic del canale.\n" "\n" " L'elenco delle capacità è nella variabile di configurazione requireManageCapability.\n" "\n" " Autorizza anche un operatore del canale, dal momento che potrebbe comunque farlo manualmente.\n" " " #: plugin.py:265 #, docstring msgid "" "[]\n" "\n" " Returns the topic for . is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Restituisce il topic di . è necessario solo se il messaggio\n" " non viene inviato nel canale stesso.\n" " " #: plugin.py:276 #, docstring msgid "" "[] \n" "\n" " Adds to the topics for . is only necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Aggiunge al topic di . è necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:291 #, docstring msgid "" "[] \n" "\n" " Adds to the topics for . If the topic is too long\n" " for the server, topics will be popped until there is enough room.\n" " is only necessary if the message isn't sent in the channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Aggiunge al topic di . Se il topic è troppo lungo\n" " per il server, gli argomenti saranno tagliati per rientrare nel limite.\n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:308 #, docstring msgid "" "[] \n" "\n" " Replaces topic with .\n" " " msgstr "" "[] \n" "\n" " Sostituisce argomento con .\n" " " #: plugin.py:322 #, docstring msgid "" "[] \n" "\n" " Adds to the topics for at the beginning of the topics\n" " currently on . is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Aggiunge al topic di all'inizio dello stesso. \n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:338 #, docstring msgid "" "[]\n" "\n" " Shuffles the topics in . is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Mescola gli argomenti del topic di . è necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:348 msgid "I can't shuffle 1 or fewer topics." msgstr "Non posso mescolare uno o pochi argomenti." #: plugin.py:360 #, docstring msgid "" "[] [ ...]\n" "\n" " Reorders the topics from in the order of the specified\n" " arguments. is a one-based index into the topics.\n" " is only necessary if the message isn't sent in the channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Riordina gli argomenti del topic di nell'ordine specificato\n" " da , dove è un indice di argomenti. è\n" " necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:373 msgid "I cannot reorder 1 or fewer topics." msgstr "Non posso riordinare uno o pochi argomenti." #: plugin.py:375 msgid "All topic numbers must be specified." msgstr "Bisogna specificare tutti i numeri degli argomenti." #: plugin.py:377 msgid "Duplicate topic numbers cannot be specified." msgstr "Impossibile specificare numeri di argomenti doppi." #: plugin.py:385 #, docstring msgid "" "[]\n" "\n" " Returns a list of the topics in , prefixed by their indexes.\n" " Mostly useful for topic reordering. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta un elenco degli argomenti di , con il proprio indice\n" " come prefisso; essenzialmente utile per riordinare gli argomenti. \n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:394 msgid "%i: %s" msgstr "%i: %s" #: plugin.py:401 #, docstring msgid "" "[] \n" "\n" " Returns topic number from . is a one-based\n" " index into the topics. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Restituisce l'argomento di , dove è un indice di argomenti.\n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:416 #, docstring msgid "" "[] \n" "\n" " Changes the topic number on according to the regular\n" " expression . is the one-based index into the topics;\n" " is a regular expression of the form\n" " s/regexp/replacement/flags. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Modifica l'argomento di in base a . \n" " è un indice di argomenti e un'espressine regolare nella forma\n" " s/regexp/sostituzione/flag. è necessario solo se il messaggio\n" " non viene inviato nel canale stesso.\n" " " #: plugin.py:434 #, docstring msgid "" "[] [] \n" "\n" " Sets the topic to be . If no is given, this\n" " sets the entire topic. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] [] \n" "\n" " Definisce l'argomento . Se non è specificato imposta l'intero topic.\n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:455 #, docstring msgid "" "[] \n" "\n" " Removes topic from the topic for Topics are\n" " numbered starting from 1; you can also use negative indexes to refer\n" " to topics starting the from the end of the topic. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Rimuove l'argomento dal topic di ; la numerazione\n" ". degli argomenti parte da 1, è possibile definire un valore negativo\n" " per gli inidici iniziando a contare dal fondo. è necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:472 #, docstring msgid "" "[]\n" "\n" " Locks the topic (sets the mode +t) in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Blocca il topic (imposta il mode +t) in . è\n" " necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:482 msgid "lock the topic" msgstr "bloccare il topic" #: plugin.py:486 #, docstring msgid "" "[]\n" "\n" " Unlocks the topic (sets the mode -t) in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Sblocca il topic (imposta il mode -t) in . è\n" " necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:496 msgid "unlock the topic" msgstr "sbloccare il topic" #: plugin.py:500 #, docstring msgid "" "[]\n" "\n" " Restores the topic to the last topic set by the bot. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Ripristina il topic all'ultimo impostato dal bot. è\n" " necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:511 msgid "I haven't yet set the topic in %s." msgstr "Non ho ancora impostato il topic in %s." #: plugin.py:519 #, docstring msgid "" "[]\n" "\n" " Restores the topic to the one previous to the last topic command that\n" " set it. is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" " Ripristina il topic a quello precedente all'ultimo impostato dal comando \"topic\".\n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:533 msgid "There are no more undos for %s." msgstr "Non ci sono più annullamenti per %s." #: plugin.py:538 #, docstring msgid "" "[]\n" "\n" " Undoes the last undo. is only necessary if the message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Annulla l'ultimo ripristino. è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:550 msgid "There are no redos for %s." msgstr "Non ci sono ripristini per %s." #: plugin.py:555 #, docstring msgid "" "[] \n" "\n" " Swaps the order of the first topic number and the second topic number.\n" " is only necessary if the message isn't sent in the channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Scambia l'ordine del numero del primo argomento con il secondo. \n" " è necessario solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:566 msgid "I refuse to swap the same topic with itself." msgstr "Mi rifiuto di scambiare l'argomento con lo stesso." #: plugin.py:576 #, docstring msgid "" "[]\n" "\n" " Sets the topic in to the default topic for . The\n" " default topic for a channel may be configured via the configuration\n" " variable supybot.plugins.Topic.default.\n" " " msgstr "" "[]\n" "\n" " Imposta il topic in a quello predefinito. Il topic predefinito\n" " per un canale può essere configurato tramite la variabile di configurazione\n" " supybot.plugins.Topic.default.\n" " " #: plugin.py:589 msgid "There is no default topic configured for %s." msgstr "Non c'è un topic predefinito configurato per %s." #: plugin.py:595 #, docstring msgid "" "[] \n" "\n" " Sets the topic separator for to Converts the\n" " current topic appropriately.\n" " " msgstr "" "[] \n" "\n" " Imposta il separatore per gli argomenti del topic di .\n" " Converte l'attuale topic di conseguenza.\n" " " limnoria-2018.01.25/plugins/Topic/locales/fr.po0000644000175000017500000003642113233426066020455 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2014-01-21 22:32+CET\n" "PO-Revision-Date: 2014-07-05 00:09+0200\n" "Last-Translator: \n" "Language-Team: Limnoria \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" #: config.py:45 msgid "" "Value must include $topic, otherwise the actual topic would be left out." msgstr "" "La valeur doit inclure $topic, sinon, le topic actuel serait laissé tel quel." #: config.py:50 msgid "" "Determines what separator is\n" " used between individually added topics in the channel topic." msgstr "" "Détermine quel séparateur est utilisé entre les différents topics dans le " "topic du canal." #: config.py:53 msgid "" "Determines what format is used to add\n" " topics in the topic. All the standard substitutes apply, in addition " "to\n" " \"$topic\" for the topic itself." msgstr "" "Détermine quel format est utilisé pour ajouter des topics dans le topic. " "Tous les substituts standard s'appliquent, en plus de \"$topic\" pour le " "topic lui-même." #: config.py:57 msgid "" "Determines whether the bot will recognize the\n" " TOPICLEN value sent to it by the server and thus refuse to send TOPICs\n" " longer than the TOPICLEN. These topics are likely to be truncated by " "the\n" " server anyway, so this defaults to True." msgstr "" "Détermine si le bot reconnaitra la valeur TOPICLEN envoyée par le serveur et " "refusera d'envoyer des topics plus longs que TOPICLEN. De toutes manière, " "les topics trop longs seront tronqués par le serveur, don cç vaut par défaut " "True." #: config.py:62 msgid "" "Determines what the default topic for the channel\n" " is. This is used by the default command to set this topic." msgstr "" "Détermine quel est le topic par défaut du canal. C'est utilisé par la " "commande 'default'." #: config.py:65 msgid "" "Determines whether the bot will set the topic\n" " every time it joins, or only if the topic is empty." msgstr "" "Détermine si le bot définira le topic à chaque fois qu’il rejoint le salon, " "ou seulement si le topic est vide." #: config.py:69 msgid "" "Determines the number of previous\n" " topics to keep around in case the undo command is called." msgstr "" "Détermine le nombre de topics précédents à garder pour le cas où la commande " "'undo' est appelée." #: config.py:72 msgid "" "Determines the\n" " capabilities required (if any) to make any topic changes,\n" " (everything except for read-only operations). Use 'channel,capab' for\n" " channel-level capabilities.\n" " Note that absence of an explicit anticapability means user has\n" " capability." msgstr "" "Détermine les capacités requises (s'il y en a) pour changer le topic (c'est " "à dire tout ce qui n'est pas une opération en lecture seule). Utilisez " "#canal,capacité pour les capacités de canaux. Notez qu'en l'absence d'une " "anticapacité explicite, l'utilisateur a cette capacité." #: plugin.py:57 msgid "I'm not currently in %s." msgstr "Je ne suis pas actuellement sur %s." #: plugin.py:61 msgid "I can't change the topic, I'm not (half)opped and %s is +t." msgstr "Je ne peux changer le topic, je ne suis pas halfopé, et %s a le mode +t." #: plugin.py:68 msgid "The topic must not include %q." msgstr "Le topic ne doit pas inclure %q." #: plugin.py:79 msgid "topic number" msgstr "numéro de topic" #: plugin.py:92 msgid "There are no topics in %s." msgstr "Il n'y a pas de topic sur %s." #: plugin.py:200 msgid "" "That topic is too long for this server (maximum length: %i; this topic: %i)." msgstr "" "Ce topic est trop long pour ce serveur (longueur maximum : %i ; ce topic : " "%i)." #: plugin.py:219 msgid "" "Check if the user has any of the required capabilities to manage\n" " the channel topic.\n" "\n" " The list of required capabilities is in requireManageCapability\n" " channel config.\n" "\n" " Also allow if the user is a chanop. Since they can change the topic\n" " manually anyway.\n" " " msgstr "." #: plugin.py:276 msgid "" "[]\n" "\n" " Returns the topic for . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne le topic du . n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:287 msgid "" "[] \n" "\n" " Adds to the topics for . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Ajoute le aux topics du . n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:302 msgid "" "[] \n" "\n" " Adds to the topics for . If the topic is too long\n" " for the server, topics will be popped until there is enough room.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Ajoute le topic aux topics du . Si le topic est trop long " "pour le serveur, les topics les plus vieux seront supprimés jusqu'à ce qu'il " "y ai assez de place. n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:319 msgid "" "[] \n" "\n" " Replaces topic with .\n" " " msgstr "" "[] \n" "\n" "Remplace le topic par le ." #: plugin.py:333 msgid "" "[] \n" "\n" " Adds to the topics for at the beginning of the " "topics\n" " currently on . is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Ajoute le aux topics du , au début des topics actuellement " "sur le . n'est nécessaire que si le message n'est pas envoyé " "sur le canal lui-même." #: plugin.py:349 msgid "" "[]\n" "\n" " Shuffles the topics in . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Mélance les topics sur le . n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:359 msgid "I can't shuffle 1 or fewer topics." msgstr "Je ne peux mélanger les topics que si il y en a au moins deux." #: plugin.py:371 msgid "" "[] [ ...]\n" "\n" " Reorders the topics from in the order of the specified\n" " arguments. is a one-based index into the topics.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Remet les topics du dans l'ordre spécifié par les arguments " ". est un index dans les topics. n'est nécessaire " "que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:384 msgid "I cannot reorder 1 or fewer topics." msgstr "Je ne peux réordonner les topics s'il y en a moins de deux." #: plugin.py:386 msgid "All topic numbers must be specified." msgstr "Tous les nombres de topics doivent être spécifiés." #: plugin.py:388 msgid "Duplicate topic numbers cannot be specified." msgstr "Les numéros de topics ne peuvent être en double." #: plugin.py:396 msgid "" "[]\n" "\n" " Returns a list of the topics in , prefixed by their " "indexes.\n" " Mostly useful for topic reordering. is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne la liste des topics sur le , préfixés par leur index. " "Généralement utile pour réordonner les topics. n'est nécessaire que " "si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:405 msgid "%i: %s" msgstr "%i : %s" #: plugin.py:412 msgid "" "[] \n" "\n" " Returns topic number from . is a one-" "based\n" " index into the topics. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Retourne le topic numéro du canal. est un index dans les " "topics. n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:424 msgid "" "[] \n" "\n" " Changes the topic number on according to the " "regular\n" " expression . is the one-based index into the " "topics;\n" " is a regular expression of the form\n" " s/regexp/replacement/flags. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Change le topic sur le , en accord avec l'expression " "régulière . est un index parmis les topics ; est " "une expression régulière de la forme s/regexp/replacement/flags. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:442 msgid "" "[] [] \n" "\n" " Sets the topic to be . If no is given, " "this\n" " sets the entire topic. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] [] \n" "\n" "Définit le topic . Si le n'est pas donné, il s'agit du " "topic entier. n'est nécessaire que si le message n'est pas envoyé " "sur le canal lui-même." #: plugin.py:463 msgid "" "[] \n" "\n" " Removes topic from the topic for Topics are\n" " numbered starting from 1; you can also use negative indexes to " "refer\n" " to topics starting the from the end of the topic. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Supprime le topic des topics du . Les topics sont numérotés " "à partir de 1 ; vous pouvez également utiliser des indexs négatifs pour " "compter à partir de la fin. n'est nécessaire que si le message n'est " "pas envoyé sur le canal lui-même." #: plugin.py:480 msgid "" "[]\n" "\n" " Locks the topic (sets the mode +t) in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Verrouille le topic (défini le mode +t) sur le . n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:490 msgid "lock the topic" msgstr "verrouiller le topic" #: plugin.py:494 msgid "" "[]\n" "\n" " Unlocks the topic (sets the mode -t) in . is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Déverrouille le topic (défini le mode -t) sur le . n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:504 msgid "unlock the topic" msgstr "déverrouiller le topic" #: plugin.py:508 msgid "" "[]\n" "\n" " Restores the topic to the last topic set by the bot. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Restaure le topic tel qu'il était la dernière fois que le bot l'a défini. " " n'est nécessaire que si le message n'est pas envoyé sur le canal lui-" "même." #: plugin.py:519 msgid "I haven't yet set the topic in %s." msgstr "Je n'ai encore jamais définit le topic sur %s." #: plugin.py:527 msgid "" "[]\n" "\n" " Restores the topic to the one previous to the last topic command " "that\n" " set it. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" "Restaure le topic à l'état dans lequel il était avant la dernière " "utilisation de la commande 'topic. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:541 msgid "There are no more undos for %s." msgstr "Il n'y a plus rien à défaire sur %s" #: plugin.py:546 msgid "" "[]\n" "\n" " Undoes the last undo. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Défait le dernier 'undo'. n'est nécessaire que si le message n'est " "pas envoyé sur le canal lui-même." #: plugin.py:558 msgid "There are no redos for %s." msgstr "Il n'y a plus rien à refaire sur %s" #: plugin.py:563 msgid "" "[] \n" "\n" " Swaps the order of the first topic number and the second topic " "number.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Inverse les deux topics " "donnés. n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:574 msgid "I refuse to swap the same topic with itself." msgstr "Je refuse d'échanger un topic avec lui-même." #: plugin.py:584 msgid "" "[]\n" "\n" " Saves the topic in to be restored with 'topic default'\n" " later. is only necessary if the message isn't sent in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" "Enregistre le topic du pour être restauré avec 'topic default' par la " "suite. n'est nécessaire que si vous n'exécutez pas la commande dans " "le canal lui-même." #: plugin.py:603 msgid "" "[]\n" "\n" " Sets the topic in to the default topic for . " "The\n" " default topic for a channel may be configured via the configuration\n" " variable supybot.plugins.Topic.default.\n" " " msgstr "" "[]\n" "\n" "Définit le topic du pour correspondre à celui par défaut défini pour " "le . Le topic par défaut pour un canal peut être configuré via la " "variable supybot.plugins.Topic.default. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:616 msgid "There is no default topic configured for %s." msgstr "Il n'y a pas de topic par défaut configuré pour %s." #: plugin.py:622 msgid "" "[] \n" "\n" " Sets the topic separator for to Converts the\n" " current topic appropriately.\n" " " msgstr "" "[] \n" "\n" "Définir le séparateur de topic du pour être . Convertit " "le topic actuel de manière appropriée. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." limnoria-2018.01.25/plugins/Topic/locales/fi.po0000644000175000017500000004342213233426066020443 0ustar valval00000000000000# Topic plugin in Limnoria. # Copyright (C) 2011,2014 # Mikaela Suomalainen , 2012. # msgid "" msgstr "" "Project-Id-Version: Topic plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 13:29+EET\n" "PO-Revision-Date: 2014-12-20 13:45+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: Finnish <>\n" "Language: 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" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: config.py:45 msgid "" "Value must include $topic, otherwise the actual topic would be left out." msgstr "Asetusarvon täytyy sisältää $topic tai muuten itse aihe jätetään pois." #: config.py:50 msgid "" "Determines what separator is\n" " used between individually added topics in the channel topic." msgstr "" "Määrittää mitä erotinta käytetään\n" " erikseen lisättyjen aiheiden välissä kanavan aiheessa." #: config.py:53 msgid "" "Determines what format is used to add\n" " topics in the topic. All the standard substitutes apply, in addition " "to\n" " \"$topic\" for the topic itself." msgstr "" "Määrittää mitä muotoa käytetään, kun lisätään aiheita aiheeseen. \n" " Kaikki peru muunnokset ovat voimassa, \n" " \"$topic\" tarkoittaa aihetta itseään." #: config.py:57 msgid "" "Determines whether the bot will recognize the\n" " TOPICLEN value sent to it by the server and thus refuse to send TOPICs\n" " longer than the TOPICLEN. These topics are likely to be truncated by " "the\n" " server anyway, so this defaults to True." msgstr "" "Määrittää tunnistaako botti TOPICLEN-arvon, jonka palvelin on lähettänyt " "sille\n" " ja näin kieltäytyy asettamasta pidempiä aiheita, kuin TOPICLEN. \n" " Kyseiset aiheet tulisivat muutenkin palvelimen lyhentämiksi, joten\n" " tämän asetusarvo on oletuksena True." #: config.py:62 msgid "" "Determines what the default topic for the channel\n" " is. This is used by the default command to set this topic." msgstr "" "Määrittää mikä on oletus aihe kanavalle. \n" " 'Default' komento käyttää tätä asettaakseen aiheen." #: config.py:65 #, fuzzy msgid "" "Determines whether the bot will automatically\n" " set the topic on join if it is empty." msgstr "" "Määrittää asettaako botti aiheen aina liittyessään vai vain, kun aihe on " "tyhjä." #: config.py:68 #, fuzzy msgid "" "Determines whether the bot will set the topic\n" " every time it joins, or only if the topic is empty. Requires 'config\n" " plugins.topic.setOnJoin' to be set to True." msgstr "" "Määrittää asettaako botti aiheen aina liittyessään vai vain, kun aihe on " "tyhjä." #: config.py:73 msgid "" "Determines the number of previous\n" " topics to keep around in case the undo command is called." msgstr "" "Määrittää edellisten aiheiden määrän, jotka säilytetään siltä varalta, että\n" " 'undo' komentoa käytetään." #: config.py:76 msgid "" "Determines the\n" " capabilities required (if any) to make any topic changes,\n" " (everything except for read-only operations). Use 'channel,capab' for\n" " channel-level capabilities.\n" " Note that absence of an explicit anticapability means user has\n" " capability." msgstr "" "Määrittää\n" " valtuudet, jotka (jos mitkään) vaaditaan aiheen vaihtamiseen,\n" " (=kaikki, paitsi vain-luku komennot). Käytä valtuutta 'channel," "valtuus' \n" " kanava-tason valtuuksia varten.\n" " Huomaa, että anti-valtuuden poissaolo tarkoittaa, että käyttäjällä on\n" " valtuus." #: plugin.py:57 msgid "I'm not currently in %s." msgstr "En juuri nyt ole kanavalla %s." #: plugin.py:61 msgid "I can't change the topic, I'm not (half)opped and %s is +t." msgstr "" "En voi vaihtaa aihetta, koska en ole kyseisellä kanavalla puolioperaattori " "ja kanavalla %s on tila +t." #: plugin.py:68 msgid "The topic must not include %q." msgstr "%q ei saa olla aiheessa." #: plugin.py:79 msgid "topic number" msgstr "aiheen numero" #: plugin.py:92 msgid "There are no topics in %s." msgstr "Kanavalla %s ei ole aiheita." #: plugin.py:114 msgid "" "This plugin allows you to use many topic-related functions,\n" " such as Add, Undo, and Remove." msgstr "" "Tämä plugini sallii monien aiheeseen liittyvien functioiden käytön, kuten " "lisäämisen (add),\n" " kumoamisen (undo) ja poistamisen (remove)." #: plugin.py:202 msgid "" "That topic is too long for this server (maximum length: %i; this topic: %i)." msgstr "" "Määrittämäsi aihe on liian pitkä tälle palvelimelle (maksimi pituus on %i; " "tämän aiheen pituus on %i)." #: plugin.py:221 msgid "" "Check if the user has any of the required capabilities to manage\n" " the channel topic.\n" "\n" " The list of required capabilities is in requireManageCapability\n" " channel config.\n" "\n" " Also allow if the user is a chanop. Since they can change the topic\n" " manually anyway.\n" " " msgstr "" "Tarkista onko käyttäjällä valtuudet, jotka on vaadittu\n" " kanavan aiheen hallintaan.\n" "\n" " Lista vaadituista valtuuksista on kanava-asetusarvossa " "requireManageCapability\n" "\n" " Salli myös jos käyttäjä on kanavaoperaattori, koska hän voisi " "vaihtaa aiheen\n" " muutenkin manuaalisesti.\n" " " #: plugin.py:278 msgid "" "[]\n" "\n" " Returns the topic for . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa aiheen. on vaadittu vain ellei viestiä " "lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:289 msgid "" "[] \n" "\n" " Adds to the topics for . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Lisää aiheisiin. on vaadittu vain, " "ellei\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:304 msgid "" "[] \n" "\n" " Adds to the topics for . If the topic is too long\n" " for the server, topics will be popped until there is enough room.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Lisää aiheisiin. Jos aihe on liian pitkä\n" " palvelimelle, aihetta kutistetaan kunnes sille on tarpeeksi tilaa.\n" " on vaadittu vain, ellei viestiä lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:321 msgid "" "[] \n" "\n" " Replaces topic with .\n" " " msgstr "" "[] \n" "\n" " Korvaa aiheen .\n" " " #: plugin.py:335 msgid "" "[] \n" "\n" " Adds to the topics for at the beginning of the " "topics\n" " currently on . is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Lisää aiheisiin \n" " silloisten aiheiden alkuun. on vaadittu vain, " "jos viestiä ei lähetetä \n" " kanavalla itsellään.\n" " " #: plugin.py:351 msgid "" "[]\n" "\n" " Shuffles the topics in . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Sekoittaa aiheet . on vaadittu vain, jos " "viestiä ei lähetetä \n" " kanavalla itsellään.\n" " " #: plugin.py:361 msgid "I can't shuffle 1 or fewer topics." msgstr "Yhtä tai vähempää aihetta ei voida sekoittaa." #: plugin.py:373 msgid "" "[] [ ...]\n" "\n" " Reorders the topics from in the order of the specified\n" " arguments. is a one-based index into the topics.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Järjestää aiheet järjestyksessä, joka on määritetty\n" " parametreillä. on yksi-indexinen aiheisiin.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla \n" " itsellään.\n" " " #: plugin.py:386 msgid "I cannot reorder 1 or fewer topics." msgstr "En voi uudelleen järjestää yhtä tai vähempää aihetta." #: plugin.py:388 msgid "All topic numbers must be specified." msgstr "Kaikki numerot täytyy määrittää." #: plugin.py:390 msgid "Duplicate topic numbers cannot be specified." msgstr "Aiheen numeroiden kaksoiskappaleita ei voida määrittää." #: plugin.py:398 msgid "" "[]\n" "\n" " Returns a list of the topics in , prefixed by their " "indexes.\n" " Mostly useful for topic reordering. is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa listan aiheista , etuliitettyinä " "indekseihinsä.\n" " Enimmäkseen hyödyllinen aiheen uudelleenjärjestämisessä. " "on vaadittu vain jos viestiä ei lähetetä \n" " kanavalla itsellään.\n" " " #: plugin.py:407 msgid "%i: %s" msgstr "%i: %s" #: plugin.py:414 #, fuzzy msgid "" "[] \n" "\n" " Returns topic number from . is a one-" "based\n" " index into the topics. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa aiheen . on yksi-indexinen\n" " aiheissa. on vaadittu vain jos viestiä ei lähetetä " "kanavalla\n" " itsellään.\n" " " #: plugin.py:426 #, fuzzy msgid "" "[] \n" "\n" " Changes the topic number on according to the " "regular\n" " expression . is the one-based index into the " "topics;\n" " is a regular expression of the form\n" " s/regexp/replacement/flags. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Vaihtaa aiheen " "mukaan.\n" " on yksi-indexinen aiheissa;\n" " on säännöllinen lauseke muodossa\n" " s/säännöllinen lauseke/korvaus/liput. on vaadittu vain jos " "viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:444 msgid "" "[] [] \n" "\n" " Sets the topic to be . If no is given, " "this\n" " sets the entire topic. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] [] \n" "\n" " Asettaa aiheen . Jos ei ole annettu, " "tämä\n" " asettaa koko aiheen. on vaadittu vain, ellei viestiä " "lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:465 msgid "" "[] \n" "\n" " Removes topic from the topic for Topics are\n" " numbered starting from 1; you can also use negative indexes to " "refer\n" " to topics starting the from the end of the topic. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Poistaa aiheen aiheesta. Aiheet on numeroitu\n" " alkaen numerosta 1; voit käyttää negatiivisia lukuja saadaksesi ne " "viittaamaan\n" " aiheisiin, jotka alkavat lopusta. on vaadittu\n" " vain, jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:482 msgid "" "[]\n" "\n" " Locks the topic (sets the mode +t) in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Lukitsee aiheen (asettaa tilan +t) . on " "vaadittu vain, ellei\n" " viestiä lähetetä kanavalla itsellään.\n" " " #: plugin.py:492 msgid "lock the topic" msgstr "lukitse aihe" #: plugin.py:496 msgid "" "[]\n" "\n" " Unlocks the topic (sets the mode -t) in . is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Avaa aiheen (asettaa tilan -t) . on vaadittu " "vain, \n" " ellei viestiä lähetetä kanavalla itsellään.\n" " " #: plugin.py:506 msgid "unlock the topic" msgstr "avaa aiheen" #: plugin.py:510 msgid "" "[]\n" "\n" " Restores the topic to the last topic set by the bot. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa aiheen viimeiseksi aiheeksi, jonka botti on asettanut. " " on vaadittu\n" " vain, ellei viestiä lähetetä kanavalla itsellään.\n" " " #: plugin.py:523 plugin.py:548 msgid "I haven't yet set the topic in %s." msgstr "En ole vielä asettanut aihetta kanavalla %s." #: plugin.py:531 #, fuzzy msgid "" "[]\n" " Refreshes current topic set by anyone. Restores topic if empty.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa aiheen viimeiseksi aiheeksi, jonka botti on asettanut. " " on vaadittu\n" " vain, ellei viestiä lähetetä kanavalla itsellään.\n" " " #: plugin.py:556 #, fuzzy msgid "" "[]\n" "\n" " Restores the topic to the one previous to the last topic command " "that\n" " set it. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa aiheen yhdeksi edellisistä aiheista, jotka \"last\" " "komento on asettanut \n" " siksi. on vaadittu vain, ellei viestiä lähetetä " "kanavalla\n" " itsellään.\n" " " #: plugin.py:570 msgid "There are no more undos for %s." msgstr "Kanavalle %s ei ole enempää kumouksia." #: plugin.py:575 msgid "" "[]\n" "\n" " Undoes the last undo. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Kumoaa viimeisen kumouksen. on vaadittu vain jos viestiä " "ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:587 #, fuzzy msgid "There are no redos for %s." msgstr "Kanavalle %s ei ole enempää uudelleentekoja." #: plugin.py:592 msgid "" "[] \n" "\n" " Swaps the order of the first topic number and the second topic " "number.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Vaihtaa ensimmäisen ja toisen aiheen paikat.\n" " on vaadittu vain, ellei viestiä lähetetä kanavalla itsellään." #: plugin.py:603 msgid "I refuse to swap the same topic with itself." msgstr "Kieltäydyn vaihtamasta aiheen paikkaa sen itsensä kanssa." #: plugin.py:613 msgid "" "[]\n" "\n" " Saves the topic in to be restored with 'topic default'\n" " later. is only necessary if the message isn't sent in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" " Tallentaa aiheen , jotta se voidaan palauttaa 'topic default'—" "komennolla\n" " myöhemmin. on vaadittu vain, ellei viestiä lähetetä kanavalla " "itsellään." #: plugin.py:632 msgid "" "[]\n" "\n" " Sets the topic in to the default topic for . " "The\n" " default topic for a channel may be configured via the configuration\n" " variable supybot.plugins.Topic.default.\n" " " msgstr "" "[]\n" "\n" " Asettaa aiheen oletusaiheeksi. Kanavan oletusaihe " "voidaan\n" " määrittää asetuksen supybot.plugins.Topic.default arvolla.\n" " " #: plugin.py:645 msgid "There is no default topic configured for %s." msgstr "Kanavalle %s ei ole määritetty oletusaihetta." #: plugin.py:651 msgid "" "[] \n" "\n" " Sets the topic separator for to Converts the\n" " current topic appropriately.\n" " " msgstr "" "[] \n" "\n" " Asettaa erottimen , muuntaa\n" " nykyisen aiheen sen mukaisesti.\n" " " #~ msgid "" #~ "[] \n" #~ "\n" #~ " Vaihtaa ensinmäisen ja toisen aiheen numeron paikkoja.\n" #~ " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" #~ " itsellään.\n" #~ " " #~ msgstr "" #~ "[] \n" #~ "\n" #~ " Vaihtaa ensinmäisen ja toisen aiheen numeron paikkoja.\n" #~ " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" #~ " itsellään.\n" #~ " " limnoria-2018.01.25/plugins/Todo/0000755000175000017500000000000013233426077015707 5ustar valval00000000000000limnoria-2018.01.25/plugins/Todo/test.py0000644000175000017500000001471713233426066017250 0ustar valval00000000000000### # Copyright (c) 2003-2005, Daniel DiPaolo # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from supybot.test import * class TodoTestCase(PluginTestCase): plugins = ('Todo', 'User', 'Config') _user1 = 'foo!bar@baz' _user2 = 'bar!foo@baz' def setUp(self): PluginTestCase.setUp(self) # Create a valid user to use self.prefix = self._user2 self.assertNotError('register testy oom') self.prefix = self._user1 self.assertNotError('register tester moo') def testTodo(self): # Should not error, but no tasks yet. self.assertNotError('todo') self.assertRegexp('todo', 'You have no tasks') # Add a task self.assertNotError('todo add wash my car') self.assertRegexp('todo', '#1: wash my car') # Check that task self.assertRegexp('todo 1', 'Todo for tester: wash my car \(Added .*?\)') # Check that it lists all my tasks when given my name self.assertResponse('todo tester', 'Todo for tester: #1: wash my car') # Check pluralization self.assertNotError('todo add moo') self.assertRegexp('todo tester', 'Todos for tester: #1: wash my car and #2: moo') # Check error self.assertError('todo asfas') self.assertRegexp('todo asfas', 'Error: \'asfas\' is not a valid task') # Check priority sorting self.assertNotError('todo setpriority 1 100') self.assertNotError('todo setpriority 2 10') self.assertRegexp('todo', '#2: moo and #1: wash my car') # Check permissions self.prefix = self._user2 self.assertError('todo tester') self.assertNotRegexp('todo tester', 'task id') self.prefix = self._user1 self.assertNotError('todo tester') self.assertNotError('config plugins.Todo.allowThirdpartyReader True') self.prefix = self._user2 self.assertNotError('todo tester') self.prefix = self._user1 self.assertNotError('todo tester') def testAddtodo(self): self.assertNotError('todo add code a new plugin') self.assertNotError('todo add --priority=1000 fix all bugs') def testRemovetodo(self): self.nick = 'testy' self.prefix = self._user2 self.assertNotError('todo add do something') self.assertNotError('todo add do something else') self.assertNotError('todo add do something again') self.assertNotError('todo remove 1') self.assertNotError('todo 1') self.nick = 'tester' self.prefix = self._user1 self.assertNotError('todo add make something') self.assertNotError('todo add make something else') self.assertNotError('todo add make something again') self.assertNotError('todo remove 1 3') self.assertRegexp('todo 1', r'Inactive') self.assertRegexp('todo 3', r'Inactive') self.assertNotError('todo') def testSearchtodo(self): self.assertNotError('todo add task number one') self.assertRegexp('todo search task*', '#1: task number one') self.assertRegexp('todo search number', '#1: task number one') self.assertNotError('todo add task number two is much longer than' ' task number one') self.assertRegexp('todo search task*', '#1: task number one and #2: task number two is ' 'much longer than task number...') self.assertError('todo search --regexp s/bustedregex') self.assertRegexp('todo search --regexp m/task/', '#1: task number one and #2: task number two is ' 'much longer than task number...') def testSetPriority(self): self.assertNotError('todo add --priority=1 moo') self.assertRegexp('todo 1', 'moo, priority: 1 \(Added at .*?\)') self.assertNotError('setpriority 1 50') self.assertRegexp('todo 1', 'moo, priority: 50 \(Added at .*?\)') self.assertNotError('setpriority 1 0') self.assertRegexp('todo 1', 'moo \(Added at .*?\)') def testChangeTodo(self): self.assertNotError('todo add moo') self.assertError('todo change 1 asdfas') self.assertError('todo change 1 m/asdfaf//') self.assertNotError('todo change 1 s/moo/foo/') self.assertRegexp('todo 1', 'Todo for tester: foo \(Added .*?\)') def testActiveInactiveTodo(self): self.assertNotError('todo add foo') self.assertNotError('todo add bar') self.assertRegexp('todo 1', 'Active') self.assertRegexp('todo 2', 'Active') self.assertNotError('todo remove 1') self.assertRegexp('todo 1', 'Inactive') self.assertRegexp('todo 2', 'Active') self.assertNotError('todo remove 2') self.assertRegexp('todo 2', 'Inactive') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Todo/plugin.py0000644000175000017500000002447013233426066017564 0ustar valval00000000000000### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010, James McCoy # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import re import time import operator import supybot.dbi as dbi import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot import commands from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Todo') class TodoRecord(dbi.Record): __fields__ = [ ('priority', int), 'at', 'task', 'active', ] dataDir = conf.supybot.directories.data class FlatTodoDb(object): def __init__(self): self.directory = dataDir.dirize('Todo') if not os.path.exists(self.directory): os.mkdir(self.directory) self.dbs = {} def _getDb(self, uid): dbfile = os.path.join(self.directory, str(uid)) if uid not in self.dbs: self.dbs[uid] = dbi.DB(dbfile, Record=TodoRecord) return self.dbs[uid] def close(self): for db in self.dbs.values(): db.close() def get(self, uid, tid): db = self._getDb(uid) return db.get(tid) def getTodos(self, uid): db = self._getDb(uid) L = [R for R in db.select(lambda r: r.active)] if not L: raise dbi.NoRecordError return L def add(self, priority, now, uid, task): db = self._getDb(uid) return db.add(TodoRecord(priority=priority, at=now, task=task, active=True)) def remove(self, uid, tid): db = self._getDb(uid) t = db.get(tid) t.active = False db.set(tid, t) def select(self, uid, criteria): db = self._getDb(uid) def match(todo): for p in criteria: if not p(todo.task): return False return True todos = db.select(lambda t: match(t)) if not todos: raise dbi.NoRecordError return todos def setpriority(self, uid, tid, priority): db = self._getDb(uid) t = db.get(tid) t.priority = priority db.set(tid, t) def change(self, uid, tid, replacer): db = self._getDb(uid) t = db.get(tid) t.task = replacer(t.task) db.set(tid, t) class Todo(callbacks.Plugin): """This plugin allows you to create your own personal to-do list on the bot.""" def __init__(self, irc): self.__parent = super(Todo, self) self.__parent.__init__(irc) self.db = FlatTodoDb() def die(self): self.__parent.die() self.db.close() def _shrink(self, s): return utils.str.ellipsisify(s, 50) @internationalizeDocstring def todo(self, irc, msg, args, user, taskid): """[] [] Retrieves a task for the given task id. If no task id is given, it will return a list of task ids that that user has added to their todo list. """ try: u = ircdb.users.getUser(msg.prefix) except KeyError: u = None if u != user and not self.registryValue('allowThirdpartyReader'): irc.error(_('You are not allowed to see other users todo-list.')) return # List the active tasks for the given user if not taskid: try: tasks = self.db.getTodos(user.id) utils.sortBy(operator.attrgetter('priority'), tasks) tasks = [format(_('#%i: %s'), t.id, self._shrink(t.task)) for t in tasks] Todo = 'Todo' if len(tasks) != 1: Todo = 'Todos' irc.reply(format(_('%s for %s: %L'), Todo, user.name, tasks)) except dbi.NoRecordError: if u != user: irc.reply(_('That user has no tasks in their todo list.')) else: irc.reply(_('You have no tasks in your todo list.')) return # Reply with the user's task else: try: t = self.db.get(user.id, taskid) if t.active: active = _('Active') else: active = _('Inactive') if t.priority: t.task += format(_(', priority: %i'), t.priority) at = time.strftime(conf.supybot.reply.format.time(), time.localtime(t.at)) s = format(_('%s todo for %s: %s (Added at %s)'), active, user.name, t.task, at) irc.reply(s) except dbi.NoRecordError: irc.errorInvalid(_('task id'), taskid) todo = wrap(todo, [first('otherUser', 'user'), additional(('id', 'task'))]) @internationalizeDocstring def add(self, irc, msg, args, user, optlist, text, now): """[--priority=] Adds as a task in your own personal todo list. The optional priority argument allows you to set a task as a high or low priority. Any integer is valid. """ priority = 0 for (option, arg) in optlist: if option == 'priority': priority = arg todoId = self.db.add(priority, now, user.id, text) irc.replySuccess(format(_('(Todo #%i added)'), todoId)) add = wrap(add, ['user', getopts({'priority': ('int', 'priority')}), 'text', 'now']) @internationalizeDocstring def remove(self, irc, msg, args, user, tasks): """ [ ...] Removes from your personal todo list. """ invalid = [] for taskid in tasks: try: self.db.get(user.id, taskid) except dbi.NoRecordError: invalid.append(taskid) if invalid and len(invalid) == 1: irc.error(format(_('Task %i could not be removed either because ' 'that id doesn\'t exist or it has been removed ' 'already.'), invalid[0])) elif invalid: irc.error(format(_('No tasks were removed because the following ' 'tasks could not be removed: %L.'), invalid)) else: for taskid in tasks: self.db.remove(user.id, taskid) irc.replySuccess() remove = wrap(remove, ['user', many(('id', 'task'))]) @internationalizeDocstring def search(self, irc, msg, args, user, optlist, globs): """[--{regexp} ] [ ...] Searches your todos for tasks matching . If --regexp is given, its associated value is taken as a regexp and matched against the tasks. """ if not optlist and not globs: raise callbacks.ArgumentError criteria = [] for (option, arg) in optlist: if option == 'regexp': criteria.append(lambda s: regexp_wrapper(s, reobj=arg, timeout=0.1, plugin_name=self.name(), fcn_name='search')) for glob in globs: glob = utils.python.glob2re(glob) criteria.append(re.compile(glob).search) try: tasks = self.db.select(user.id, criteria) L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks] irc.reply(format('%L', L)) except dbi.NoRecordError: irc.reply(_('No tasks matched that query.')) search = wrap(search, ['user', getopts({'regexp': 'regexpMatcher'}), any('glob')]) @internationalizeDocstring def setpriority(self, irc, msg, args, user, id, priority): """ Sets the priority of the todo with the given id to the specified value. """ try: self.db.setpriority(user.id, id, priority) irc.replySuccess() except dbi.NoRecordError: irc.errorInvalid(_('task id'), id) setpriority = wrap(setpriority, ['user', ('id', 'task'), ('int', 'priority')]) @internationalizeDocstring def change(self, irc, msg, args, user, id, replacer): """ Modify the task with the given id using the supplied regexp. """ try: self.db.change(user.id, id, replacer) irc.replySuccess() except dbi.NoRecordError: irc.errorInvalid(_('task id'), id) change = wrap(change, ['user', ('id', 'task'), 'regexpReplacer']) Class = Todo # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Todo/config.py0000644000175000017500000000511713233426066017530 0ustar valval00000000000000### # Copyright (c) 2003-2005, Daniel DiPaolo # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Todo') 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('Todo', True) Todo = conf.registerPlugin('Todo') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Todo, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerGlobalValue(Todo, 'allowThirdpartyReader', registry.Boolean(False, _("""Determines whether users can read the todo-list of another user."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Todo/__init__.py0000644000175000017500000000471613233426066020026 0ustar valval00000000000000### # Copyright (c) 2003-2005, Daniel DiPaolo # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ The Todo plugin allows registered users to keep their own personal list of tasks to do, with an optional priority for each. """ 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__ = "%%VERSION%%" __author__ = supybot.authors.strike # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from imp import reload 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! if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Todo/locales/0000755000175000017500000000000013233426077017331 5ustar valval00000000000000limnoria-2018.01.25/plugins/Todo/locales/it.po0000644000175000017500000001072513233426066020310 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-02-26 09:49+CET\n" "PO-Revision-Date: 2011-08-10 14:53+0200\n" "Last-Translator: skizzhg \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: config.py:50 msgid "" "Determines whether users can read the\n" " todo-list of another user." msgstr "" "Determina se un utente possa leggere la lista delle cose da fare di un altro utente." #: plugin.py:135 #, docstring msgid "" "[] []\n" "\n" " Retrieves a task for the given task id. If no task id is given, it\n" " will return a list of task ids that that user has added to their todo\n" " list.\n" " " msgstr "" "[] []\n" "\n" " Recupera il compito corrispondente a fornito. Se non è\n" " specificato alcun id, riporta un elenco degli id che quell'utente\n" " ha aggiunto alla sua lista delle cose da fare.\n" " " #: plugin.py:146 msgid "You are not allowed to see other users todo-list." msgstr "Non hai l'autorizzazione per leggere la lista delle cose da fare degli altri utenti." #: plugin.py:153 msgid "#%i: %s" msgstr "#%i: %s" #: plugin.py:158 msgid "%s for %s: %L" msgstr "%s per %s: %L" #: plugin.py:162 msgid "That user has no tasks in their todo list." msgstr "Questo utente non ha compiti nella sua lista delle cose da fare." #: plugin.py:164 msgid "You have no tasks in your todo list." msgstr "Non hai compiti nella tua lista delle cose da fare." #: plugin.py:171 msgid "Active" msgstr "Attivo" #: plugin.py:173 msgid "Inactive" msgstr "Inattivo" #: plugin.py:175 msgid ", priority: %i" msgstr ", priorità: %i" #: plugin.py:178 msgid "%s todo for %s: %s (Added at %s)" msgstr "%s compito per %s: %s (Aggiunto il %s)" #: plugin.py:182 plugin.py:263 plugin.py:277 msgid "task id" msgstr "id compito" #: plugin.py:187 #, docstring msgid "" "[--priority=] \n" "\n" " Adds as a task in your own personal todo list. The optional\n" " priority argument allows you to set a task as a high or low priority.\n" " Any integer is valid.\n" " " msgstr "" "[--priority=] \n" "\n" " Aggiunge come compito nella lista personale di cose da fare.\n" " L'argomento opzionale \"priority\" permette di definire un'alta o bassa priorità.\n" " Ogni numero intero è valido.\n" " " #: plugin.py:198 msgid "(Todo #%i added)" msgstr "(Compito #%i aggiunto)" #: plugin.py:204 #, docstring msgid "" " [ ...]\n" "\n" " Removes from your personal todo list.\n" " " msgstr "" " [ ...]\n" "\n" " Rimuove dalla lista personale delle cose da fare.\n" " " #: plugin.py:215 msgid "Task %i could not be removed either because that id doesn't exist or it has been removed already." msgstr "Il compito %i non può essere rimosso in quanto l'id non esiste o è già stato rimosso." #: plugin.py:219 msgid "No tasks were removed because the following tasks could not be removed: %L." msgstr "Non è stato rimosso nessun compito perché i seguenti non possono essere rimossi: %L." #: plugin.py:229 #, docstring msgid "" "[--{regexp} ] [ ...]\n" "\n" " Searches your todos for tasks matching . If --regexp is given,\n" " its associated value is taken as a regexp and matched against the\n" " tasks.\n" " " msgstr "" "[--{regexp} ] [ ...]\n" "\n" " Cerca i compiti che corrispondono a nella lista di cose da fare.\n" " Se --regexp è fornita, il suo valore associato è usato come regexp e confrontato con i compiti.\n" " " #: plugin.py:249 msgid "No tasks matched that query." msgstr "Nessun compito corrisponde alla richiesta." #: plugin.py:255 #, docstring msgid "" " \n" "\n" " Sets the priority of the todo with the given id to the specified value.\n" " " msgstr "" " \n" "\n" " Imposta la priorità del compito con l'id fornito al valore specificato.\n" " " #: plugin.py:269 #, docstring msgid "" " \n" "\n" " Modify the task with the given id using the supplied regexp.\n" " " msgstr "" " \n" "\n" " Modifica il compito con il dato id utilizzando una regexp.\n" " " limnoria-2018.01.25/plugins/Todo/locales/fr.po0000644000175000017500000001050313233426066020275 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2011-08-10 11:28+CEST\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:50 msgid "" "Determines whether users can read the\n" " todo-list of another user." msgstr "Détermine si les utilisateurs peuvent lire la todo-list d'autres utilisateurs." #: plugin.py:135 msgid "" "[] []\n" "\n" " Retrieves a task for the given task id. If no task id is given, it\n" " will return a list of task ids that that user has added to their todo\n" " list.\n" " " msgstr "" "[ ]\n" "\n" "Récupère la tâche correspondant à l' donné. Si aucun id n'est donné, retourne une liste d'ids que cet utilisateur a ajouté à sa liste." #: plugin.py:146 msgid "You are not allowed to see other users todo-list." msgstr "Vous n'êtes pas autorisé(e) à voir la todo-list d'autres utilisateurs." #: plugin.py:153 msgid "#%i: %s" msgstr "#%i : %s" #: plugin.py:158 msgid "%s for %s: %L" msgstr "%s pour %s : %L" #: plugin.py:162 msgid "That user has no tasks in their todo list." msgstr "Cet utilisateur n'a pas de tâche dans sa todo-list." #: plugin.py:164 msgid "You have no tasks in your todo list." msgstr "Vous n'avez pas de tâche dans votre todo-list." #: plugin.py:171 msgid "Active" msgstr "Active" #: plugin.py:173 msgid "Inactive" msgstr "Inactive" #: plugin.py:175 msgid ", priority: %i" msgstr ", priorité : %i" #: plugin.py:178 msgid "%s todo for %s: %s (Added at %s)" msgstr "%s tâche pour %s : %s (Ajoutée à %s)" #: plugin.py:182 #: plugin.py:263 #: plugin.py:277 msgid "task id" msgstr "id de tâche" #: plugin.py:187 msgid "" "[--priority=] \n" "\n" " Adds as a task in your own personal todo list. The optional\n" " priority argument allows you to set a task as a high or low priority.\n" " Any integer is valid.\n" " " msgstr "" "[--priority=] \n" "\n" "Ajoute le comme une tâche dans votre liste personnelle de choses à faire. L'argument 'priority' optionnel vous permet de définir une priorité faible ou grande. Tout entier est accepté." #: plugin.py:198 msgid "(Todo #%i added)" msgstr "(Tâche #%i ajoutée)" #: plugin.py:204 msgid "" " [ ...]\n" "\n" " Removes from your personal todo list.\n" " " msgstr "" " [ ...]\n" "\n" "Supprime différentes tâches, désignées par leur ID, de votre liste personnelle de choses à faire." #: plugin.py:215 msgid "Task %i could not be removed either because that id doesn't exist or it has been removed already." msgstr "La tâche %i ne peut être supprimée car cet id n'existe pas, ou a déjà été supprimé." #: plugin.py:219 msgid "No tasks were removed because the following tasks could not be removed: %L." msgstr "Aucune tâche n'a été supprimée car les tâches suivantes ne peuvent l'être : %L." #: plugin.py:229 msgid "" "[--{regexp} ] [ ...]\n" "\n" " Searches your todos for tasks matching . If --regexp is given,\n" " its associated value is taken as a regexp and matched against the\n" " tasks.\n" " " msgstr "" "[--{regexp} ] [ ...]\n" "\n" "Recherche parmis vos tâches celle(s) correspondant au . Si --regexp est donné, il prend la valeur associée comme étant une expression régulière et re-teste les tâches." #: plugin.py:249 msgid "No tasks matched that query." msgstr "Aucune tâche ne correspond à cette requête." #: plugin.py:255 msgid "" " \n" "\n" " Sets the priority of the todo with the given id to the specified value.\n" " " msgstr "" " \n" "\n" "Défini la priorité de la tâche d' donné, à la valeur choisie." #: plugin.py:269 msgid "" " \n" "\n" " Modify the task with the given id using the supplied regexp.\n" " " msgstr "" " \n" "\n" "Modifie la tâche en utilisant l'expression régulière donnée." limnoria-2018.01.25/plugins/Todo/locales/fi.po0000644000175000017500000001174013233426066020270 0ustar valval00000000000000# Todo plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Todo plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 14:04+EET\n" "PO-Revision-Date: 2014-12-20 14:20+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: 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" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: config.py:50 msgid "" "Determines whether users can read the\n" " todo-list of another user." msgstr "" "Määrittää voivatko käyttäjät lukea muiden käyttäjien\n" " tehtävälistoja." #: plugin.py:122 msgid "" "This plugin allows you to create your own personal to-do list on\n" " the bot." msgstr "" "Tämä plugin sallii käyttäjien tehdä henkilökohtaisia tehtävälistoja bottiin." #: plugin.py:138 msgid "" "[] []\n" "\n" " Retrieves a task for the given task id. If no task id is given, it\n" " will return a list of task ids that that user has added to their " "todo\n" " list.\n" " " msgstr "" "[] [] \n" "\n" " Adds as a task in your own personal todo list. The optional\n" " priority argument allows you to set a task as a high or low " "priority.\n" " Any integer is valid.\n" " " msgstr "" "[--priority=] \n" "\n" " Lisää tehtävänä sinun omalle henkilökohtaiselle " "tehtävälistallesi. Vaihtoehtoinen\n" " priority(=tärkeysaste) sallii sinun asettaa tehtävän korkealle tai " "matalalle tärkeysasteelle.\n" " Mikä tahansa kokonaisluku on kelvollinen.\n" " " #: plugin.py:201 msgid "(Todo #%i added)" msgstr "Tehtävä %i lisätty" #: plugin.py:207 msgid "" " [ ...]\n" "\n" " Removes from your personal todo list.\n" " " msgstr "" " [ ...]\n" "\n" " Poistaa henkilökohtaiselta tehtävälistaltasi.\n" " " #: plugin.py:218 msgid "" "Task %i could not be removed either because that id doesn't exist or it has " "been removed already." msgstr "" "Tehtävää %i ei voitu poistaa, koska se ei ollut olemassa tai se on jo " "poistettu." #: plugin.py:222 msgid "" "No tasks were removed because the following tasks could not be removed: %L." msgstr "Tehtäviä ei poistettu, koska seuraavia tehtäviä ei voitu poistaa: %L." #: plugin.py:232 msgid "" "[--{regexp} ] [ ...]\n" "\n" " Searches your todos for tasks matching . If --regexp is " "given,\n" " its associated value is taken as a regexp and matched against the\n" " tasks.\n" " " msgstr "" "[--{regexp} ] [ ...]\n" "\n" " Etsii täsmääviä tehtävia tehtävälistaltasi. Jos --regexp " "on annettu,\n" " sen liitetty arvo otetaan säännölliseksi lausekkeeksi ja sitä " "täsmätään\n" " tehtäviin.\n" " " #: plugin.py:255 msgid "No tasks matched that query." msgstr "Yksikään tehtävä ei täsmännyt tuohon hakuun." #: plugin.py:261 msgid "" " \n" "\n" " Sets the priority of the todo with the given id to the specified " "value.\n" " " msgstr "" " \n" "\n" " Asettaa tehtävän ID tärkeysasteen annetulle arvolle.\n" " " #: plugin.py:275 msgid "" " \n" "\n" " Modify the task with the given id using the supplied regexp.\n" " " msgstr "" " \n" "\n" " Muokkaa tehtävää annetulla ID:llä käyttäen annettua säännöllistä " "lauseketta.\n" " " limnoria-2018.01.25/plugins/Todo/locales/de.po0000644000175000017500000001046413233426066020264 0ustar valval00000000000000msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2011-08-10 11:28+CEST\n" "PO-Revision-Date: 2011-11-05 17:29+0100\n" "Last-Translator: Florian Besser \n" "Language-Team: German \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" "Language: de\n" #: config.py:50 msgid "" "Determines whether users can read the\n" " todo-list of another user." msgstr "Legt fest ob Benutzer die Aufgabenlisten anderes Benutzer lesen können." #: plugin.py:135 msgid "" "[] []\n" "\n" " Retrieves a task for the given task id. If no task id is given, it\n" " will return a list of task ids that that user has added to their todo\n" " list.\n" " " msgstr "" "[] []\n" "\n" "Empfängt die Aufgabe für die gegebene Aufgaben ID. Falls keine Aufgaben ID angegeben wird, wird eine Liste der Aufgaben IDs ausgegeben, die der Benutzer zu seiner Aufgabenliste hinzugefügt hat." #: plugin.py:146 msgid "You are not allowed to see other users todo-list." msgstr "Du bist nicht berechtigt, Aufgabenlisten anderer Benutzer zu sehen." #: plugin.py:153 msgid "#%i: %s" msgstr "#%i: %s" #: plugin.py:158 msgid "%s for %s: %L" msgstr "%s für %s: %L" #: plugin.py:162 msgid "That user has no tasks in their todo list." msgstr "Der Benutzer hat keine Aufgaben in seiner Aufgabenliste." #: plugin.py:164 msgid "You have no tasks in your todo list." msgstr "Du hast keine Aufgaben in deiner Aufgabenliste." #: plugin.py:171 msgid "Active" msgstr "Aktiv" #: plugin.py:173 msgid "Inactive" msgstr "Inaktiv" #: plugin.py:175 msgid ", priority: %i" msgstr ", Priorität: %i" #: plugin.py:178 msgid "%s todo for %s: %s (Added at %s)" msgstr "%s Aufgabe für %s: %s (hinzugefügt am %s)" #: plugin.py:182 #: plugin.py:263 #: plugin.py:277 msgid "task id" msgstr "Aufgaben ID" #: plugin.py:187 msgid "" "[--priority=] \n" "\n" " Adds as a task in your own personal todo list. The optional\n" " priority argument allows you to set a task as a high or low priority.\n" " Any integer is valid.\n" " " msgstr "" "[--priority=] \n" "\n" "Fügt als Aufgabe deiner persönlinen Aufgabenliste hinzu. Das optionale Prioritätsargument, erlaubt dir deiner Aufgabe eine hohe oder niedrige Priorität zuzuweisen. Jede Ganzzahl ist zulässig." #: plugin.py:198 msgid "(Todo #%i added)" msgstr "(Aufgabe #%i hinzugefügt)" #: plugin.py:204 msgid "" " [ ...]\n" "\n" " Removes from your personal todo list.\n" " " msgstr "" " [ ...]\n" "\n" "Entfernt von deiner persönlichen Aufgabenliste." #: plugin.py:215 msgid "Task %i could not be removed either because that id doesn't exist or it has been removed already." msgstr "Aufgabe %i konnte nicht entfernt werden, da entweder die ID nicht existiert oder sie bereits entfernt wurde." #: plugin.py:219 msgid "No tasks were removed because the following tasks could not be removed: %L." msgstr "Es wurden keine Aufgaben entfernt, da die folgenen Aufgaben nicht entfernt werden konnten: %L." #: plugin.py:229 msgid "" "[--{regexp} ] [ ...]\n" "\n" " Searches your todos for tasks matching . If --regexp is given,\n" " its associated value is taken as a regexp and matched against the\n" " tasks.\n" " " msgstr "" "[--{regexp} ] [ ...]\n" "\n" "Sucht in deinen Aufgaben nach Aufgaben die auf passen. Falls --regexp angegeben wird, wird der damit verknüpfte Wert dazu benutzt um nach Aufgaben zu suchen." #: plugin.py:249 msgid "No tasks matched that query." msgstr "Keine Aufgaben die auf die Anfrage passen." #: plugin.py:255 msgid "" " \n" "\n" " Sets the priority of the todo with the given id to the specified value.\n" " " msgstr "" " \n" "\n" "Setzte die Priorität der Aufgabe, der ID, auf den angegeben Wert." #: plugin.py:269 msgid "" " \n" "\n" " Modify the task with the given id using the supplied regexp.\n" " " msgstr "" " \n" "\n" "Modifiziert die Aufgaben, der gegeben ID, durch den angegebenen regulären Ausdruck." limnoria-2018.01.25/plugins/Time/0000755000175000017500000000000013233426077015700 5ustar valval00000000000000limnoria-2018.01.25/plugins/Time/test.py0000644000175000017500000001010013233426066017217 0ustar valval00000000000000### # Copyright (c) 2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from supybot.test import * try: import pytz except ImportError: has_pytz = False else: has_pytz = True try: import dateutil except ImportError: has_dateutil = False else: has_dateutil = True try: import ddate.base except ImportError: has_ddate = False else: has_ddate = True try: from unittest import skipIf except ImportError: # Python 2.6 def skipIf(cond, reason): if cond: print('Skipped: %s' % reason) def decorator(f): return None else: def decorator(f): return f return decorator class TimeTestCase(PluginTestCase): plugins = ('Time','Utilities') def testSeconds(self): self.assertResponse('seconds 1s', '1') self.assertResponse('seconds 10s', '10') self.assertResponse('seconds 1m', '60') self.assertResponse('seconds 1m 1s', '61') self.assertResponse('seconds 1h', '3600') self.assertResponse('seconds 1h 1s', '3601') self.assertResponse('seconds 1d', '86400') self.assertResponse('seconds 1d 1s', '86401') self.assertResponse('seconds 2s', '2') self.assertResponse('seconds 2m', '120') self.assertResponse('seconds 2d 2h 2m 2s', '180122') self.assertResponse('seconds 1s', '1') self.assertResponse('seconds 1y 1s', '31536001') self.assertResponse('seconds 1w 1s', '604801') def testNoErrors(self): self.assertNotError('ctime') self.assertNotError('time %Y') @skipIf(not has_pytz, 'pytz is missing') def testTztime(self): self.assertNotError('tztime Europe/Paris') self.assertError('tztime Europe/Gniarf') @skipIf(not has_dateutil, 'python-dateutil is missing') def testUntil(self): self.assertNotError('echo [until 4:00]') self.assertNotError('echo [at now]') def testNoNestedErrors(self): self.assertNotError('echo [seconds 4m]') @skipIf(not has_ddate, 'ddate is missing') def testDDate(self): self.assertNotError('ddate') self.assertHelp('ddate 0 0 0') # because nonsense was put in self.assertHelp('ddate -1 1 1') # because nonsense was put in self.assertHelp('ddate -1 -1 -1') # because nonsense was put in # plugin.py:223 would catch these otherwise self.assertResponse('ddate 1 1 1', 'Sweetmorn, the 1st day of Chaos in the YOLD 1167') # make sure the laws of physics and time aren't out of wack # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: limnoria-2018.01.25/plugins/Time/plugin.py0000644000175000017500000002165413233426066017556 0ustar valval00000000000000### # Copyright (c) 2004, Jeremiah Fincher # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import time TIME = time # For later use. from datetime import datetime import supybot.conf as conf import supybot.log as log import supybot.utils as utils from supybot.commands import * import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Time') try: from ddate.base import DDate as _ddate except ImportError: log.debug("Time: the ddate module is not available; disabling that command.") _ddate = None try: from dateutil import parser def parse(s): todo = [] s = s.replace('noon', '12:00') s = s.replace('midnight', '00:00') if 'tomorrow' in s: todo.append(lambda i: i + 86400) s = s.replace('tomorrow', '') if 'next week' in s: todo.append(lambda i: i + 86400*7) s = s.replace('next week', '') i = int(time.mktime(parser.parse(s, fuzzy=True).timetuple())) for f in todo: i = f(i) return i except ImportError: parse = None try: from dateutil.tz import tzlocal except ImportError: tzlocal = None try: import pytz except ImportError: pytz = None class Time(callbacks.Plugin): """This plugin allows you to use different time-related functions.""" @internationalizeDocstring def seconds(self, irc, msg, args): """[y] [w] [d] [h] [m] [s] Returns the number of seconds in the number of , , , , , and given. An example usage is "seconds 2h 30m", which would return 9000, which is '3600*2 + 30*60'. Useful for scheduling events at a given number of seconds in the future. """ if not args: raise callbacks.ArgumentError seconds = 0 for arg in args: if not arg or arg[-1] not in 'ywdhms': raise callbacks.ArgumentError (s, kind) = arg[:-1], arg[-1] try: i = int(s) except ValueError: irc.errorInvalid('argument', arg, Raise=True) if kind == 'y': seconds += i*31536000 elif kind == 'w': seconds += i*604800 elif kind == 'd': seconds += i*86400 elif kind == 'h': seconds += i*3600 elif kind == 'm': seconds += i*60 elif kind == 's': seconds += i irc.reply(str(seconds)) @internationalizeDocstring def at(self, irc, msg, args, s=None): """[