././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3737545 limnoria-2023.11.18/0000755000175000017500000000000014535072535012122 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/LICENSE.md0000644000175000017500000000316614535072470013532 0ustar00valvalCopyright (c) 2002-2009 Jeremiah Fincher and others All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author of this software nor the name of contributors to this software may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Portions of the included source code are copyright by its original author(s) and remain subject to its associated license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/MANIFEST.in0000644000175000017500000000016514535072470013660 0ustar00valvalinclude LICENSE.md include README.md include Makefile include .travis.yml include requirements.txt include test/*.py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/Makefile0000644000175000017500000000172714535072470013567 0ustar00valvalPYTHON=`which python3` DESTDIR=/ PROJECT=limnoria all: @echo "make source - Create source package" @echo "make install - Install on local system" @echo "make buildrpm - Generate a rpm package" @echo "make builddeb_py2 - Generate a deb package for Python 2" @echo "make builddeb_py3 - Generate a deb package for Python 3" @echo "make clean - Get rid of scratch and byte files" test: PATH=./scripts/:${PATH} PYTHONPATH=. $(PYTHON) ./scripts/supybot-test test --plugins-dir=plugins/ source: $(PYTHON) setup.py sdist $(COMPILE) install: $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) buildrpm: $(PYTHON) setup.py bdist_rpm builddeb_py2: cp debian/control.py2 debian/control debuild -us -uc rm debian/control builddeb_py3: cp debian/control.py3 debian/control debuild -us -uc rm debian/control clean: $(PYTHON) setup.py clean $(MAKE) -f $(CURDIR)/debian/rules clean rm -rf build/ MANIFEST find . -name '*.pyc' -delete rm debian/control .PHONY: test ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3697546 limnoria-2023.11.18/PKG-INFO0000644000175000017500000000347114535072535013224 0ustar00valvalMetadata-Version: 2.1 Name: limnoria Version: 2023.11.18 Summary: A multipurpose Python IRC bot, designed for flexibility and robustness , while being easy to install, set up, and maintain. Home-page: https://limnoria.net/ Download-URL: https://pypi.python.org/pypi/limnoria Author: Valentin Lorentz Author-email: progval+limnoria@progval.net 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 :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Communications :: Chat :: Internet Relay Chat Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides: supybot License-File: LICENSE.md 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/README.md0000644000175000017500000000436414535072470013406 0ustar00valvalLimnoria is a multipurpose Python IRC bot, designed for flexibility and robustness, while being easy to install, set up, and maintain. It aims to be an adequate replacement for most existing IRC bots. It includes a very flexible and powerful [ACL system](https://docs.limnoria.net/use/capabilities.html) for controlling access to commands, an equality powerful [configuration system](https://docs.limnoria.net/use/configuration.html) to customize your bot, as well as more than 60 builtin [plugins](https://limnoria.net/plugins.xhtml) providing around 400 actual commands. There are also dozens of third-party [plugins](https://limnoria.net/plugins.xhtml) written by dozens of independent developers, and it is very easy to [write your own](https://docs.limnoria.net/develop/plugin_tutorial.html) with only basic knowledge of Python. It is the successor of [Supybot](https://sourceforge.net/projects/supybot/) since 2010 and provides many new features, but keeps full compatibility with existing configurations and plugins. # Support ## Documentation If this is your first install, there is an [install guide](https://docs.limnoria.net/en/latest/use/install.html). You will probably be pointed to it if you ask on IRC how to install Limnoria. TL;DR version: ``` sudo apt-get install python3 python3-pip python3-wheel pip3 install --user limnoria # You might need to add $HOME/.local/bin to your PATH supybot-wizard ``` There is extensive documentation at [docs.limnoria.net] and at [Gribble wiki]. We took the time to write it; you should take the time to read it. [docs.limnoria.net]:https://docs.limnoria.net/ [Gribble wiki]:https://sourceforge.net/p/gribble/wiki/Main_Page/ ## IRC channels ### In English If you have any trouble, feel free to swing by [#limnoria](ircs://irc.libera.chat:6697/#limnoria) on [Libera.Chat](https://libera.chat/) and ask questions. We'll be happy to help wherever we can. And by all means, if you find anything hard to understand or think you know of a better way to do something, *please* post it on the [issue tracker] so we can improve the bot! [issue tracker]:https://github.com/ProgVal/Limnoria/issues ### In Other languages Only in French at the moment, located at [#limnoria-fr on Libera.Chat](ircs://irc.libera.chat:6697/#libera-fr). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3257546 limnoria-2023.11.18/limnoria.egg-info/0000755000175000017500000000000014535072535015426 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131037.0 limnoria-2023.11.18/limnoria.egg-info/PKG-INFO0000644000175000017500000000347114535072535016530 0ustar00valvalMetadata-Version: 2.1 Name: limnoria Version: 2023.11.18 Summary: A multipurpose Python IRC bot, designed for flexibility and robustness , while being easy to install, set up, and maintain. Home-page: https://limnoria.net/ Download-URL: https://pypi.python.org/pypi/limnoria Author: Valentin Lorentz Author-email: progval+limnoria@progval.net 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 :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Communications :: Chat :: Internet Relay Chat Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides: supybot License-File: LICENSE.md 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131037.0 limnoria-2023.11.18/limnoria.egg-info/SOURCES.txt0000644000175000017500000003760014535072535017320 0ustar00valvalLICENSE.md MANIFEST.in Makefile README.md pyproject.toml requirements.txt setup.py limnoria.egg-info/PKG-INFO limnoria.egg-info/SOURCES.txt limnoria.egg-info/dependency_links.txt limnoria.egg-info/entry_points.txt limnoria.egg-info/top_level.txt locales/__init__.py locales/de.po locales/fi.po locales/fr.po locales/fr.py locales/it.po locales/messages.pot man/limnoria-adduser.1 man/limnoria-botchk.1 man/limnoria-plugin-create.1 man/limnoria-plugin-doc.1 man/limnoria-reset-password.1 man/limnoria-test.1 man/limnoria-wizard.1 man/limnoria.1 man/supybot-adduser.1 man/supybot-botchk.1 man/supybot-plugin-create.1 man/supybot-plugin-doc.1 man/supybot-reset-password.1 man/supybot-test.1 man/supybot-wizard.1 man/supybot.1 plugins/__init__.py plugins/Admin/__init__.py plugins/Admin/config.py plugins/Admin/plugin.py plugins/Admin/test.py plugins/Admin/locales/de.po plugins/Admin/locales/fi.po plugins/Admin/locales/fr.po plugins/Admin/locales/it.po plugins/Aka/__init__.py plugins/Aka/config.py plugins/Aka/plugin.py plugins/Aka/test.py plugins/Aka/locales/fi.po plugins/Alias/__init__.py plugins/Alias/config.py plugins/Alias/plugin.py plugins/Alias/test.py plugins/Alias/locales/de.po plugins/Alias/locales/fi.po plugins/Alias/locales/fr.po plugins/Alias/locales/hu.po plugins/Alias/locales/it.po plugins/Anonymous/__init__.py plugins/Anonymous/config.py plugins/Anonymous/plugin.py plugins/Anonymous/test.py plugins/Anonymous/locales/de.po plugins/Anonymous/locales/fi.po plugins/Anonymous/locales/fr.po plugins/Anonymous/locales/hu.po plugins/Anonymous/locales/it.po plugins/AutoMode/__init__.py plugins/AutoMode/config.py plugins/AutoMode/plugin.py plugins/AutoMode/test.py plugins/AutoMode/locales/de.po plugins/AutoMode/locales/fi.po plugins/AutoMode/locales/fr.po plugins/AutoMode/locales/it.po plugins/Autocomplete/__init__.py plugins/Autocomplete/config.py plugins/Autocomplete/plugin.py plugins/Autocomplete/test.py plugins/BadWords/__init__.py plugins/BadWords/config.py plugins/BadWords/plugin.py plugins/BadWords/test.py plugins/BadWords/locales/fi.po plugins/BadWords/locales/fr.po plugins/BadWords/locales/it.po plugins/Channel/__init__.py plugins/Channel/config.py plugins/Channel/plugin.py plugins/Channel/test.py plugins/Channel/locales/de.po plugins/Channel/locales/fi.po plugins/Channel/locales/fr.po plugins/Channel/locales/hu.po plugins/Channel/locales/it.po plugins/ChannelLogger/__init__.py plugins/ChannelLogger/config.py plugins/ChannelLogger/plugin.py plugins/ChannelLogger/test.py plugins/ChannelLogger/locales/fi.po plugins/ChannelLogger/locales/fr.po plugins/ChannelLogger/locales/hu.po plugins/ChannelLogger/locales/it.po plugins/ChannelStats/__init__.py plugins/ChannelStats/config.py plugins/ChannelStats/plugin.py plugins/ChannelStats/test.py plugins/ChannelStats/locales/fi.po plugins/ChannelStats/locales/fr.po plugins/ChannelStats/locales/it.po plugins/Conditional/__init__.py plugins/Conditional/config.py plugins/Conditional/plugin.py plugins/Conditional/test.py plugins/Conditional/locales/fi.po plugins/Conditional/locales/fr.po plugins/Conditional/locales/it.po plugins/Config/__init__.py plugins/Config/config.py plugins/Config/plugin.py plugins/Config/test.py plugins/Config/locales/de.po plugins/Config/locales/fi.po plugins/Config/locales/fr.po plugins/Config/locales/hu.po plugins/Config/locales/it.po plugins/Ctcp/__init__.py plugins/Ctcp/config.py plugins/Ctcp/plugin.py plugins/Ctcp/test.py plugins/Ctcp/locales/de.po plugins/Ctcp/locales/fi.po plugins/Ctcp/locales/fr.po plugins/Ctcp/locales/hu.po plugins/Ctcp/locales/it.po plugins/DDG/__init__.py plugins/DDG/config.py plugins/DDG/parser.py plugins/DDG/plugin.py plugins/DDG/test.py plugins/Debug/__init__.py plugins/Debug/config.py plugins/Debug/plugin.py plugins/Debug/test.py plugins/Dict/__init__.py plugins/Dict/config.py plugins/Dict/plugin.py plugins/Dict/test.py plugins/Dict/local/__init__.py plugins/Dict/local/dictclient.py plugins/Dict/locales/fi.po plugins/Dict/locales/fr.po plugins/Dict/locales/it.po plugins/Dunno/__init__.py plugins/Dunno/config.py plugins/Dunno/plugin.py plugins/Dunno/test.py plugins/Dunno/locales/de.po plugins/Dunno/locales/fi.po plugins/Dunno/locales/fr.po plugins/Dunno/locales/it.po plugins/Factoids/__init__.py plugins/Factoids/config.py plugins/Factoids/plugin.py plugins/Factoids/test.py plugins/Factoids/locales/fi.po plugins/Factoids/locales/fr.po plugins/Factoids/locales/it.po plugins/Fediverse/__init__.py plugins/Fediverse/activitypub.py plugins/Fediverse/config.py plugins/Fediverse/plugin.py plugins/Fediverse/test.py plugins/Fediverse/test_data.py plugins/Fediverse/utils.py plugins/Filter/__init__.py plugins/Filter/config.py plugins/Filter/plugin.py plugins/Filter/test.py plugins/Filter/locales/fi.po plugins/Filter/locales/fr.po plugins/Filter/locales/it.po plugins/Format/__init__.py plugins/Format/config.py plugins/Format/plugin.py plugins/Format/test.py plugins/Format/locales/fi.po plugins/Format/locales/fr.po plugins/Format/locales/it.po plugins/GPG/__init__.py plugins/GPG/config.py plugins/GPG/plugin.py plugins/GPG/test.py plugins/Games/__init__.py plugins/Games/config.py plugins/Games/plugin.py plugins/Games/test.py plugins/Games/locales/de.po plugins/Games/locales/fi.po plugins/Games/locales/fr.po plugins/Games/locales/it.po plugins/Geography/__init__.py plugins/Geography/common.py plugins/Geography/config.py plugins/Geography/nominatim.py plugins/Geography/plugin.py plugins/Geography/test.py plugins/Geography/wikidata.py plugins/Google/__init__.py plugins/Google/config.py plugins/Google/parser.py plugins/Google/plugin.py plugins/Google/test.py plugins/Google/locales/fi.po plugins/Google/locales/fr.po plugins/Google/locales/it.po plugins/Hashes/__init__.py plugins/Hashes/config.py plugins/Hashes/plugin.py plugins/Hashes/test.py plugins/Herald/__init__.py plugins/Herald/config.py plugins/Herald/plugin.py plugins/Herald/test.py plugins/Herald/locales/fi.po plugins/Herald/locales/fr.po plugins/Herald/locales/it.po plugins/Internet/__init__.py plugins/Internet/config.py plugins/Internet/plugin.py plugins/Internet/test.py plugins/Internet/locales/fi.po plugins/Internet/locales/fr.po plugins/Internet/locales/it.po plugins/Karma/__init__.py plugins/Karma/config.py plugins/Karma/plugin.py plugins/Karma/test.py plugins/Karma/locales/fi.po plugins/Karma/locales/fr.po plugins/Karma/locales/it.po plugins/Lart/__init__.py plugins/Lart/config.py plugins/Lart/plugin.py plugins/Lart/test.py plugins/Lart/locales/fi.po plugins/Lart/locales/fr.po plugins/Lart/locales/it.po plugins/Later/__init__.py plugins/Later/config.py plugins/Later/plugin.py plugins/Later/test.py plugins/Later/locales/de.po plugins/Later/locales/fi.po plugins/Later/locales/fr.po plugins/Later/locales/it.po plugins/Limiter/__init__.py plugins/Limiter/config.py plugins/Limiter/plugin.py plugins/Limiter/test.py plugins/Limiter/locales/fi.po plugins/Limiter/locales/fr.po plugins/Limiter/locales/hu.po plugins/Limiter/locales/it.po plugins/LogToIrc/__init__.py plugins/LogToIrc/config.py plugins/LogToIrc/handler.py plugins/LogToIrc/plugin.py plugins/LogToIrc/test.py plugins/Math/__init__.py plugins/Math/config.py plugins/Math/plugin.py plugins/Math/test.py plugins/Math/local/__init__.py plugins/Math/local/convertcore.py plugins/Math/locales/fi.po plugins/Math/locales/fr.po plugins/Math/locales/hu.po plugins/Math/locales/it.po plugins/MessageParser/__init__.py plugins/MessageParser/config.py plugins/MessageParser/plugin.py plugins/MessageParser/test.py plugins/MessageParser/locales/fi.po plugins/MessageParser/locales/fr.po plugins/MessageParser/locales/it.po plugins/Misc/__init__.py plugins/Misc/config.py plugins/Misc/plugin.py plugins/Misc/test.py plugins/Misc/locales/de.po plugins/Misc/locales/fi.po plugins/Misc/locales/fr.po plugins/Misc/locales/hu.po plugins/Misc/locales/it.po plugins/MoobotFactoids/__init__.py plugins/MoobotFactoids/config.py plugins/MoobotFactoids/plugin.py plugins/MoobotFactoids/test.py plugins/MoobotFactoids/locales/fi.po plugins/MoobotFactoids/locales/fr.po plugins/MoobotFactoids/locales/it.po plugins/Network/__init__.py plugins/Network/config.py plugins/Network/plugin.py plugins/Network/test.py plugins/Network/locales/de.po plugins/Network/locales/fi.po plugins/Network/locales/fr.po plugins/Network/locales/it.po plugins/News/__init__.py plugins/News/config.py plugins/News/plugin.py plugins/News/test.py plugins/News/locales/fi.po plugins/News/locales/fr.po plugins/News/locales/it.po plugins/NickAuth/__init__.py plugins/NickAuth/config.py plugins/NickAuth/plugin.py plugins/NickAuth/test.py plugins/NickAuth/locales/de.po plugins/NickAuth/locales/fi.po plugins/NickCapture/__init__.py plugins/NickCapture/config.py plugins/NickCapture/plugin.py plugins/NickCapture/test.py plugins/NickCapture/locales/de.po plugins/NickCapture/locales/fi.po plugins/NickCapture/locales/fr.po plugins/NickCapture/locales/it.po plugins/Nickometer/__init__.py plugins/Nickometer/config.py plugins/Nickometer/plugin.py plugins/Nickometer/test.py plugins/Nickometer/locales/fi.po plugins/Nickometer/locales/fr.po plugins/Nickometer/locales/it.po plugins/Note/__init__.py plugins/Note/config.py plugins/Note/plugin.py plugins/Note/test.py plugins/Note/locales/fi.po plugins/Note/locales/fr.po plugins/Note/locales/it.po plugins/Owner/__init__.py plugins/Owner/config.py plugins/Owner/plugin.py plugins/Owner/test.py plugins/Owner/locales/de.po plugins/Owner/locales/fi.po plugins/Owner/locales/fr.po plugins/Owner/locales/hu.po plugins/Owner/locales/it.po plugins/Plugin/__init__.py plugins/Plugin/config.py plugins/Plugin/plugin.py plugins/Plugin/test.py plugins/Plugin/locales/de.po plugins/Plugin/locales/fi.po plugins/Plugin/locales/fr.po plugins/Plugin/locales/it.po plugins/PluginDownloader/__init__.py plugins/PluginDownloader/config.py plugins/PluginDownloader/plugin.py plugins/PluginDownloader/test.py plugins/PluginDownloader/locales/de.po plugins/PluginDownloader/locales/fi.po plugins/PluginDownloader/locales/fr.po plugins/PluginDownloader/locales/it.po plugins/Poll/__init__.py plugins/Poll/config.py plugins/Poll/plugin.py plugins/Poll/test.py plugins/Praise/__init__.py plugins/Praise/config.py plugins/Praise/plugin.py plugins/Praise/test.py plugins/Praise/locales/fi.po plugins/Praise/locales/fr.po plugins/Praise/locales/it.po plugins/Protector/__init__.py plugins/Protector/config.py plugins/Protector/plugin.py plugins/Protector/test.py plugins/Protector/locales/fi.po plugins/Protector/locales/fr.po plugins/Protector/locales/it.po plugins/Quote/__init__.py plugins/Quote/config.py plugins/Quote/plugin.py plugins/Quote/test.py plugins/Quote/locales/fi.po plugins/Quote/locales/fr.po plugins/Quote/locales/it.po plugins/QuoteGrabs/__init__.py plugins/QuoteGrabs/config.py plugins/QuoteGrabs/plugin.py plugins/QuoteGrabs/test.py plugins/QuoteGrabs/locales/fi.po plugins/QuoteGrabs/locales/fr.po plugins/QuoteGrabs/locales/it.po plugins/RSS/__init__.py plugins/RSS/config.py plugins/RSS/plugin.py plugins/RSS/test.py plugins/RSS/locales/de.po plugins/RSS/locales/fi.po plugins/RSS/locales/fr.po plugins/RSS/locales/hu.po plugins/RSS/locales/it.po plugins/Relay/__init__.py plugins/Relay/config.py plugins/Relay/plugin.py plugins/Relay/test.py plugins/Relay/locales/fi.po plugins/Relay/locales/fr.po plugins/Relay/locales/it.po plugins/Reply/__init__.py plugins/Reply/config.py plugins/Reply/plugin.py plugins/Reply/test.py plugins/Reply/locales/de.po plugins/Reply/locales/fi.po plugins/Reply/locales/fr.po plugins/Reply/locales/hu.po plugins/Reply/locales/it.po plugins/Scheduler/__init__.py plugins/Scheduler/config.py plugins/Scheduler/plugin.py plugins/Scheduler/test.py plugins/Scheduler/locales/fi.po plugins/Scheduler/locales/fr.po plugins/Scheduler/locales/it.po plugins/SedRegex/__init__.py plugins/SedRegex/config.py plugins/SedRegex/constants.py plugins/SedRegex/plugin.py plugins/SedRegex/test.py plugins/Seen/__init__.py plugins/Seen/config.py plugins/Seen/plugin.py plugins/Seen/test.py plugins/Seen/locales/de.po plugins/Seen/locales/fi.po plugins/Seen/locales/fr.po plugins/Seen/locales/it.po plugins/Services/__init__.py plugins/Services/config.py plugins/Services/plugin.py plugins/Services/test.py plugins/Services/locales/de.po plugins/Services/locales/fi.po plugins/Services/locales/fr.po plugins/Services/locales/it.po plugins/ShrinkUrl/__init__.py plugins/ShrinkUrl/config.py plugins/ShrinkUrl/plugin.py plugins/ShrinkUrl/test.py plugins/ShrinkUrl/locales/fi.po plugins/ShrinkUrl/locales/fr.po plugins/ShrinkUrl/locales/it.po plugins/Status/__init__.py plugins/Status/config.py plugins/Status/plugin.py plugins/Status/test.py plugins/Status/locales/de.po plugins/Status/locales/fi.po plugins/Status/locales/fr.po plugins/Status/locales/it.po plugins/String/__init__.py plugins/String/config.py plugins/String/plugin.py plugins/String/test.py plugins/String/locales/fi.po plugins/String/locales/fr.po plugins/String/locales/it.po plugins/Success/__init__.py plugins/Success/config.py plugins/Success/plugin.py plugins/Success/test.py plugins/Success/locales/fi.po plugins/Success/locales/fr.po plugins/Success/locales/it.po plugins/Time/__init__.py plugins/Time/config.py plugins/Time/plugin.py plugins/Time/test.py plugins/Time/locales/de.po plugins/Time/locales/fi.po plugins/Time/locales/fr.po plugins/Time/locales/hu.po plugins/Time/locales/it.po plugins/Todo/__init__.py plugins/Todo/config.py plugins/Todo/plugin.py plugins/Todo/test.py plugins/Todo/locales/de.po plugins/Todo/locales/fi.po plugins/Todo/locales/fr.po plugins/Todo/locales/it.po plugins/Topic/__init__.py plugins/Topic/config.py plugins/Topic/plugin.py plugins/Topic/test.py plugins/Topic/locales/fi.po plugins/Topic/locales/fr.po plugins/Topic/locales/it.po plugins/URL/__init__.py plugins/URL/config.py plugins/URL/plugin.py plugins/URL/test.py plugins/URL/locales/fi.po plugins/URL/locales/fr.po plugins/URL/locales/it.po plugins/Unix/__init__.py plugins/Unix/config.py plugins/Unix/plugin.py plugins/Unix/test.py plugins/Unix/locales/fi.po plugins/Unix/locales/fr.po plugins/Unix/locales/it.po plugins/User/__init__.py plugins/User/config.py plugins/User/plugin.py plugins/User/test.py plugins/User/locales/de.po plugins/User/locales/fi.po plugins/User/locales/fr.po plugins/User/locales/hu.po plugins/User/locales/it.po plugins/Utilities/__init__.py plugins/Utilities/config.py plugins/Utilities/plugin.py plugins/Utilities/test.py plugins/Utilities/locales/de.po plugins/Utilities/locales/fi.po plugins/Utilities/locales/fr.po plugins/Utilities/locales/it.po plugins/Web/__init__.py plugins/Web/config.py plugins/Web/plugin.py plugins/Web/test.py plugins/Web/locales/de.po plugins/Web/locales/fi.po plugins/Web/locales/fr.po plugins/Web/locales/it.po src/__init__.py src/ansi.py src/callbacks.py src/cdb.py src/commands.py src/conf.py src/dbi.py src/dynamicScope.py src/gpg.py src/httpserver.py src/i18n.py src/ircdb.py src/irclib.py src/ircmsgs.py src/ircutils.py src/log.py src/plugin.py src/questions.py src/registry.py src/schedule.py src/setup.py src/shlex.py src/test.py src/unpreserve.py src/version.py src/world.py src/drivers/Socket.py src/drivers/__init__.py src/scripts/__init__.py src/scripts/limnoria.py src/scripts/limnoria_adduser.py src/scripts/limnoria_botchk.py src/scripts/limnoria_plugin_create.py src/scripts/limnoria_plugin_doc.py src/scripts/limnoria_reset_password.py src/scripts/limnoria_test.py src/scripts/limnoria_wizard.py src/utils/__init__.py src/utils/crypt.py src/utils/error.py src/utils/file.py src/utils/gen.py src/utils/iter.py src/utils/math_evaluator.py src/utils/minisix.py src/utils/net.py src/utils/python.py src/utils/seq.py src/utils/str.py src/utils/structures.py src/utils/time.py src/utils/transaction.py src/utils/web.py test/__init__.py test/test.py test/test_callbacks.py test/test_commands.py test/test_conf.py test/test_drivers.py test/test_dynamicScope.py test/test_firewall.py test/test_format.py test/test_i18n.py test/test_ircdb.py test/test_irclib.py test/test_ircmsgs.py test/test_ircutils.py test/test_misc.py test/test_plugin.py test/test_plugin_create.py test/test_plugins.py test/test_registry.py test/test_schedule.py test/test_standardSubstitute.py test/test_utils.py test/test_yn.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131037.0 limnoria-2023.11.18/limnoria.egg-info/dependency_links.txt0000644000175000017500000000000114535072535021474 0ustar00valval ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131037.0 limnoria-2023.11.18/limnoria.egg-info/entry_points.txt0000644000175000017500000000164614535072535020733 0ustar00valval[console_scripts] limnoria = supybot.scripts.limnoria:main limnoria-adduser = supybot.scripts.limnoria_adduser:main limnoria-botchk = supybot.scripts.limnoria_botchk:main limnoria-plugin-create = supybot.scripts.limnoria_plugin_create:main limnoria-plugin-doc = supybot.scripts.limnoria_plugin_doc:main limnoria-reset-password = supybot.scripts.limnoria_reset_password:main limnoria-test = supybot.scripts.limnoria_test:main limnoria-wizard = supybot.scripts.limnoria_wizard:main supybot = supybot.scripts.limnoria:main supybot-adduser = supybot.scripts.limnoria_adduser:main supybot-botchk = supybot.scripts.limnoria_botchk:main supybot-plugin-create = supybot.scripts.limnoria_plugin_create:main supybot-plugin-doc = supybot.scripts.limnoria_plugin_doc:main supybot-reset-password = supybot.scripts.limnoria_reset_password:main supybot-test = supybot.scripts.limnoria_test:main supybot-wizard = supybot.scripts.limnoria_wizard:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131037.0 limnoria-2023.11.18/limnoria.egg-info/top_level.txt0000644000175000017500000000001014535072535020147 0ustar00valvalsupybot ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3257546 limnoria-2023.11.18/locales/0000755000175000017500000000000014535072535013544 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/__init__.py0000644000175000017500000000000014535072470015641 0ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/de.po0000644000175000017500000015154114535072470014501 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-28 18:53+0100\n" "Last-Translator: Florian Besser \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: German\n" "X-Poedit-Country: Germany\n" #: src/callbacks.py:217 msgid "Error: " msgstr "Fehler: " #: src/callbacks.py:233 msgid "Error: I tried to send you an empty message." msgstr "Fehler: Ich habe versucht eine leere Nachricht zu senden." #: src/callbacks.py:361 msgid "" "Missing \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "Fehlend \"%s\". Vielleicht solltest du deine Argumente in doppelte " "Anführungszeichen setzen um zu verhindern das eine Klammern als " "Verschachtelung gewertet werden." #: src/callbacks.py:390 msgid "" "\"|\" with nothing preceding. I obviously can't do a pipe with nothing " "before the |." msgstr "" "\"|\" mit nichts vorhergehendem. Eine pipe ohne Eingangsmaterial vor dem | " "funktioniert nicht." #: src/callbacks.py:398 msgid "" "Spurious \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "Falsche \"%s\". Du solltest Zitate in doppelte Anführungszeichen packen um " "zu verhindern, dass zusätzliche Anführungszeichen als verschachtelte Befehle " "ausgewertet werden." #: src/callbacks.py:407 msgid "" "\"|\" with nothing following. I obviously can't do a pipe with nothing " "after the |." msgstr "" "\"|\" mit nichts nachfolgendem. Eine pipe ohne Ziel nach dem | funktioniert " "nicht." #: src/callbacks.py:623 msgid "%s is not a valid %s." msgstr "%s nicht zulässig als %s" #: src/callbacks.py:625 msgid "That's not a valid %s." msgstr "%s nicht zulässig" #: src/callbacks.py:794 msgid "(XX more messages)" msgstr "(XX mehr Nachrichten)" #: src/callbacks.py:849 msgid "more message" msgstr "mehr Nachrichten" #: src/callbacks.py:851 msgid "more messages" msgstr "mehr Nachrichten" #: src/callbacks.py:986 msgid "You've attempted more nesting than is currently allowed on this bot." msgstr "" "Du hast versucht mehr zu Verschachteln als bei diesem Bot momentan erlaubt " "ist." #: src/callbacks.py:1171 msgid "" "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." msgstr "" "Der Befehl %q ist verfügbar in den %L Plugins. Bitte gebe an welches Plugin " "diesen Befehl ausführen soll, indem du es vor dem Befehl %q angibst." #: src/callbacks.py:1356 #, fuzzy msgid "" "Determines what commands are currently disabled. Such\n" " commands will not appear in command lists, etc. They will appear not " "even\n" " to exist." msgstr "" "Legt fest welche Befehle momentan abgeschaltet sind.\n" " Diese Befehle werden nicht in Befehlslisten, etc... erscheinen.\n" " Es wird den anschein haben, das diese nicht einmal existieren." #: src/callbacks.py:1595 msgid "Invalid arguments for %s." msgstr "Unzulässige Argumente für %s." #: src/callbacks.py:1627 msgid "The %q command has no help." msgstr "%q hat keine Hilfe." #: src/commands.py:284 msgid "integer" msgstr "Ganzzahl" #: src/commands.py:295 msgid "non-integer value" msgstr "Kein Ganzzahlwert" #: src/commands.py:306 msgid "floating point number" msgstr "Fließkommazahl" #: src/commands.py:315 msgid "positive integer" msgstr "positive Ganzzahl" #: src/commands.py:319 msgid "non-negative integer" msgstr "nicht negative Ganzzahl" #: src/commands.py:322 msgid "index" msgstr "Index" #: src/commands.py:347 msgid "number of seconds" msgstr "eingen Sekunden" #: src/commands.py:354 msgid "boolean" msgstr "Boolean" #: src/commands.py:368 src/commands.py:375 src/commands.py:384 #: src/commands.py:391 src/commands.py:400 msgid "do that" msgstr "tu dass" #: src/commands.py:371 src/commands.py:378 src/commands.py:387 #: src/commands.py:394 src/commands.py:403 msgid "I'm not even in %s." msgstr "Ich bin nicht mal in %s." #: src/commands.py:373 #, fuzzy msgid "I need to be voiced to %s." msgstr "Ich benötige Op zu %s." #: src/commands.py:381 #, fuzzy msgid "I need to be at least voiced to %s." msgstr "Ich benötige Op zu %s." #: src/commands.py:389 #, fuzzy msgid "I need to be halfopped to %s." msgstr "Ich benötige Op zu %s." #: src/commands.py:397 #, fuzzy msgid "I need to be at least halfopped to %s." msgstr "Ich benötige Op zu %s." #: src/commands.py:405 msgid "I need to be opped to %s." msgstr "Ich benötige Op zu %s." #: src/commands.py:411 src/commands.py:569 msgid "channel" msgstr "Kanal" #: src/commands.py:424 msgid "nick or hostmask" msgstr "Nick oder Hostmaske" #: src/commands.py:478 msgid "regular expression" msgstr "Regulärer Ausdruck" #: src/commands.py:489 src/commands.py:493 msgid "nick" msgstr "" #: src/commands.py:490 msgid "That nick is too long for this server." msgstr "Dieser Nick ist zu lang für den Server." #: src/commands.py:501 #, fuzzy msgid "I haven't seen %s." msgstr "Ich bin nicht mal in %s." #: src/commands.py:548 src/commands.py:567 msgid "I'm not in %s." msgstr "Ich bin nicht in %s." #: src/commands.py:552 #, fuzzy msgid "This command may only be given in a channel that I am in." msgstr "" "Dieser Befehl kann mir nur in einem Kanal gegeben werden in dem ich bin." #: src/commands.py:565 msgid "You must be in %s." msgstr "Du musst in %s sein." #: src/commands.py:576 msgid "%s is not in %s." msgstr "%s ist nicht in %s." #: src/commands.py:624 #, fuzzy msgid "You must not give the empty string as an argument." msgstr "Du darfst keinen leeren String als Argument angeben." #: src/commands.py:632 #, fuzzy msgid "You must not give a string containing spaces as an argument." msgstr "Du darfst keinen leeren String als Argument angeben." #: src/commands.py:642 msgid "This message must be sent in a channel." msgstr "Diese Nachricht muss in einem Kanal gesendet werden." #: src/commands.py:674 src/commands.py:681 msgid "url" msgstr "" #: src/commands.py:687 msgid "IRI" msgstr "" #: src/commands.py:693 msgid "email" msgstr "" #: src/commands.py:703 src/commands.py:711 msgid "http url" msgstr "HTTP URL" #: src/commands.py:718 msgid "command name" msgstr "Befehlsname" #: src/commands.py:726 msgid "ip" msgstr "IP" #: src/commands.py:732 msgid "letter" msgstr "Buchstabe" #: src/commands.py:764 msgid "plugin" msgstr "Plugin" #: src/commands.py:772 msgid "irc color" msgstr "IRC Farbe" #: src/conf.py:130 #, fuzzy msgid "" "Determines whether this plugin is loaded\n" " by default." msgstr "" "Legt fest ob das Plugin standardgemäß\n" " geladen wird" #: src/conf.py:134 msgid "" "Determines whether this plugin is\n" " publicly visible." msgstr "" "Legt fest ob das Plugin öffentlich\n" " zu sehen ist" #: src/conf.py:231 msgid "Determines the bot's default nick." msgstr "Legt den Standardnick des Bots fest." #: src/conf.py:234 #, fuzzy msgid "" "Determines what alternative\n" " nicks will be used if the primary nick (supybot.nick) isn't available. " "A\n" " %s in this nick is replaced by the value of supybot.nick when used. If " "no\n" " alternates are given, or if all are used, the supybot.nick will be " "perturbed\n" " appropriately until an unused nick is found." msgstr "" "Legt fest welche alternativ Nicknamen der Bot benutzt, falls der Primärnick " "(supybot.nick) nicht verfügbar ist.\n" " Ein %s in diesem Nicknamen wird durch den Wert von supybot.nick ersetzt. " "Falls kein alternativ Nickname angegeben wird oder alle bereits vergeben " "sind, wird supybot.nick verändert bis ein unbenuzter Nickname gefunden wurde." #: src/conf.py:241 msgid "" "Determines the bot's ident string, if the server\n" " doesn't provide one by default." msgstr "" "Legt die Ident Zeichenkette des Bots fest, falls der Server keine vergibt." #: src/conf.py:258 #, fuzzy msgid "" "Determines the real name which the bot sends to\n" " the server. A standard real name using the current version of the bot\n" " will be generated if this is left empty." msgstr "" "Legt den User fest den der Bot an den Server sendet. Er wird die momentane " "Version des Bots als User benutzen, falls diese Variable leer bleibt." #: src/conf.py:267 msgid "Determines what networks the bot will connect to." msgstr "Legt fest zu welchen Netzwerken der Bot sich verbindet." #: src/conf.py:377 msgid "" "Determines what password will be used on %s. Yes, we know that\n" " technically passwords are server-specific and not network-specific,\n" " but this is the best we can do right now." msgstr "" "Legt fest welches Passwort für %s genutzt wird. Ja wir wissen das Passwörter " "nicht Netzwerk spezifisch sondern Server spezifisch sind , aber das ist das " "beste was wir momentan anbieten können." #: src/conf.py:381 #, fuzzy msgid "" "Space-separated list of servers the bot will connect to for %s.\n" " Each will be tried in order, wrapping back to the first when the " "cycle\n" " is completed." msgstr "" "Legt fest zu welchem Server der Bot für %s verbindet. Jeder Server wird in " "der Reihenfolge probiert, falls der letzte Server erreicht ist, wird wieder " "beim Ersten angefangen." #: src/conf.py:385 #, fuzzy msgid "Space-separated list of channels the bot will join only on %s." msgstr "Legt fest welche Kanäle der Bot nur bei %s betritt." #: src/conf.py:389 #, fuzzy msgid "" "Determines whether the bot will attempt to connect with SSL\n" " sockets to %s." msgstr "" "Legt fest ob der Bot versuchen soll per SSL Sockets zu %s zu verbinden." #: src/conf.py:392 msgid "" "Space-separated list\n" " of fingerprints of trusted certificates for this network.\n" " Supported hash algorithms are: %L.\n" " If non-empty, Certification Authority signatures will not be used " "to\n" " verify certificates." msgstr "" #: src/conf.py:398 msgid "" "A certificate that is trusted to verify\n" " certificates of this network (aka. Certificate Authority)." msgstr "" #: src/conf.py:401 msgid "Deprecated config value, keep it to False." msgstr "" #: src/conf.py:404 #, fuzzy msgid "" "Determines what certificate file (if any) the bot will use to\n" " connect with SSL sockets to %s." msgstr "" "Legt fest ob der Bot versuchen soll per SSL Sockets zu %s zu verbinden." #: src/conf.py:407 msgid "" "Determines what key (if any) will be used to join the\n" " channel." msgstr "" "Legt den Schlüssel fest, den der Bot benutzt um einen Kanal zu betreten " "(falls vorhanden)." #: src/conf.py:409 msgid "" "Determines\n" " what nick the bot will use on this network. If empty, defaults to\n" " supybot.nick." msgstr "" #: src/conf.py:412 #, fuzzy msgid "" "Determines\n" " the bot's ident string, if the server doesn't provide one by " "default.\n" " If empty, defaults to supybot.ident." msgstr "" "Legt die Ident Zeichenkette des Bots fest, falls der Server keine vergibt." #: src/conf.py:415 msgid "" "Determines\n" " the real name which the bot sends to the server. If empty, defaults " "to\n" " supybot.user" msgstr "" #: src/conf.py:419 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. If empty, defaults to\n" " supybot.protocols.irc.umodes" msgstr "" #: src/conf.py:424 #, fuzzy msgid "" "Determines what SASL username will be used on %s. This should\n" " be the bot's account name." msgstr "" "Legt den Schlüssel fest, den der Bot benutzt um einen Kanal zu betreten " "(falls vorhanden)." #: src/conf.py:427 #, fuzzy msgid "Determines what SASL password will be used on %s." msgstr "Legt fest welche Plugins geladen werden." #: src/conf.py:430 msgid "" "Determines what SASL ECDSA key (if any) will be used on %s.\n" " The public key must be registered with NickServ for SASL\n" " ECDSA-NIST256P-CHALLENGE to work." msgstr "" #: src/conf.py:435 #, fuzzy msgid "" "Determines what SASL mechanisms will be tried and in which order.\n" " " msgstr "" "Legt den Schlüssel fest, den der Bot benutzt um einen Kanal zu betreten " "(falls vorhanden)." #: src/conf.py:438 #, fuzzy msgid "" "Determines whether the bot will abort the connection if the\n" " none of the enabled SASL mechanism succeeded." msgstr "" "Legt fest ob der Bot versuchen soll per SSL Sockets zu %s zu verbinden." #: src/conf.py:441 msgid "" "If not empty, determines the hostname:port of the socks proxy that\n" " will be used to connect to this network." msgstr "" #: src/conf.py:461 #, fuzzy msgid "Determines how urls should be formatted." msgstr "Legt fest welche Plugins geladen werden." #: src/conf.py:469 msgid "" "Determines how timestamps\n" " printed for human reading should be formatted. Refer to the Python\n" " documentation for the time module to see valid formatting characters " "for\n" " time formats." msgstr "" "Legt fest wie Zeitstempel für das menschliche Lesen formattiert werden " "sollen. Schlag in der Python Dokumentation das time Modul nach um gültige " "Zeichen zu erfahren." #: src/conf.py:484 msgid "" "Determines whether elapsed times will be given\n" " as \"1 day, 2 hours, 3 minutes, and 15 seconds\" or as \"1d 2h 3m 15s\"." msgstr "" "Legt fest wie die vergangene Zeit angeben wird, als \"1 Tag, 2 Stunden, 3 " "Minuten und 15 Sekunden\" oder als \"1d 2h 3m 15s\"" #: src/conf.py:495 msgid "" "Maximum number of items in a list\n" " before the end is replaced with 'and others'. Set to 0 to always\n" " show the entire list." msgstr "" #: src/conf.py:512 msgid "other" msgstr "" #: src/conf.py:517 msgid "" "Determines the absolute maximum length of\n" " the bot's reply -- no reply will be passed through the bot with a " "length\n" " greater than this." msgstr "" "Legt die absolut maximale Länge für Bot-Antworten fest. Keine Antwort länger " "als dieser Wert wird vom Bot zugelassen." #: src/conf.py:522 msgid "" "Determines whether the bot will break up long\n" " messages into chunks and allow users to use the 'more' command to get " "the\n" " remaining chunks." msgstr "" "Legt fest ob der Bot lange Nachrichten in mehrere Stücke unterteilt und der " "Nutzer mit dem more-Befehl die restlichen Stücke erhalten kann." #: src/conf.py:527 msgid "" "Determines what the maximum number of\n" " chunks (for use with the 'more' command) will be." msgstr "" "Legt die maximal Anzahl an Teilen fest (für die Nutzung mit dem 'more' " "Befehl)" #: src/conf.py:531 msgid "" "Determines how long individual chunks\n" " will be. If set to 0, uses our super-tweaked,\n" " get-the-most-out-of-an-individual-message default." msgstr "" "Legt fest wie lang einzelne Stücke sein werden. Beim Wert 0 wird unser super-" "getunter Nutze-alles-was-aus-einzelnen-Nachrichten-machbar-ist-Standard " "verwendet." #: src/conf.py:536 msgid "" "Determines how many mores will be sent\n" " instantly (i.e., without the use of the more command, immediately when\n" " they are formed). Defaults to 1, which means that a more command will " "be\n" " required for all but the first chunk." msgstr "" #: src/conf.py:542 msgid "" "Determines whether the bot will send\n" " multi-message replies in a single message. This defaults to True \n" " in order to prevent the bot from flooding. If this is set to False\n" " the bot will send multi-message replies on multiple lines." msgstr "" #: src/conf.py:548 msgid "" "Determines whether the bot will reply with an\n" " error message when it is addressed but not given a valid command. If " "this\n" " value is False, the bot will remain silent, as long as no other plugins\n" " override the normal behavior." msgstr "" #: src/conf.py:555 msgid "" "Determines whether error messages that result\n" " from bugs in the bot will show a detailed error message (the uncaught\n" " exception) or a generic error message." msgstr "" #: src/conf.py:559 msgid "" "Determines whether the bot will send error\n" " messages to users in private. You might want to do this in order to " "keep\n" " channel traffic to minimum. This can be used in combination with\n" " supybot.reply.error.withNotice." msgstr "" #: src/conf.py:564 msgid "" "Determines whether the bot will send error\n" " messages to users via NOTICE instead of PRIVMSG. You might want to do " "this\n" " so users can ignore NOTICEs from the bot and not have to see error\n" " messages; or you might want to use it in combination with\n" " supybot.reply.error.inPrivate so private errors don't open a query " "window\n" " in most IRC clients." msgstr "" #: src/conf.py:571 msgid "" "Determines whether the bot will *not* provide\n" " details in the error\n" " message to users who attempt to call a command for which they do not " "have\n" " the necessary capability. You may wish to make this True if you don't " "want\n" " users to understand the underlying security system preventing them from\n" " running certain commands." msgstr "" #: src/conf.py:579 msgid "" "Determines whether the bot will reply\n" " privately when replying in a channel, rather than replying to the " "whole\n" " channel." msgstr "" #: src/conf.py:584 msgid "" "Determines whether the bot will reply with a\n" " notice when replying in a channel, rather than replying with a privmsg " "as\n" " normal." msgstr "" #: src/conf.py:590 msgid "" "Determines whether the bot will reply with a\n" " notice when it is sending a private message, in order not to open a /" "query\n" " window in clients." msgstr "" #: src/conf.py:595 #, fuzzy msgid "" "Determines whether the bot will always prefix\n" " the user's nick to its reply to that user's command." msgstr "Legt fest ob der Bot automatisch alles als Befehl behandeln soll." #: src/conf.py:599 msgid "" "Determines whether the bot should attempt to\n" " reply to all messages even if they don't address it (either via its " "nick\n" " or a prefix character). If you set this to True, you almost certainly " "want\n" " to set supybot.reply.whenNotCommand to False." msgstr "" #: src/conf.py:605 msgid "" "Determines whether the bot will allow you to\n" " send channel-related commands outside of that channel. Sometimes " "people\n" " find it confusing if a channel-related command (like Filter.outfilter)\n" " changes the behavior of the channel but was sent outside the channel\n" " itself." msgstr "" #: src/conf.py:612 msgid "" "Determines whether the bot will unidentify\n" " someone when that person changes their nick. Setting this to True\n" " will cause the bot to track such changes. It defaults to False for a\n" " little greater security." msgstr "" #: src/conf.py:618 msgid "" "Determines whether the bot will always join a\n" " channel when it's invited. If this value is False, the bot will only " "join\n" " a channel if the user inviting it has the 'admin' capability (or if " "it's\n" " explicitly told to join the channel using the Admin.join command)." msgstr "" #: src/conf.py:624 msgid "" "Supybot normally replies with the full help\n" " whenever a user misuses a command. If this value is set to True, the " "bot\n" " will only reply with the syntax of the command (the first line of the\n" " help) rather than the full help." msgstr "" #: src/conf.py:639 msgid "" "Determines what prefix characters the bot will\n" " reply to. A prefix character is a single character that the bot will " "use\n" " to determine what messages are addressed to it; when there are no " "prefix\n" " characters set, it just uses its nick. Each character in this string " "is\n" " interpreted individually; you can have multiple prefix chars\n" " simultaneously, and if any one of them is used as a prefix the bot will\n" " assume it is being addressed." msgstr "" #: src/conf.py:648 msgid "" "Determines what strings the\n" " bot will reply to when they are at the beginning of the message. " "Whereas\n" " prefix.chars can only be one character (although there can be many of\n" " them), this variable is a space-separated list of strings, so you can\n" " set something like '@@ ??' and the bot will reply when a message is\n" " prefixed by either @@ or ??." msgstr "" #: src/conf.py:655 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick, rather than with a prefix character." msgstr "" #: src/conf.py:658 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick at the end of the message, rather than at\n" " the beginning." msgstr "" #: src/conf.py:662 msgid "" "Determines what extra nicks\n" " the bot will always respond to when addressed by, even if its current " "nick\n" " is something else." msgstr "" #: src/conf.py:672 msgid "The operation succeeded." msgstr "Anweisung erfolgreich." #: src/conf.py:673 msgid "" "Determines what message the bot replies with when a command succeeded.\n" " If this configuration variable is empty, no success message will be\n" " sent." msgstr "" "Legt fest mit welcher Nachricht der Bot antwortet, wenn ein Befehl " "erfolgreich war. Falls diese Konfigurationsvariable leer ist wird keine " "Erfolgsmeldung gesendet." #: src/conf.py:678 msgid "" "An error has occurred and has been logged.\n" " Please contact this bot's administrator for more information." msgstr "" "Ein Fehler ist aufgetreten und wurde gelogged.\n" " Bitte kontaktiere den Botadministartor für mehr Informationen." #: src/conf.py:679 msgid "" "\n" " Determines what error message the bot gives when it wants to be\n" " ambiguous." msgstr "" "\n" "Bestimmt welche Fehlermeldung der Bot ausgibt, wenn er zweideutig sein " "möchte." #: src/conf.py:684 #, fuzzy msgid "" "An error has occurred and has been logged.\n" " Check the logs for more information." msgstr "" "Ein Fehler ist augetreten und wurde protokolliert.\n" " Überprüfe die Protokolldateien." #: src/conf.py:685 msgid "" "Determines what error\n" " message the bot gives to the owner when it wants to be ambiguous." msgstr "" "Bestimmt welche Fehlermeldung der Bot an den Besitzer ausgibt, wenn er " "zweideutig sein möchte." #: src/conf.py:689 msgid "" "Your hostmask doesn't match or your password\n" " is wrong." msgstr "" "Deine Hostmaske passt nicht oder dein Passwort\n" " ist falsch." #: src/conf.py:690 msgid "" "Determines what message the bot replies with when\n" " someone tries to use a command that requires being identified or having " "a\n" " password and neither credential is correct." msgstr "" "Bestimmt welche Fehlermeldung der Bot ausgibt, wenn jemand einen Befehl " "versucht der es erforderlich macht sich identifiziert zu haben oder korrekte " "Anmeldedaten zu haben." #: src/conf.py:696 msgid "" "I can't find %s in my user\n" " database. If you didn't give a user name, then I might not know what " "your\n" " user is, and you'll need to identify before this command might work." msgstr "" "Ich konnte %s nicht in meiner Benutzerdatenbank finden. Falls du keinen " "Benutzernamen angegeben hast, weiß ich vielleicht nicht wer du bist unnd du " "mich dich identifizieren bevor dieser Befehl funktioniert." #: src/conf.py:699 msgid "" "Determines what error message the bot replies with when someone tries\n" " to accessing some information on a user the bot doesn't know about." msgstr "" "Bestimmt welche Fehlermeldung der Bot ausgibt, wenn jemand versucht auf " "Informationen zuzugreifen über die der Bot nichts weiß." #: src/conf.py:703 msgid "" "You must be registered to use this command.\n" " If you are already registered, you must either identify (using the " "identify\n" " command) or add a hostmask matching your current hostmask (using the\n" " \"hostmask add\" command)." msgstr "" "Du musst registiert sein um diesen Befehl benutzen zu können. Wenn du " "bereits registriert bist musst du dich identifizieren (durch den identify " "Befehl) oder eine Hostmaske hinzufügen die zu deiner momentanen Hostmaske " "passt (durch den \"hostmask add\" Befehl)." #: src/conf.py:706 msgid "" "Determines what error message the bot\n" " replies with when someone tries to do something that requires them to " "be\n" " registered but they're not currently recognized." msgstr "" "Bestimmt welche Fehlermeldung der Bot antortet wenn jemand etwas versucht, " "das es erforderlich macht registriert zu sein, es aber nicht ist." #: src/conf.py:711 msgid "" "You don't have the %s capability. If you\n" " think that you should have this capability, be sure that you are " "identified\n" " before trying again. The 'whoami' command can tell you if you're\n" " identified." msgstr "" "Du hast die %s Fähigkeit nicht. Falls du denkst du solltest diese haben, sei " "dir sicher das du identifiziert bist, bevor du es erneurt versuchst. Der " "Befehl 'whoami' kann dir sagen ob du identifiziert bist." #: src/conf.py:714 msgid "" "Determines what error message is given when the bot\n" " is telling someone they aren't cool enough to use the command they tried " "to\n" " use." msgstr "" "Bestimmt welche Fehlermeldung der Bot einer Person ausgibt, die nicht cool " "genug ist den Befehl auszuführen." #: src/conf.py:719 msgid "" "You're missing some capability you need.\n" " This could be because you actually possess the anti-capability for the\n" " capability that's required of you, or because the channel provides that\n" " anti-capability by default, or because the global capabilities include\n" " that anti-capability. Or, it could be because the channel or\n" " supybot.capabilities.default is set to False, meaning that no commands " "are\n" " allowed unless explicitly in your capabilities. Either way, you can't " "do\n" " what you want to do." msgstr "" "Dir fehlen ein paar Fähigkeiten. Es könnte sein, dass du die Anti-Fähigkeit " "für diese Fähigkeit besitzt oder der Channel diese Anti-Fähigkeit als " "Standard bereitstellt oder die globalen Anti-Fähigkeiten diese als Standard " "bereitstellen. Es könnte auch sein, dass der Kanal oder supybot.capabilities." "default auf False eingestellt sind, bedeutet es sind keinerlei Befehle " "erlaubt ausgenommen der explizit zugestandenen mittels Fähigkeiten. Wie dem " "auch sei, das was du versuchst funktioniert gerade nicht." #: src/conf.py:727 msgid "" "Determines what generic error message is given when the bot is telling\n" " someone that they aren't cool enough to use the command they tried to " "use,\n" " and the author of the code calling errorNoCapability didn't provide an\n" " explicit capability for whatever reason." msgstr "" "Bestimt welche generische Fehlernachricht der Bot einer Person ausgibt, die " "nicht cool genug ist den Befehl auszuführen den sie versucht hat und der " "Autor des Codes hat keine bestimmte Fähigkeit dafür bereitgestellt." #: src/conf.py:733 #, fuzzy msgid "" "That operation cannot be done in a\n" " channel." msgstr "Diese Anweisung kann nicht in einem Kanal gegeben werden." #: src/conf.py:734 msgid "" "Determines what error messages the bot sends to people\n" " who try to do things in a channel that really should be done in\n" " private." msgstr "" "Bestimmt welche Fehlermeldung der Bot an Leute schickt, die Dinge im Kanal " "versuchen, welche jedoch besser per privater Nachricht getan werden sollten." #: src/conf.py:739 #, fuzzy msgid "" "This may be a bug. If you think it is,\n" " please file a bug report at\n" " ." msgstr "" "Das könnte ein Programmierfehler sein. Falls du das denkst, melden diesen " "Fehler bitte bei ." #: src/conf.py:742 msgid "" "Determines what message the bot sends when it thinks you've\n" " encountered a bug that the developers don't know about." msgstr "" "Bestimmt welche Meldung der Bot schickt, wenn er glaubt, dass du einen " "Programmierfehler gefunden hast von dem die Entwickler nichts wissen." #: src/conf.py:749 msgid "$Type #$id: $text (added by $username at $at)" msgstr "" #: src/conf.py:750 msgid "" "Format used by generic database plugins (Lart, Dunno, Prase, Success,\n" " Quote, ...) to show an entry. You can use the following variables:\n" " $type/$types/$Type/$Types (plugin name and variants), $id, $text,\n" " $at (creation time), $userid/$username/$nick (author)." msgstr "" #: src/conf.py:760 msgid "" "A floating point number of seconds to throttle\n" " snarfed URLs, in order to prevent loops between two bots snarfing the " "same\n" " URLs and having the snarfed URL in the output of the snarf message." msgstr "" "Eine Fließkommazahl in Sekunden um das Erschnüffeln von URLs zu bremsen. " "Notwendig um zu verhindern, dass zum einen zwei Bots die gleichen URLs " "erschnüffeln und zum anderen die erschnüffelte URL in der Ausgabe der " "erschnüffelten Nachricht zu haben." #: src/conf.py:765 msgid "" "Determines the number of seconds\n" " between running the upkeep function that flushes (commits) open " "databases,\n" " collects garbage, and records some useful statistics at the debugging\n" " level." msgstr "" "Bestimmt die Anzahl der Sekunden zwischen einem Durchlauf der upkeep-" "Funktion, die offene in die Datenbank schreibt, Müll sammelt und ein paar " "nützliche Statistiken zum Zwecke der Fehlersuche sammelt." #: src/conf.py:771 msgid "" "Determines whether the bot will periodically\n" " flush data and configuration files to disk. Generally, the only time\n" " you'll want to set this to False is when you want to modify those\n" " configuration files by hand and don't want the bot to flush its current\n" " version over your modifications. Do note that if you change this to " "False\n" " inside the bot, your changes won't be flushed. To make this change\n" " permanent, you must edit the registry yourself." msgstr "" "Bestimmt ob der Bot perodisch alle Daten auf die Festplatte schreibt. " "Generell sollte das nur auf False stehen wenn du die Konfigurationsdateien " "von Hand ändern und verhindern willst, dass der Bot deine Änderungen " "überschreibt. Wenn du diesen Wert auf False änderst bedenke, dass deine " "Änderungen innerhalb des Bots nicht gespeichert werden. Um diese Änderungen " "permanent zu machen musst du die Registrierung händisch anpassen." #: src/conf.py:797 msgid "" "Determines what characters are valid for quoting\n" " arguments to commands in order to prevent them from being tokenized.\n" " " msgstr "" "Bestimmt welche Zeichen für das Zitieren von Argumenten an Befehle gültig " "sind um zu verhindern, dass diese in Tokens übersetzt werden." #: src/conf.py:804 msgid "" "Determines whether the bot will allow nested\n" " commands, which rule. You definitely should keep this on." msgstr "" #: src/conf.py:807 msgid "" "Determines what the maximum number of\n" " nested commands will be; users will receive an error if they attempt\n" " commands more nested than this." msgstr "" #: src/conf.py:816 msgid "" "Supybot allows you to specify what brackets\n" " are used for your nested commands. Valid sets of brackets include\n" " [], <>, {}, and (). [] has strong historical motivation, but <> or\n" " () might be slightly superior because they cannot occur in a nick.\n" " If this string is empty, nested commands will not be allowed in this\n" " channel." msgstr "" #: src/conf.py:823 msgid "" "Supybot allows nested commands. Enabling this\n" " option will allow nested commands with a syntax similar to UNIX pipes, " "for\n" " example: 'bot: foo | bar'." msgstr "" "Supybot erlaubt verschachtelte Befehle. Wenn man diese Option aktiviert wird " "der Bot verschachtelte Befehle erlauben, mit einer Syntax die ähnlich ist " "wie UNIX Pipes, zum Beispiel 'bot: foo | bar'." #: src/conf.py:828 msgid "" "Determines what commands have default\n" " plugins set, and which plugins are set to be the default for each of " "those\n" " commands." msgstr "" #: src/conf.py:834 msgid "" "Determines what plugins automatically get precedence over all\n" " other plugins when selecting a default plugin for a command. By\n" " default, this includes the standard loaded plugins. You probably\n" " shouldn't change this if you don't know what you're doing; if you " "do\n" " know what you're doing, then also know that this set is\n" " case-sensitive." msgstr "" #: src/conf.py:844 msgid "" "Allows this bot's owner user to use commands\n" " that grants them shell access. This config variable exists in case you " "want\n" " to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)\n" " from gaining shell access to the bot's server by impersonating the " "owner.\n" " Setting this to False also disables plugins and commands that can be\n" " used to indirectly gain shell access." msgstr "" #: src/conf.py:859 msgid "" "Determines the interval used for\n" " the history storage." msgstr "" #: src/conf.py:862 msgid "" "Determines whether the bot will defend itself\n" " against command-flooding." msgstr "" #: src/conf.py:865 msgid "" "Determines how many commands users are\n" " allowed per minute. If a user sends more than this many commands in " "any\n" " 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.punishment seconds." msgstr "" #: src/conf.py:870 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with commands." msgstr "" #: src/conf.py:873 #, fuzzy msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for command flooding." msgstr "Legt fest ob der Bot automatisch alles als Befehl behandeln soll." #: src/conf.py:877 msgid "" "Determines whether the bot will defend itself\n" " against invalid command-flooding." msgstr "" #: src/conf.py:880 msgid "" "Determines how many invalid commands users\n" " are allowed per minute. If a user sends more than this many invalid\n" " commands in any 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.invalid.punishment seconds. Typically, " "this\n" " value is lower than supybot.abuse.flood.command.maximum, since it's far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" #: src/conf.py:888 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with invalid commands. Typically, this\n" " value is higher than supybot.abuse.flood.command.punishment, since it's " "far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" #: src/conf.py:894 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for invalid command flooding." msgstr "" #: src/conf.py:903 msgid "" "Determines the default length of time a\n" " driver should block waiting for input." msgstr "" #: src/conf.py:911 msgid "" "Determines what driver module the \n" " bot will use. Current, the only (and default) driver is Socket." msgstr "" #: src/conf.py:915 msgid "" "Determines the maximum time the bot will\n" " wait before attempting to reconnect to an IRC server. The bot may, of\n" " course, reconnect earlier if possible." msgstr "" #: src/conf.py:967 msgid "" "Determines what directory configuration data is\n" " put into." msgstr "" "Legt fest in welchem Verzeichnis Konfigurationsdaten gespeichert werden." #: src/conf.py:970 msgid "Determines what directory data is put into." msgstr "Legt fest in welchem Verzeichnis Daten gespeichert werden." #: src/conf.py:972 msgid "" "Determines what directory backup data is put\n" " into. Set it to /dev/null to disable backup (it is a special value,\n" " so it also works on Windows and systems without /dev/null)." msgstr "" #: src/conf.py:979 msgid "" "Determines what directory temporary files\n" " are put into." msgstr "Legt fest in welchem Verzeichnis temporäre Dateien gespeichert werden." #: src/conf.py:982 #, fuzzy msgid "" "Determines what directory files of the\n" " web server (templates, custom images, ...) are put into." msgstr "Legt fest in welchem Verzeichnis temporäre Dateien gespeichert werden." #: src/conf.py:995 msgid "" "Determines what directories\n" " the bot will look for plugins in. Accepts a comma-separated list of\n" " strings.\n" " This means that to add another directory, you can nest the former value " "and\n" " add a new one. E.g. you can say: bot: 'config supybot.directories." "plugins\n" " [config supybot.directories.plugins], newPluginDirectory'." msgstr "" #: src/conf.py:1003 msgid "" "List of all plugins that were\n" " ever loaded. Currently has no effect whatsoever. You probably want to " "use\n" " the 'load' or 'unload' commands, or edit supybot.plugins.\n" " instead of this." msgstr "" #: src/conf.py:1008 msgid "" "Determines whether the bot will always load\n" " important plugins (Admin, Channel, Config, Misc, Owner, and User)\n" " regardless of what their configured state is. Generally, if these " "plugins\n" " are configured not to load, you didn't do it on purpose, and you still\n" " want them to load. Users who don't want to load these plugins are " "smart\n" " enough to change the value of this variable appropriately :)" msgstr "" #: src/conf.py:1036 msgid "" "Determines what databases are available for use. If this\n" " value is not configured (that is, if its value is empty) then sane " "defaults\n" " will be provided." msgstr "" #: src/conf.py:1042 msgid "" "Determines what filename will be used\n" " for the users database. This file will go into the directory specified " "by\n" " the supybot.directories.conf variable." msgstr "" #: src/conf.py:1046 msgid "" "Determines how long it takes identification to\n" " time out. If the value is less than or equal to zero, identification " "never\n" " times out." msgstr "" #: src/conf.py:1050 msgid "" "Determines whether the bot will allow users to\n" " unregister their users. This can wreak havoc with already-existing\n" " databases, so by default we don't allow it. Enable this at your own " "risk.\n" " (Do also note that this does not prevent the owner of the bot from " "using\n" " the unregister command.)\n" " " msgstr "" #: src/conf.py:1059 msgid "" "Determines what filename will be used\n" " for the ignores database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" #: src/conf.py:1065 msgid "" "Determines what filename will be used\n" " for the channels database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" #: src/conf.py:1071 msgid "" "Determines what filename will be used\n" " for the networks database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" #: src/conf.py:1103 msgid "" "Determines whether the bot will require user\n" " registration to use 'add' commands in database-based Supybot\n" " plugins." msgstr "" #: src/conf.py:1107 msgid "" "Determines whether database-based plugins that\n" " can be channel-specific will be so. This can be overridden by " "individual\n" " channels. Do note that the bot needs to be restarted immediately after\n" " changing this variable or your db plugins may not work for your " "channel;\n" " also note that you may wish to set\n" " supybot.databases.plugins.channelSpecific.link appropriately if you " "wish\n" " to share a certain channel's databases globally." msgstr "" #: src/conf.py:1115 msgid "" "Determines what channel global\n" " (non-channel-specific) databases will be considered a part of. This is\n" " helpful if you've been running channel-specific for awhile and want to " "turn\n" " the databases for your primary channel into global databases. If\n" " supybot.databases.plugins.channelSpecific.link.allow prevents linking, " "the\n" " current channel will be used. Do note that the bot needs to be " "restarted\n" " immediately after changing this variable or your db plugins may not " "work\n" " for your channel." msgstr "" #: src/conf.py:1124 msgid "" "Determines whether another channel's global\n" " (non-channel-specific) databases will be allowed to link to this " "channel's\n" " databases. Do note that the bot needs to be restarted immediately " "after\n" " changing this variable or your db plugins may not work for your " "channel.\n" " " msgstr "" #: src/conf.py:1142 msgid "" "Determines\n" " whether CDB databases will be allowed as a database implementation." msgstr "" #: src/conf.py:1145 msgid "" "Determines how often CDB databases will have\n" " their modifications flushed to disk. When the number of modified " "records\n" " is greater than this fraction of the total number of records, the " "database\n" " will be entirely flushed to disk." msgstr "" #: src/conf.py:1237 msgid "" "Determines what will be used as the\n" " default banmask style." msgstr "Legt fest was als Standard Banmaskenstil verwendet wird." #: src/conf.py:1241 msgid "" "Determines whether the bot will strictly\n" " follow the RFC; currently this only affects what strings are\n" " considered to be nicks. If you're using a server or a network that\n" " requires you to message a nick such as services@this.network.server\n" " then you you should set this to False." msgstr "" #: src/conf.py:1248 msgid "" "Determines whether the bot will enable\n" " draft/experimental extensions of the IRC protocol. Setting this to True\n" " may break your bot at any time without warning and/or break your\n" " configuration irreversibly. So keep it False unless you know what you " "are\n" " doing." msgstr "" #: src/conf.py:1255 #, fuzzy msgid "" "Determines what certificate file (if any) the bot\n" " will use connect with SSL sockets by default." msgstr "" "Legt fest ob der Bot versuchen soll per SSL Sockets zu %s zu verbinden." #: src/conf.py:1259 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. Many people might choose +i; " "some\n" " networks allow +x, which indicates to the auth services on those " "networks\n" " that you should be given a fake host." msgstr "" #: src/conf.py:1265 msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv4." msgstr "" #: src/conf.py:1269 msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv6." msgstr "" #: src/conf.py:1273 msgid "" "Determines how many old messages the bot will\n" " keep around in its history. Changing this variable will not take " "effect\n" " on a network until it is reconnected." msgstr "" #: src/conf.py:1278 msgid "" "A floating point number of seconds to throttle\n" " queued messages -- that is, messages will not be sent faster than once " "per\n" " throttleTime seconds." msgstr "" #: src/conf.py:1283 msgid "" "Determines whether the bot will send PINGs to\n" " the server it's connected to in order to keep the connection alive and\n" " discover earlier when it breaks. Really, this option only exists for\n" " debugging purposes: you always should make it True unless you're " "testing\n" " some strange server issues." msgstr "" #: src/conf.py:1290 msgid "" "Determines the number of seconds between sending\n" " pings to the server, if pings are being sent to the server." msgstr "" #: src/conf.py:1295 msgid "" "Determines whether the bot will refuse\n" " duplicated messages to be queued for delivery to the server. This is a\n" " safety mechanism put in place to prevent plugins from sending the same\n" " message multiple times; most of the time it doesn't matter, unless " "you're\n" " doing certain kinds of plugin hacking." msgstr "" #: src/conf.py:1303 msgid "" "Determines how many seconds must elapse between\n" " JOINs sent to the server." msgstr "" "Legt fest wieviel Zeit, zwischen den JOINs die an den Server gesendet " "werden, vergehen muss." #: src/conf.py:1311 msgid "" "Determines how many bytes the bot will\n" " 'peek' at when looking through a URL for a doctype or title or " "something\n" " similar. It'll give up after it reads this many bytes, even if it " "hasn't\n" " found what it was looking for." msgstr "" #: src/conf.py:1335 msgid "" "Determines what HTTP proxy all HTTP requests should go\n" " through. The value should be of the form 'host:port'." msgstr "" #: src/conf.py:1375 msgid "" "If set, the Accept-Language HTTP header will be set to this\n" " value for requests. Useful for overriding the auto-detected language " "based on\n" " the server's location." msgstr "" #: src/conf.py:1381 msgid "" "If set, the User-Agent HTTP header will be set to a randomly\n" " selected value from this comma-separated list of strings for requests." msgstr "" #: src/conf.py:1389 msgid "" "Determines whether server certificates\n" " will be verified, which checks whether the server certificate is signed\n" " by a known certificate authority, and aborts the connection if it is " "not.\n" " This is assumed to be True of serverFingerprints or " "authorityCertificate\n" " is set." msgstr "" #: src/conf.py:1416 msgid "" "If true, uses IPV6_V6ONLY to disable\n" " forwaring of IPv4 traffic to IPv6 sockets. On *nix, has the same\n" " effect as setting kernel variable net.ipv6.bindv6only to 1." msgstr "" #: src/conf.py:1420 #, fuzzy msgid "" "Space-separated list of IPv4 hosts the HTTP server\n" " will bind." msgstr "Legt fest an welchen host sich der HTTP Server bindet." #: src/conf.py:1423 #, fuzzy msgid "" "Space-separated list of IPv6 hosts the HTTP server will\n" " bind." msgstr "Legt den Port fest an den der HTTP Server bindet." #: src/conf.py:1426 msgid "" "Determines what port the HTTP server will\n" " bind." msgstr "Legt den Port fest an den der HTTP Server bindet." #: src/conf.py:1429 #, fuzzy msgid "" "Determines whether the server will stay\n" " alive if no plugin is using it. This also means that the server will\n" " start even if it is not used." msgstr "" "Legt fest ob der Server gestartet bleiben soll, wenn\n" " kein Plugin ihn benutzt. Das bedeutet außerdem das der Server startet\n" " obwohl er nicht benutzt wird." #: src/conf.py:1433 #, fuzzy msgid "" "Determines the path of the file served as\n" " favicon to browsers." msgstr "Legt den Port fest an den der HTTP Server bindet." #: src/conf.py:1436 msgid "" "Determines the public URL of the server.\n" " By default it is http://:/, but you will want to change\n" " this if there is a reverse proxy (nginx, apache, ...) in front of\n" " the bot." msgstr "" #: src/conf.py:1446 msgid "" "Determines whether the bot will ignore\n" " unidentified users by default. Of course, that'll make it\n" " particularly hard for those users to register or identify with the bot\n" " without adding their hostmasks, but that's your problem to solve." msgstr "" #: src/conf.py:1453 msgid "" "A string that is the external IP of the bot. If this is the\n" " empty string, the bot will attempt to find out its IP dynamically " "(though\n" " sometimes that doesn't work, hence this variable). This variable is not " "used\n" " by Limnoria and its built-in plugins: see supybot.protocols.irc.vhost /\n" " supybot.protocols.irc.vhost6 to set the IRC bind host, and\n" " supybot.servers.http.hosts4 / supybot.servers.http.hosts6 to set the " "HTTP\n" " server bind host." msgstr "" #: src/conf.py:1472 msgid "" "Determines what the default timeout for socket\n" " objects will be. This means that *all* sockets will timeout when this " "many\n" " seconds has gone by (unless otherwise modified by the author of the " "code\n" " that uses the sockets)." msgstr "" #: src/conf.py:1478 msgid "" "Determines what file the bot should write its PID\n" " (Process ID) to, so you can kill it more easily. If it's left unset (as " "is\n" " the default) then no PID file will be written. A restart is required " "for\n" " changes to this variable to take effect." msgstr "" #: src/conf.py:1488 msgid "" "Determines whether the bot will automatically\n" " thread all commands." msgstr "Legt fest ob der Bot automatisch alles als Befehl behandeln soll." #: src/conf.py:1491 msgid "" "Determines whether the bot will automatically\n" " flush all flushers *very* often. Useful for debugging when you don't " "know\n" " what's breaking or when, but think that it might be logged." msgstr "" #: src/httpserver.py:62 msgid "Supybot Web server index" msgstr "Supybot Web Server Index" #: src/httpserver.py:67 msgid "Here is a list of the plugins that have a Web interface:" msgstr "Das ist eine Liste der Plugins die ein Webinterface haben:" #: src/httpserver.py:245 #, fuzzy msgid "" "\n" " This is a default response of the Supybot HTTP server. If you see this\n" " message, it probably means you are developing a plugin, and you have\n" " neither overriden this message or defined an handler for this query." msgstr "" "\n" " Das ist eine Standardantwort eines Supybot HTTP Servers.\n" " Wenn du diese Nachricht siehst, heißt das womöglich das du ein Plugin\n" " entwickelst und das weder diese Nachricht übersüringst oder einen " "Handler für diese Anfrage definiert hast." #: src/httpserver.py:291 msgid "" "\n" " I am a pretty clever IRC bot, but I suck at serving Web pages, " "particulary\n" " if I don't know what to serve.\n" " What I'm saying is you just triggered a 404 Not Found, and I am not\n" " trained to help you in such a case." msgstr "" #: src/httpserver.py:310 #, fuzzy msgid "Request not handled." msgstr "Anfrage nicht behandelt." #: src/httpserver.py:317 msgid "No plugins available." msgstr "Keine Plugins verfügbar" #: src/httpserver.py:335 src/httpserver.py:353 src/httpserver.py:391 #, fuzzy msgid "Request not handled" msgstr "Anfrage nicht behandelt." #: src/httpserver.py:378 msgid "No favicon set." msgstr "" #: src/ircutils.py:573 msgid "is an op on %L" msgstr "" #: src/ircutils.py:575 msgid "is a halfop on %L" msgstr "" #: src/ircutils.py:577 msgid "is voiced on %L" msgstr "" #: src/ircutils.py:580 msgid "is also on %L" msgstr "" #: src/ircutils.py:582 msgid "is on %L" msgstr "" #: src/ircutils.py:585 msgid "isn't on any publicly visible channels" msgstr "" #: src/ircutils.py:593 src/ircutils.py:594 src/ircutils.py:600 msgid "" msgstr "" #: src/ircutils.py:602 #, fuzzy msgid " %s is away: %s." msgstr "%s nicht zulässig als %s" #: src/ircutils.py:607 msgid " identified" msgstr "" #: src/ircutils.py:613 msgid "%s (%s) has been%s on server %s since %s (idle for %s). %s %s.%s" msgstr "" #: src/ircutils.py:617 msgid "%s (%s) has been%s on server %s and disconnected on %s." msgstr "" #: src/questions.py:61 msgid "Sorry, that response was not an option." msgstr "Entschuldigung, diese Antwort war keine Option." #: src/questions.py:110 msgid "Sorry, you must enter a value." msgstr "Entschuldigung du musst einen Wert angeben." #: src/questions.py:130 msgid "Enter password: " msgstr "Passwort eingeben:" #: src/questions.py:132 msgid "Re-enter password: " msgstr "Passwort erneut eingeben:" #: src/questions.py:145 msgid "Passwords don't match." msgstr "Passwörter stimmen nicht überein" #: src/registry.py:219 #, fuzzy msgid "%r is not a valid entry in %r" msgstr "%s nicht zulässig als %s" #: src/registry.py:568 msgid "Value must be either True or False (or On or Off), not %r." msgstr "" #: src/registry.py:585 msgid "Value must be an integer, not %r." msgstr "" #: src/registry.py:595 #, fuzzy msgid "Value must be a non-negative integer, not %r." msgstr "nicht negative Ganzzahl" #: src/registry.py:604 msgid "Value must be positive (non-zero) integer, not %r." msgstr "" #: src/registry.py:613 msgid "Value must be a floating-point number, not %r." msgstr "" #: src/registry.py:629 msgid "Value must be a floating-point number greater than zero, not %r." msgstr "" #: src/registry.py:640 msgid "Value must be a floating point number in the range [0, 1], not %r." msgstr "" #: src/registry.py:655 msgid "Value should be a valid Python string, not %r." msgstr "" #: src/registry.py:693 msgid "Valid values include %L." msgstr "" #: src/registry.py:695 msgid "Valid values include %L, not %%r." msgstr "" #: src/registry.py:769 msgid "Value must be a valid regular expression, not %r." msgstr "" #: src/utils/gen.py:113 msgid "year" msgstr "" #: src/utils/gen.py:116 msgid "week" msgstr "" #: src/utils/gen.py:119 msgid "day" msgstr "" #: src/utils/gen.py:122 msgid "hour" msgstr "" #: src/utils/gen.py:126 msgid "minute" msgstr "" #: src/utils/gen.py:129 msgid "second" msgstr "" #: src/utils/gen.py:138 msgid "%s ago" msgstr "" #: src/utils/str.py:349 msgid "and" msgstr "" #~ msgid "" #~ "Determines what directory backup data is put\n" #~ " into." #~ msgstr "" #~ "Legt fest in welchem Verzeichnis Sicherungskopien gespeichert werden." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/fi.po0000644000175000017500000022506114535072470014506 0ustar00valval# Limnoria # Copyright (C) 2011-2014 Limnoria # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: Limnoria core\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11:27+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" #: src/callbacks.py:217 msgid "Error: " msgstr "Virhe: " #: src/callbacks.py:233 msgid "Error: I tried to send you an empty message." msgstr "Virhe: Yritin lähettää sinulle tyhjän viestin." #: src/callbacks.py:361 msgid "" "Missing \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "Puttuva \"%s\". Voit tahtoa laittaa parametrisi kaksiin lainausmerkkeihin " "estääksesi ylimääräisiä hakasulkuja tulemasta tulkituiksi sisäkkäisiksi " "komennoiksi." #: src/callbacks.py:390 msgid "" "\"|\" with nothing preceding. I obviously can't do a pipe with nothing " "before the |." msgstr "" "\"|\" ennen mitään. En ilmiselvästi voi putkittaa tyhjyyttä ilman mitään " "ennen |-merkkiä." #: src/callbacks.py:398 msgid "" "Spurious \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "Pettävää \"%s\". Voit tahtoa laittaa parametrisi kaksiin lainausmerkkeihin " "estääksesiylimääräisiä hakasulkuja tulemasta tulkituiksi sisäkkäisiksi " "komennoiksi." #: src/callbacks.py:407 msgid "" "\"|\" with nothing following. I obviously can't do a pipe with nothing " "after the |." msgstr "" "\"|\" ilman mitään sen perässä. En ilmeisesti voi putkittaa tyhjyyttä |:n " "jälkeen." #: src/callbacks.py:623 msgid "%s is not a valid %s." msgstr "%s ei ole kelvollinen %s." #: src/callbacks.py:625 msgid "That's not a valid %s." msgstr "Tuo ei ole kelvollinen %s." #: src/callbacks.py:794 msgid "(XX more messages)" msgstr "(XX viestiä jatkoa)" #: src/callbacks.py:849 msgid "more message" msgstr "viesti jatkoa" #: src/callbacks.py:851 msgid "more messages" msgstr "viestejä jatkoa" #: src/callbacks.py:986 msgid "You've attempted more nesting than is currently allowed on this bot." msgstr "" "Yritit enempiä sisäkkäisiä komentoja, kuin on tällä hetkellä sallittu tässä " "botissa." #: src/callbacks.py:1171 msgid "" "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." msgstr "" "Komento %q on saatavilla %L lisäosissa. Ole hyvä ja määritä minkä lisäosan " "komentoa tahdot kutsua laittamalla sen nimi komentoon ennen %q:ta." #: src/callbacks.py:1356 msgid "" "Determines what commands are currently disabled. Such\n" " commands will not appear in command lists, etc. They will appear not " "even\n" " to exist." msgstr "" "Määrittää mitkä komennon ovat tällä hetkellä pois käytöstä. Sellaiset\n" " komennot eivät ilmesty komento listoissa, jne. Ne eivät näytä edes\n" " olevan olemassa." #: src/callbacks.py:1595 msgid "Invalid arguments for %s." msgstr "Virheelliset parametrit kohteelle %s." #: src/callbacks.py:1627 msgid "The %q command has no help." msgstr "Komennolla %q ei ole ohjetta." #: src/commands.py:284 msgid "integer" msgstr "kokonaisluku" #: src/commands.py:295 msgid "non-integer value" msgstr "ei-kokonaisluku arvo" #: src/commands.py:306 msgid "floating point number" msgstr "liukuluku numero" #: src/commands.py:315 msgid "positive integer" msgstr "positiivinen kokonaisluku" #: src/commands.py:319 msgid "non-negative integer" msgstr "ei-negatiivinen kokonaisluku" #: src/commands.py:322 msgid "index" msgstr "indeksi" #: src/commands.py:347 msgid "number of seconds" msgstr "määrä sekunteja" #: src/commands.py:354 msgid "boolean" msgstr "boolean" #: src/commands.py:368 src/commands.py:375 src/commands.py:384 #: src/commands.py:391 src/commands.py:400 msgid "do that" msgstr "tee se" #: src/commands.py:371 src/commands.py:378 src/commands.py:387 #: src/commands.py:394 src/commands.py:403 msgid "I'm not even in %s." msgstr "En edes ole kanavalla %s." #: src/commands.py:373 msgid "I need to be voiced to %s." msgstr "Minulla täytyy olla ääni tehdäkseni %s." #: src/commands.py:381 #, fuzzy msgid "I need to be at least voiced to %s." msgstr "Minulla täytyy olla ainakin voice, jotta voin %s" #: src/commands.py:389 msgid "I need to be halfopped to %s." msgstr "" "Minulla täytyy olla puolikanavaoperaattorin valtuudet voidakseni tehdä %s." #: src/commands.py:397 msgid "I need to be at least halfopped to %s." msgstr "" "Minulla täytyy olla vähintään puolioperaattorin oikeudet voidakseni tehdä %s" #: src/commands.py:405 msgid "I need to be opped to %s." msgstr "Minun täytyy olla opattuna, jotta voin %s." #: src/commands.py:411 src/commands.py:569 msgid "channel" msgstr "kanava" #: src/commands.py:424 msgid "nick or hostmask" msgstr "nimimerkki tai hostmask" #: src/commands.py:478 msgid "regular expression" msgstr "säännöllinen lauseke" #: src/commands.py:489 src/commands.py:493 msgid "nick" msgstr "nimimerkki" #: src/commands.py:490 msgid "That nick is too long for this server." msgstr "Tuo nimimerkki on liian pitkä tälle palvelimelle." #: src/commands.py:501 msgid "I haven't seen %s." msgstr "En ole nähnyt käyttäjää %s." #: src/commands.py:548 src/commands.py:567 msgid "I'm not in %s." msgstr "En ole kanavalla %s." #: src/commands.py:552 msgid "This command may only be given in a channel that I am in." msgstr "Tämä komento voidaan antaa vain kanavalla, jolla minä olen." #: src/commands.py:565 msgid "You must be in %s." msgstr "Sinun täytyy olla kanavalla %s." #: src/commands.py:576 msgid "%s is not in %s." msgstr "%s ei ole kanavalla %s." #: src/commands.py:624 msgid "You must not give the empty string as an argument." msgstr "Et voi antaa tyhjää merkkiketjua parametriksi." #: src/commands.py:632 #, fuzzy msgid "You must not give a string containing spaces as an argument." msgstr "Et voi antaa välilyöntejä sisältävää merkkiketjua parametriksi." #: src/commands.py:642 msgid "This message must be sent in a channel." msgstr "Tämä viesti täytyy lähettää kanavalla." #: src/commands.py:674 src/commands.py:681 msgid "url" msgstr "url" #: src/commands.py:687 msgid "IRI" msgstr "" #: src/commands.py:693 msgid "email" msgstr "sähköposti" #: src/commands.py:703 src/commands.py:711 msgid "http url" msgstr "http URL-osoite" #: src/commands.py:718 msgid "command name" msgstr "komennon nimi" #: src/commands.py:726 msgid "ip" msgstr "IP" #: src/commands.py:732 msgid "letter" msgstr "kirjain" #: src/commands.py:764 msgid "plugin" msgstr "lisä-osa" #: src/commands.py:772 msgid "irc color" msgstr "irc väri" #: src/conf.py:130 msgid "" "Determines whether this plugin is loaded\n" " by default." msgstr "Määrittää ladataanko tämä lisäosa oletuksena." #: src/conf.py:134 msgid "" "Determines whether this plugin is\n" " publicly visible." msgstr "" "Määrittää onko tämä lisäosa\n" " julkisesti näkyvillä." #: src/conf.py:231 msgid "Determines the bot's default nick." msgstr "Määrittää botin oletus nimimerkin." #: src/conf.py:234 msgid "" "Determines what alternative\n" " nicks will be used if the primary nick (supybot.nick) isn't available. " "A\n" " %s in this nick is replaced by the value of supybot.nick when used. If " "no\n" " alternates are given, or if all are used, the supybot.nick will be " "perturbed\n" " appropriately until an unused nick is found." msgstr "" "Määrittää mitä vaihtoehtoisia nimimerkkejä käytetään, jos\n" " ensisijainen nimimerkki (supybot.nick) ei ole saatavilla. \n" " %s tässä nimimerkissä korvataa asetusarvolla supybot.nick, kun sitä " "käytetään. Jos\n" " vaihtoehtoja ei anneta, tai jos kaikki ovat käytössä, supybot.nick " "tehdään levottomaksi, \n" " kunnes käyttämätön nimimerkki löydetään." #: src/conf.py:241 msgid "" "Determines the bot's ident string, if the server\n" " doesn't provide one by default." msgstr "" "Määrittää botin ident merkkiketjun, mikäli käyttäjä\n" " ei tarjoa yhtä oletuksena." #: src/conf.py:258 msgid "" "Determines the real name which the bot sends to\n" " the server. A standard real name using the current version of the bot\n" " will be generated if this is left empty." msgstr "" "Määrittää minkä oikean nimen botti lähettää palvelimelle.\n" " Perus user-merkkiketju luodaan botin nykyisestä versiosta, mikäli\n" " tämä on jätetty tyhjäksi." #: src/conf.py:267 msgid "Determines what networks the bot will connect to." msgstr "Määrittää mihin verkkoihin botti muodostaa yhdeyden." #: src/conf.py:377 msgid "" "Determines what password will be used on %s. Yes, we know that\n" " technically passwords are server-specific and not network-specific,\n" " but this is the best we can do right now." msgstr "" "Määrittää mitä salasanaa käytetään verkossa %s. Kyllä, me tiedämme, että \n" " tietyille palvelimille, eivätkä tietyille verkoille, mutta\n" " tämä on paras johon pystymme juuri nyt." #: src/conf.py:381 msgid "" "Space-separated list of servers the bot will connect to for %s.\n" " Each will be tried in order, wrapping back to the first when the " "cycle\n" " is completed." msgstr "" "Välilyönneillä erotettu lista palvelimista, joihin botti muodostaa yhteyden " "muodostaakseen yhteyden\n" " verkkoon %s. Jokaista palvelinta yritetään, palaten takaisin ensimmäiseen, " "kun kaikkia on yritetty." #: src/conf.py:385 msgid "Space-separated list of channels the bot will join only on %s." msgstr "" "Välilyönneille eroteltu lista kanavista, joille botti liittyy vain verkossa " "%s. " #: src/conf.py:389 msgid "" "Determines whether the bot will attempt to connect with SSL\n" " sockets to %s." msgstr "" "Määrittää yrittääkö botti yhdistää SSL\n" " solmuilla verkkoon %s." #: src/conf.py:392 msgid "" "Space-separated list\n" " of fingerprints of trusted certificates for this network.\n" " Supported hash algorithms are: %L.\n" " If non-empty, Certification Authority signatures will not be used " "to\n" " verify certificates." msgstr "" #: src/conf.py:398 msgid "" "A certificate that is trusted to verify\n" " certificates of this network (aka. Certificate Authority)." msgstr "" #: src/conf.py:401 msgid "Deprecated config value, keep it to False." msgstr "" #: src/conf.py:404 msgid "" "Determines what certificate file (if any) the bot will use to\n" " connect with SSL sockets to %s." msgstr "" "Määrittää mitä SSL-varmennetta (jos mitään) botti käyttää yhdistäessään\n" " verkkoon %s käyttäen SSL-solmuja." #: src/conf.py:407 msgid "" "Determines what key (if any) will be used to join the\n" " channel." msgstr "" "Määrittää mitä salasanaa (jos mitään) käytetään kanavalle\n" " liittymisessä." #: src/conf.py:409 msgid "" "Determines\n" " what nick the bot will use on this network. If empty, defaults to\n" " supybot.nick." msgstr "" "Määrittää mitä nimimerkkiä botti käyttää tässä verkossa. Jos tämä on tyhjä, " "niin se on\n" " oletuksena supybot.nick" #: src/conf.py:412 msgid "" "Determines\n" " the bot's ident string, if the server doesn't provide one by " "default.\n" " If empty, defaults to supybot.ident." msgstr "" "Määrittää botin identin, ellei palvelin määritä sitä valmiiksi.\n" " Ollessaan tyhjä, tämä on oletuksena supybot.ident." #: src/conf.py:415 msgid "" "Determines\n" " the real name which the bot sends to the server. If empty, defaults " "to\n" " supybot.user" msgstr "" "Määrittää oikean nimen, jonka botti lähettää palvelimelle. Ollessaan tyhjä " "tämä on supybot.user:in\n" " asetusarvo." #: src/conf.py:419 #, fuzzy msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. If empty, defaults to\n" " supybot.protocols.irc.umodes" msgstr "" "Määrittää mitä umodeja botti pyytää palvelimelta yhdistäessään. Jos tämä on " "tyhjä\n" " oletusasetus on supybot.protocols.irc.umodes." #: src/conf.py:424 #, fuzzy msgid "" "Determines what SASL username will be used on %s. This should\n" " be the bot's account name." msgstr "" "Määrittää mitä SASL käyttäjätunnusta käytetään verkossa %s. Tämän pitäisi " "olla botin\n" " käyttäjätunnus. Johtuen SASLin toimintatavasta, mitään ryhmitettyä " "nimimerkkiä ei\n" " voida käyttää." #: src/conf.py:427 msgid "Determines what SASL password will be used on %s." msgstr "Määrittää mitä SASL salasanaa käytetään verkossa %s." #: src/conf.py:430 msgid "" "Determines what SASL ECDSA key (if any) will be used on %s.\n" " The public key must be registered with NickServ for SASL\n" " ECDSA-NIST256P-CHALLENGE to work." msgstr "" #: src/conf.py:435 #, fuzzy msgid "" "Determines what SASL mechanisms will be tried and in which order.\n" " " msgstr "" "Määrittää mitä salasanaa (jos mitään) käytetään kanavalle\n" " liittymisessä." #: src/conf.py:438 #, fuzzy msgid "" "Determines whether the bot will abort the connection if the\n" " none of the enabled SASL mechanism succeeded." msgstr "" "Määrittää yrittääkö botti yhdistää SSL\n" " solmuilla verkkoon %s." #: src/conf.py:441 #, fuzzy msgid "" "If not empty, determines the hostname:port of the socks proxy that\n" " will be used to connect to this network." msgstr "" "Ollessaan täytetty, määrittää sen socks-välityspalvelimen isäntämimen, jota " "käytetään tähän verkkoon yhdistettäessä." #: src/conf.py:461 msgid "Determines how urls should be formatted." msgstr "Määrittää kuinka URL:t muotoillaan." #: src/conf.py:469 msgid "" "Determines how timestamps\n" " printed for human reading should be formatted. Refer to the Python\n" " documentation for the time module to see valid formatting characters " "for\n" " time formats." msgstr "" "Määrittää, kuinka aikaleimat, jotka on tarkoitettu ihmisten luettaviksi\n" " pitäisi muotoilla. Katso Pythonin dokumentaatiota\n" " aika moduulille nähdäksesi kelvolliset muotoilumerkit \n" " ajan muodoille." #: src/conf.py:484 msgid "" "Determines whether elapsed times will be given\n" " as \"1 day, 2 hours, 3 minutes, and 15 seconds\" or as \"1d 2h 3m 15s\"." msgstr "" "Määrittää näytetäänkö kulunut aika muodossa\n" " \"1 päivä, 2 tuntia, 3 minuuttia, ja 15 sekuntia\" vai \"1d 2h 3m 15s\"." #: src/conf.py:495 msgid "" "Maximum number of items in a list\n" " before the end is replaced with 'and others'. Set to 0 to always\n" " show the entire list." msgstr "" #: src/conf.py:512 msgid "other" msgstr "" #: src/conf.py:517 msgid "" "Determines the absolute maximum length of\n" " the bot's reply -- no reply will be passed through the bot with a " "length\n" " greater than this." msgstr "" "Määrittää botin vastauksen lopullisen enimmäis pituuden\n" " -- yksikään vastaus, joka on suurempi, kuin tämä\n" " ei mene botin läpi." #: src/conf.py:522 msgid "" "Determines whether the bot will break up long\n" " messages into chunks and allow users to use the 'more' command to get " "the\n" " remaining chunks." msgstr "" "Määrittää hajottaako botti pitkät viestit\n" " paloihin ja sallii käyttäjien käyttää 'more' komentoa\n" " saadakseen jäljelläolevat palat." #: src/conf.py:527 msgid "" "Determines what the maximum number of\n" " chunks (for use with the 'more' command) will be." msgstr "" "Määrittää maksimimäärän\n" " paloja (käytettäväksi 'more' komennon) kanssa." #: src/conf.py:531 msgid "" "Determines how long individual chunks\n" " will be. If set to 0, uses our super-tweaked,\n" " get-the-most-out-of-an-individual-message default." msgstr "" "Määrittää, kuinka pitkiä yksittäiset palat tulevat\n" " olemaan. Jos tämä on asetettu arvoon 0, tämä käyttää meidän\n" " super-muokattua-saa-suurin-osa-ulos-yksittäisestä-viestistä oletusta." #: src/conf.py:536 msgid "" "Determines how many mores will be sent\n" " instantly (i.e., without the use of the more command, immediately when\n" " they are formed). Defaults to 1, which means that a more command will " "be\n" " required for all but the first chunk." msgstr "" "Määrittää, kuinka monta \"more\"a lähetetään heti \n" " (esim., ilman \"more\" command käyttöä, heti, kun ne ovat " "muodostettuja. \n" " On oletuksena 1, joka tarkoittaa, että \"more\" komentoa vaaditaan " "kaikille muille, paitsi\n" " ensinmäiselle palalle." #: src/conf.py:542 msgid "" "Determines whether the bot will send\n" " multi-message replies in a single message. This defaults to True \n" " in order to prevent the bot from flooding. If this is set to False\n" " the bot will send multi-message replies on multiple lines." msgstr "" "Määrittää lähettääkö botti monen viestin vastaukset yhdessä viestissä. Tämä " "on\n" " oletuksena \"True\", ettei botti floodaa. Jos tämä asetetaan arvoon \"False" "\", botti\n" " lähettää monen viestin vastaukset usealla rivillä." #: src/conf.py:548 msgid "" "Determines whether the bot will reply with an\n" " error message when it is addressed but not given a valid command. If " "this\n" " value is False, the bot will remain silent, as long as no other plugins\n" " override the normal behavior." msgstr "" "Määrittää vastaako botti virheilmoituksella, kun sille puhutaan, mutta " "annetaan viallinen \n" " komento. Jos tämä arvo on False, \n" " botti pysyy hiljaisena, mikäli muita lisäosia, jotka ohittavat \n" " tämän käytöksen ei ole." #: src/conf.py:555 msgid "" "Determines whether error messages that result\n" " from bugs in the bot will show a detailed error message (the uncaught\n" " exception) or a generic error message." msgstr "" "Määrittää näyttävätkö virheilmoitukset bugeista botissa tarkan " "virheilmoituksen \n" " (selittämätön poikkeus )\n" " vai tavallisen virheilmoituksen." #: src/conf.py:559 msgid "" "Determines whether the bot will send error\n" " messages to users in private. You might want to do this in order to " "keep\n" " channel traffic to minimum. This can be used in combination with\n" " supybot.reply.error.withNotice." msgstr "" "Määrittää lähettääkö botti virheilmoitukset käyttäjille yksityisesti. \n" " Voit haluta tehdä tämän, pitääksesi kanavan ruuhkan mahdollisimman " "alhaisena. \n" " Tämä voidaan yhdistää asetusarvon \n" " supybot.reply.error.withNotice kanssa." #: src/conf.py:564 #, fuzzy msgid "" "Determines whether the bot will send error\n" " messages to users via NOTICE instead of PRIVMSG. You might want to do " "this\n" " so users can ignore NOTICEs from the bot and not have to see error\n" " messages; or you might want to use it in combination with\n" " supybot.reply.error.inPrivate so private errors don't open a query " "window\n" " in most IRC clients." msgstr "" "Määrittää lähettääkö botti virheilmoitukset käyttäjille käyttäen \n" " NOTICEa PRIVMSG:een sijaan. Voit tahtoa tehdä tämän, jotta käyttäjät " "voivat \n" " jättää NOTICEt botilta huomioimatta ja heidän ei tarvitse nähdä " "virheilmoituksia; \n" " tai voit haluta yhdistää tämän asetusarvon\n" " supybot.reply.errorInPrivate kanssa, jotta virheilmoitukset eivät avaa " "yksityiskeskustelu ikkunaa \n" " enimmissä IRC-asiakasohjelmissa." #: src/conf.py:571 #, fuzzy msgid "" "Determines whether the bot will *not* provide\n" " details in the error\n" " message to users who attempt to call a command for which they do not " "have\n" " the necessary capability. You may wish to make this True if you don't " "want\n" " users to understand the underlying security system preventing them from\n" " running certain commands." msgstr "" "Määrittää lähettääkö botti virheilmoituksen käyttäjille, jotka \n" " yrittävät kutsua komennon, johon heillä ei ole valtuuksia. \n" " Tämä voidaan tahtoa asettaa arvoon True jos käyttäjien ei tahdota \n" " ymmärtävän piilossa olevaa turvallisuusjärjestelmää, joka estää heitä " "suorittamasta \n" " tiettyjä komentoja." #: src/conf.py:579 msgid "" "Determines whether the bot will reply\n" " privately when replying in a channel, rather than replying to the " "whole\n" " channel." msgstr "" "Määrittää vastaako botti yksityisesti, kun vastaa \n" " kanavalla mielummin, kuin vastaa koko \n" " kanavalle." #: src/conf.py:584 msgid "" "Determines whether the bot will reply with a\n" " notice when replying in a channel, rather than replying with a privmsg " "as\n" " normal." msgstr "" "Määrittää vastaako botti huomautuksella, kun se vastaa kanavalla\n" " mielummin kuin PRIVMSG:llä, jolla se vastaa \n" " tavallisesti." #: src/conf.py:590 #, fuzzy msgid "" "Determines whether the bot will reply with a\n" " notice when it is sending a private message, in order not to open a /" "query\n" " window in clients." msgstr "" "Määrittää vastaako botti huomautuksella, kun se lähettää yksityisviestejä\n" " välttääkseen /query ikkunan avaamista asiakasohjelmissa. \n" " Yksittäiset käyttäjät voivat ohittaa tämän asetusarvolla \n" " reply.withNoticeWhenPrivate." #: src/conf.py:595 #, fuzzy msgid "" "Determines whether the bot will always prefix\n" " the user's nick to its reply to that user's command." msgstr "" "Määrittää aloittaako botti rivin sen käyttäjän nimimerkillä, joka antoi \n" " komennon." #: src/conf.py:599 msgid "" "Determines whether the bot should attempt to\n" " reply to all messages even if they don't address it (either via its " "nick\n" " or a prefix character). If you set this to True, you almost certainly " "want\n" " to set supybot.reply.whenNotCommand to False." msgstr "" "Määrittää pitäisikö botin yrittää vastata kaikkiin viesteihin, vaikka ne " "eivät\n" " olisi tarkoitettuja sille (joko nimimerkillä tai\n" " sen etuliitemerkillä. Jos tämä asetetaan arvoon True,\n" " supybot.reply.whenNotCommand tahdotaan asettaa Falseksi." #: src/conf.py:605 msgid "" "Determines whether the bot will allow you to\n" " send channel-related commands outside of that channel. Sometimes " "people\n" " find it confusing if a channel-related command (like Filter.outfilter)\n" " changes the behavior of the channel but was sent outside the channel\n" " itself." msgstr "" "Määrittää salliiko botti kanavaan liittyvien komentojen lähettämisen\n" " kyseisen kanavan ulkopuolella. Joskus ihmisten mielestä on hämmentävää, " "mikäli\n" " kanavaan liittyvä komento (kuten Filter.outfilter), muuttaa botin\n" " käyttäytymistä kanavalla, mutta komento lähetettiin kanavan itsensä \n" " ulkopuolella." #: src/conf.py:612 msgid "" "Determines whether the bot will unidentify\n" " someone when that person changes their nick. Setting this to True\n" " will cause the bot to track such changes. It defaults to False for a\n" " little greater security." msgstr "" "Määrittää kirjaako botti ulos käyttäjän, mikäli tuo käyttäjä vaihtaa " "nimimerkkiään.\n" " Tämän asettaminen arvoon True asettaa botin seuraamaan sellaisia " "mmuutoksia. Se\n" " on oletuksena False hiukan paremman turvallisuuden vuoksi." #: src/conf.py:618 msgid "" "Determines whether the bot will always join a\n" " channel when it's invited. If this value is False, the bot will only " "join\n" " a channel if the user inviting it has the 'admin' capability (or if " "it's\n" " explicitly told to join the channel using the Admin.join command)." msgstr "" "Määrittää liittyykö botti kanavalle aina kutsuttaessa.\n" " Jos tämä on False, botti liittyy kanavalle vain, mikäli kutsuvalla " "käyttäjällä on 'admin' valtuus (tai sen\n" " on käsketty liittyä kanavalle käyttäen komentoa Admin.join)." #: src/conf.py:624 msgid "" "Supybot normally replies with the full help\n" " whenever a user misuses a command. If this value is set to True, the " "bot\n" " will only reply with the syntax of the command (the first line of the\n" " help) rather than the full help." msgstr "" "Supybot vastaa tavallisesti täydellä ohjeteksillä, kun käyttäjä " "väärinkäyttää komentoa. Mikäli tämä on asetettu arvoon \"True\", botti " "vastaa vain komennon syntaksilla (ensinmäinen rivi ohjetekstiä) ennemmin, " "kuin täydellä ohjetekstillä." #: src/conf.py:639 msgid "" "Determines what prefix characters the bot will\n" " reply to. A prefix character is a single character that the bot will " "use\n" " to determine what messages are addressed to it; when there are no " "prefix\n" " characters set, it just uses its nick. Each character in this string " "is\n" " interpreted individually; you can have multiple prefix chars\n" " simultaneously, and if any one of them is used as a prefix the bot will\n" " assume it is being addressed." msgstr "" "Määrittää mihin etuliitemerkkeihin botti vastaa.\n" " Etuliitemerkki on yksi merkki, jota botti käyttää määrittääkseen ovatko " "viestit osoitettuja sille; kun etuliitemerkkiä ei ole asetettu, se käyttää " "vain nimimerkkiään. Jokainen merkki tässä merkkiketjussa tulkitaan erikseen; " "etuliitemerkkijä voidaan käyttää monia samanaikaisesti, ja jos yhtäkään " "niistä käyetään etuliitemerkkinä, botti olettaa, että viesti on osoitettu " "sille." #: src/conf.py:648 msgid "" "Determines what strings the\n" " bot will reply to when they are at the beginning of the message. " "Whereas\n" " prefix.chars can only be one character (although there can be many of\n" " them), this variable is a space-separated list of strings, so you can\n" " set something like '@@ ??' and the bot will reply when a message is\n" " prefixed by either @@ or ??." msgstr "" "Määrittää mihin merkkiketjuihin botti vastaa, kun ne ovat rivin alussa.\n" "Kun prefix.chars voi olla vain yhden merkin pituinen (vaikka niitä voikin " "olla \n" "monia), tämä asetusarvo on välilyönneillä eroiteltu lista, joten voit " "asettaa sen \n" "joksikin, kuten '@@ ??' ja botti vastaa, kun viestissä on etuliiteenä joko " "@@ tai ??." #: src/conf.py:655 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick, rather than with a prefix character." msgstr "" "Määrittää vastaako botti, kun ihmiset osoittavat sitä \n" " nimimerkillä, mielummin kuin aloitusmerkillä." #: src/conf.py:658 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick at the end of the message, rather than at\n" " the beginning." msgstr "" "Määrittää vastaako botti, kun \n" " ihmiset osoittavat viestinsä sille, kun sen nimimerkki on lopussa, " "eikä \n" " alussa." #: src/conf.py:662 msgid "" "Determines what extra nicks\n" " the bot will always respond to when addressed by, even if its current " "nick\n" " is something else." msgstr "" "Määrittää mille nimimerkeille osoitetuille viesteille botti \n" " vastaa aina, jopa jos sen nykyinen nimimerkki on jokin \n" " muu." #: src/conf.py:672 msgid "The operation succeeded." msgstr "Tehtävä suoritettu onnistuneesti." #: src/conf.py:673 msgid "" "Determines what message the bot replies with when a command succeeded.\n" " If this configuration variable is empty, no success message will be\n" " sent." msgstr "" "Määrittää millä viestillä botti vastaa, kun komento on onnistunut.\n" " Jos tämä asetusarvo on tyhjä, onnistumisviestejä ei lähetetä." #: src/conf.py:678 msgid "" "An error has occurred and has been logged.\n" " Please contact this bot's administrator for more information." msgstr "" "Virhe on tapahtunut ja tallennettu lokiin.\n" " Ole hyvä ja ota yhteyttä tämän botin ylläpitäjään saadaksesi lisätietoja." # Google Kääntäjän mukaan "ambiguos" tarkoittaa epäselvää, mutta miksi botti haluaisi olla epäselvä? #: src/conf.py:679 msgid "" "\n" " Determines what error message the bot gives when it wants to be\n" " ambiguous." msgstr "" "\n" " Määrittää minkä virheilmoituksen botti antaa tahtoessaan olla epäselvä." #: src/conf.py:684 msgid "" "An error has occurred and has been logged.\n" " Check the logs for more information." msgstr "" "Virhe on tapahtunut ja tallennettu lokiin. Lisätietoja saadaan lokeista." # Miksi botti tahtoisi olevansa epäselvä? Google Kääntäjä... #: src/conf.py:685 msgid "" "Determines what error\n" " message the bot gives to the owner when it wants to be ambiguous." msgstr "" "Määrittää millaisen virheilmoituksen botti antaa omistajalle ollessaan " "epäselvä." # Pitäisikö "hostmask" kääntää "isäntänaamioksi" vai pitää kuten se on? #: src/conf.py:689 msgid "" "Your hostmask doesn't match or your password\n" " is wrong." msgstr "Hostmask ei täsmää tai salasana on väärä." #: src/conf.py:690 msgid "" "Determines what message the bot replies with when\n" " someone tries to use a command that requires being identified or having " "a\n" " password and neither credential is correct." msgstr "" "Määrittää millä viestillä botti vastaa, kun joku yrittää antaa komennon, " "joka vaatii tunnistautumista, ja kumpikaan vaatimus ei ole täytetty." #: src/conf.py:696 msgid "" "I can't find %s in my user\n" " database. If you didn't give a user name, then I might not know what " "your\n" " user is, and you'll need to identify before this command might work." msgstr "" "En voi löytää käyttäjää %s käyttäjätietokannastani. Mikäli käyttäjänimeä ei " "annettu,\n" " en ehkä tiedä kuka etsimäsi käyttäjä on, ja sinun täytyy tunnistautua ennen " "kuin tämä\n" " komento saattaa toimia." #: src/conf.py:699 msgid "" "Determines what error message the bot replies with when someone tries\n" " to accessing some information on a user the bot doesn't know about." msgstr "" "Määrittää millä virheilmoituksella botti vastaa, kun joku yrittää päästä " "tietoihin käyttäjästä, jota ei ole olemassakaan." #: src/conf.py:703 msgid "" "You must be registered to use this command.\n" " If you are already registered, you must either identify (using the " "identify\n" " command) or add a hostmask matching your current hostmask (using the\n" " \"hostmask add\" command)." msgstr "" "Tätä komentoa käyttääkseen on oltava rekisteröitynt.\n" " Mikäli olet jo rekisteröitynyt, tunnistaudu (käyttämällä identify komentoa) " "tai lisää\n" " hostmask, joka täsmää nykyiseen hostmaskiisi (käyttämällä \"hostmask add\" " "komentoa)." #: src/conf.py:706 msgid "" "Determines what error message the bot\n" " replies with when someone tries to do something that requires them to " "be\n" " registered but they're not currently recognized." msgstr "" "Määrittää millä virheilmoituksella botti vastaa, kun joku yrittää käyttää " "komentoa,\n" " joka vaatii hänen olevan rekisteröitynyt, mutta häntä ei ole tällä " "hetkellä\n" " tunnistettu." # Tämän pitäisi mahdollisesti olla passiivissa, mutta en ole varma miten tämä pitäisi olla passiivissa. Merkitsen sen epäselväksi. #: src/conf.py:711 #, fuzzy msgid "" "You don't have the %s capability. If you\n" " think that you should have this capability, be sure that you are " "identified\n" " before trying again. The 'whoami' command can tell you if you're\n" " identified." msgstr "" "Sinulla ei ole valtuutta %s. Mikäli uskot, että sinulla pitäisi olla tämä " "valtuus,\n" " varmista että olet tunnistautunut ennen kuin yrität uudelleen. Komento " "'whoami' voi\n" " kertoa sinulle oletko tunnistautunut." #: src/conf.py:714 msgid "" "Determines what error message is given when the bot\n" " is telling someone they aren't cool enough to use the command they tried " "to\n" " use." msgstr "" "Määrittää mikä virheilmoitus annetaan, kun botti kertoo jollekin, ettei hän " "ole\n" " riittävän kuuli käyttääkseen komentoa, jota hän yritti käyttää." #: src/conf.py:719 msgid "" "You're missing some capability you need.\n" " This could be because you actually possess the anti-capability for the\n" " capability that's required of you, or because the channel provides that\n" " anti-capability by default, or because the global capabilities include\n" " that anti-capability. Or, it could be because the channel or\n" " supybot.capabilities.default is set to False, meaning that no commands " "are\n" " allowed unless explicitly in your capabilities. Either way, you can't " "do\n" " what you want to do." msgstr "" "Jokin valtuus, jota tarvitaan puuttuu.\n" " Tämä voi johtua siitä, että vaaditulla valtuudella on antivaltuus, tai " "koska kanava\n" " tarjoaa antivaltuuden oletuksena. Tai se voi johtua siitä, että kanava tai\n" " supybot.capabilities.default on asetettu arvoon False, tarkoittaen ettei " "komentoja\n" " sallita, ellei niitä ole erikseen sallittu. Miten vain, et voi tehdä sitä, " "mitä tahdot\n" " tehdä." #: src/conf.py:727 msgid "" "Determines what generic error message is given when the bot is telling\n" " someone that they aren't cool enough to use the command they tried to " "use,\n" " and the author of the code calling errorNoCapability didn't provide an\n" " explicit capability for whatever reason." msgstr "" "Määrittää mikä geneerinen virheilmoitus annetaan, kun botti kertoo jollekin, " "ettei\n" " hän ole tarpeeksi kuuli käyttääkseen komentoa, jota yritti käyttää ja " "koodin, joka\n" " kutsuu errorNoCapability koodin ei tarjonnut tarkkaa valtuutta ihansama " "mistä syystä." #: src/conf.py:733 msgid "" "That operation cannot be done in a\n" " channel." msgstr "Tätä tehtävää ei voida suorittaa kanavalla." #: src/conf.py:734 msgid "" "Determines what error messages the bot sends to people\n" " who try to do things in a channel that really should be done in\n" " private." msgstr "" "Määrittää virheilmoituksen, jonka botti lähettää ihmisille, ketkä yrittävät " "tehdä\n" " asioita, jotka todella pitäisi tehdä yksityisesti, kanavalla." #: src/conf.py:739 msgid "" "This may be a bug. If you think it is,\n" " please file a bug report at\n" " ." msgstr "" "Tämä voi olla bugi. Mikäli uskotaan niin, raportoidaan bugi osoitteessa\n" " . Bugiraportit kirjoitetaan " "englanniksi." #: src/conf.py:742 msgid "" "Determines what message the bot sends when it thinks you've\n" " encountered a bug that the developers don't know about." msgstr "" "Määrittää minkä viestin botti lähettää uskoessaan, että on kohdattu bugi, " "josta\n" " kehittäjät eivät ole tietoisia." #: src/conf.py:749 msgid "$Type #$id: $text (added by $username at $at)" msgstr "" #: src/conf.py:750 msgid "" "Format used by generic database plugins (Lart, Dunno, Prase, Success,\n" " Quote, ...) to show an entry. You can use the following variables:\n" " $type/$types/$Type/$Types (plugin name and variants), $id, $text,\n" " $at (creation time), $userid/$username/$nick (author)." msgstr "" #: src/conf.py:760 msgid "" "A floating point number of seconds to throttle\n" " snarfed URLs, in order to prevent loops between two bots snarfing the " "same\n" " URLs and having the snarfed URL in the output of the snarf message." msgstr "" "Liukuluku numero sekunteja, joilla jarrutetaan kaapattuja URL ositteita, " "jotta estetään\n" " toistot kahden botin välillä, jotka sisältävät saman URL-osoitteen " "ulostulevassa\n" " kaappausviestissä." #: src/conf.py:765 msgid "" "Determines the number of seconds\n" " between running the upkeep function that flushes (commits) open " "databases,\n" " collects garbage, and records some useful statistics at the debugging\n" " level." msgstr "" "Määrittää sekuntimäärän, joka on ylläpito toiminnon, joka tallentaa avoimet " "tietokannat\n" " , kerää jätteet ja tallentaa hyödyllisiä tilastotietoja debuggaus tasolle, " "välissä." #: src/conf.py:771 msgid "" "Determines whether the bot will periodically\n" " flush data and configuration files to disk. Generally, the only time\n" " you'll want to set this to False is when you want to modify those\n" " configuration files by hand and don't want the bot to flush its current\n" " version over your modifications. Do note that if you change this to " "False\n" " inside the bot, your changes won't be flushed. To make this change\n" " permanent, you must edit the registry yourself." msgstr "" "Määrittää kirjoittaako botti säännöllisesti asetustiedostot levylle. Yleensä " "ainut\n" " tapaus, jossa tämä tahdotaan asettaa arvoon False on kun kyseisiä " "asetustiedostoja\n" " tahdotaan muokata käsin, eikä botin tahdota tallentavan nykyistä versiotaan " "omien\n" " muokkaustesi päälle. Huomaa, että mikäli vaihdat tämän arvoon False botin " "sisällä,\n" " muutoksiasi ei tallenneta. Tehdäksesi tämän muutoksen pysyväksi, rekisteriä " "täytyy\n" " muokata itse." # Google Kääntäjä ei tiedä mitä "tokenized" tarkoittaa. En tiedä minäkään. #: src/conf.py:797 #, fuzzy msgid "" "Determines what characters are valid for quoting\n" " arguments to commands in order to prevent them from being tokenized.\n" " " msgstr "" "Määrittää mitkä merkit ovat kelvollisia parametrien lainaamiseen, jotta " "estetään niitä\n" " tulemasta tokenisoiduiksi " #: src/conf.py:804 msgid "" "Determines whether the bot will allow nested\n" " commands, which rule. You definitely should keep this on." msgstr "" "Määrittää salliiko botti sisäkkäisten komentojen käytön, jotka ovat " "parhaita. Tämä\n" " täytyy varmasti pitää käytössä." #: src/conf.py:807 msgid "" "Determines what the maximum number of\n" " nested commands will be; users will receive an error if they attempt\n" " commands more nested than this." msgstr "" "Määrittää mikä on enimmäinen sallittu määrä sisäkkäisiä komentoja; " "käyttäjät\n" " vastaanottavat virheilmoituksen yrittäessään käyttää useampia sisäkkäisiä " "komentoja,\n" " kuin tämä arvo sallii." #: src/conf.py:816 #, fuzzy msgid "" "Supybot allows you to specify what brackets\n" " are used for your nested commands. Valid sets of brackets include\n" " [], <>, {}, and (). [] has strong historical motivation, but <> or\n" " () might be slightly superior because they cannot occur in a nick.\n" " If this string is empty, nested commands will not be allowed in this\n" " channel." msgstr "" "Supybot sallii sinun määrittää millaisia sulkuja käytetään sisäkkäisille " "komennoille.\n" " Kelvolliset sulkusarjat ovat [], <> ja {} (). []:lla on vahva\n" " historiallinen motivaatio mutta <> tai () voi olla huomattavasti parempi, " "koska se\n" " ei voi olla nimimerkissä. Mikäli tämä asetusarvo on tyhjä, sisäkkäiset " "komennot\n" " eivät ole sallittuja kanavalla." #: src/conf.py:823 msgid "" "Supybot allows nested commands. Enabling this\n" " option will allow nested commands with a syntax similar to UNIX pipes, " "for\n" " example: 'bot: foo | bar'." msgstr "" "Supybot sallii sisäkkäiset komennot. Tämän asetuksen käyttöönottaminen " "sallii\n" " sisäkkäisten komentojen käyttämisen samanlaisella syntaksilla, kuin UNIX " "putket,\n" " esimerkiksi 'botti: foo | bar'." #: src/conf.py:828 msgid "" "Determines what commands have default\n" " plugins set, and which plugins are set to be the default for each of " "those\n" " commands." msgstr "" "Määrittää millä komennoilla on oletuslisäosa määritettynä, ja mitkä lisäosat " "on\n" " määritetty olemann oletuksia kaikille noille komennoille." #: src/conf.py:834 msgid "" "Determines what plugins automatically get precedence over all\n" " other plugins when selecting a default plugin for a command. By\n" " default, this includes the standard loaded plugins. You probably\n" " shouldn't change this if you don't know what you're doing; if you " "do\n" " know what you're doing, then also know that this set is\n" " case-sensitive." msgstr "" "Määrittää mitkä lisäosat saavat automaattisesti suosion yli käikkien muiden " "lisäosien,\n" " kun valitaan oletuslisäosaa komennolle. Oletuksena tämä sisältää kaikki " "perus lisäosat\n" ", jotka ovat käytössä. Tätä ei pitäisi muuttaa, ellei olla täysin varmoja " "mitä tehdään;\n" " jos tiedetään mitä ollaan tekemisessä, tiedetään myös, että tässä " "asetusarvossa\n" " kirjainkoolla on merkitystä." #: src/conf.py:844 msgid "" "Allows this bot's owner user to use commands\n" " that grants them shell access. This config variable exists in case you " "want\n" " to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)\n" " from gaining shell access to the bot's server by impersonating the " "owner.\n" " Setting this to False also disables plugins and commands that can be\n" " used to indirectly gain shell access." msgstr "" #: src/conf.py:859 #, fuzzy msgid "" "Determines the interval used for\n" " the history storage." msgstr "Määrittää historian tallennukseen käytettävän aikavälin." #: src/conf.py:862 msgid "" "Determines whether the bot will defend itself\n" " against command-flooding." msgstr "Määrittää puolustaako botti itseään komento tulvimista vastaan." #: src/conf.py:865 #, fuzzy msgid "" "Determines how many commands users are\n" " allowed per minute. If a user sends more than this many commands in " "any\n" " 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.punishment seconds." msgstr "" "Määrittää kuinka monta komentoa käyttäjien on sallittua suorittaa " "minuutissa. Jos\n" " käyttäjä lähettää enemmän, kuin tämän verran komentoja missä tahansa 60 " "sekuntin\n" " jaksossa, hänet jätetään huomioitta sekunteiksi, jotka on määritetty " "asetusarvossa\n" " supybot.abuse.flood.command.punishment." #: src/conf.py:870 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with commands." msgstr "" "Määrittää kuinka moneksi sekuntiksi botti jättää käyttäjät, jotka tulvivat " "sitä\n" " komennoilla, botti jättää huomioimatta." #: src/conf.py:873 #, fuzzy msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for command flooding." msgstr "" "Määrittää huomauttaako bitti ihmisiä siitä, että heidän olemassa olostaan ei " "välitetä, jos \n" " heidän olemassa olostaan ei välitetä viallisten komentojen tulvan takia." #: src/conf.py:877 msgid "" "Determines whether the bot will defend itself\n" " against invalid command-flooding." msgstr "" "Määrittää puolustaako botti itseään \n" " viallisten komentojen tulvaa vastaan." #: src/conf.py:880 #, fuzzy msgid "" "Determines how many invalid commands users\n" " are allowed per minute. If a user sends more than this many invalid\n" " commands in any 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.invalid.punishment seconds. Typically, " "this\n" " value is lower than supybot.abuse.flood.command.maximum, since it's far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" "Määrittää kuinka monta virheellistä komentoa käyttäjät voivat antaa " "minuutissa. Jos\n" " käyttäjä lähettää näin monta komentoa millä tahansa minuutin jaksolla, " "hänet jätetään\n" " huomioitta sekunti määräksi, joka on määritetty asetusarvossa\n" " supybot.abuse.flood.command.invalid.punishment. Tyypillisesti tämä arvo on " "alhaisempi,\n" " kuin supybot.abuse.flood.command.maximum, koska on erittäin vähemmän " "todennäköistä (ja\n" " paljon ärsyttävämpää) käyttäjille tulvia virheellisillä komennoilla, kuin " "heille\n" " tulvia kelvollisilla komennoilla." #: src/conf.py:888 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with invalid commands. Typically, this\n" " value is higher than supybot.abuse.flood.command.punishment, since it's " "far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" "Määrittää, kuinka moneksi sekuntiksi botti jättää huomioitta käyttäjät, " "jotka tulvivat\n" " sitä virheellisillä komennoilla. Tyypillisesti tämä arvo on korkeampi, " "kuin\n" " supybot.abuse.flood.punishment, koska on paljon vähemmän todennäköistä (ja " "paljon\n" " ärsyttävämpää) käyttäjille tulvia virheellisillä komennoilla, kuin heille " "tulvia\n" " kelvollisilla komennoilla." #: src/conf.py:894 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for invalid command flooding." msgstr "" "Määrittää huomauttaako bitti ihmisiä siitä, että heidän olemassa olostaan ei " "välitetä, jos \n" " heidän olemassa olostaan ei välitetä viallisten komentojen tulvan takia." # Mitäköhän tämä tarkoittaa? #: src/conf.py:903 #, fuzzy msgid "" "Determines the default length of time a\n" " driver should block waiting for input." msgstr "" "Määrittää, oletus ajanjakson, joka ajurin pitäisi estää odottaen sisääntuloa." #: src/conf.py:911 msgid "" "Determines what driver module the \n" " bot will use. Current, the only (and default) driver is Socket." msgstr "" #: src/conf.py:915 msgid "" "Determines the maximum time the bot will\n" " wait before attempting to reconnect to an IRC server. The bot may, of\n" " course, reconnect earlier if possible." msgstr "" "Määrittää maksimi ajan, jonka botti odottaa, ennen kuin yrittää yhdistää " "uudelleen\n" " IRC palvelimeen. Botti voi tietysti yhdistää palvelimeen uudelleen " "aikaisemminkin,\n" " mikäli se on mahdollista." #: src/conf.py:967 msgid "" "Determines what directory configuration data is\n" " put into." msgstr "Määrittää mihin hakemistoon asetustiedostot laitetaan." #: src/conf.py:970 msgid "Determines what directory data is put into." msgstr "Määrittää mihin hakemistoon data laitetaan." #: src/conf.py:972 msgid "" "Determines what directory backup data is put\n" " into. Set it to /dev/null to disable backup (it is a special value,\n" " so it also works on Windows and systems without /dev/null)." msgstr "" "Määrittää mihin hakemistoon varmuuskopio tiedot laitetaan. Tämä asetetaan " "arvoon\n" " /dev/null, mikäli tahdotaan poistaa varmuuskopioiden ottaminen käytöstä (se " "on\n" " erityisarvo, joten se toimi myös Windowsilla ja käyttöjärjestelmillä,\n" " joilla ei ole hakemistoa /dev/null)." #: src/conf.py:979 msgid "" "Determines what directory temporary files\n" " are put into." msgstr "Määrittää mihin hakemistoon väliaikaistiedostot laitetaan." #: src/conf.py:982 msgid "" "Determines what directory files of the\n" " web server (templates, custom images, ...) are put into." msgstr "" "Määrittää minne verkkopalvelimen hakemistotiedostot (pohjat, mukautetut " "kuvat\n" " jne.) laitetaan." #: src/conf.py:995 msgid "" "Determines what directories\n" " the bot will look for plugins in. Accepts a comma-separated list of\n" " strings.\n" " This means that to add another directory, you can nest the former value " "and\n" " add a new one. E.g. you can say: bot: 'config supybot.directories." "plugins\n" " [config supybot.directories.plugins], newPluginDirectory'." msgstr "" "Määrittää mistä hakemistoista botti etsii lisäosia. Hyväksyy pilkuilla " "eristetyn listan\n" " merkkiketjuista. Tämä tarkoittaa, että voit lisätä toisen hakemiston. Voit " "sisäistää\n" " aiemman arvon ja lisätä uuden. Esimerkiksi voit sanoa: botti:\n" " 'config supybot.directories.plugins [config supybot.directories.plugins],\n" " UusiLisäosaHakemisto." #: src/conf.py:1003 msgid "" "List of all plugins that were\n" " ever loaded. Currently has no effect whatsoever. You probably want to " "use\n" " the 'load' or 'unload' commands, or edit supybot.plugins.\n" " instead of this." msgstr "" #: src/conf.py:1008 msgid "" "Determines whether the bot will always load\n" " important plugins (Admin, Channel, Config, Misc, Owner, and User)\n" " regardless of what their configured state is. Generally, if these " "plugins\n" " are configured not to load, you didn't do it on purpose, and you still\n" " want them to load. Users who don't want to load these plugins are " "smart\n" " enough to change the value of this variable appropriately :)" msgstr "" "Määrittää lataako botti aina tärkeäy lisäosat (Admin, Channel, Config, Misc, " "Owner ja\n" " User) riippumatta, mikä niiden määritetty tila on. Yleensä, mikäli nämä " "lisäosat on\n" " määritetty olemaan latautumatta, sitä ei tahdottu tehdä tarkoituksella ja " "niiden\n" " tahdotaan silti latautuvan. Käyttäjät, jotka eivät tahdo ladata näitä " "lisäosia ovat\n" " tarpeeksi teräviä vaihtaakseen tätä asetusarvoa sopivasti :)" #: src/conf.py:1036 msgid "" "Determines what databases are available for use. If this\n" " value is not configured (that is, if its value is empty) then sane " "defaults\n" " will be provided." msgstr "" "Määrittää mitkä tietokannat ovat käytettävissä. Mikäli tätä arvoa ei ole " "määritetty,\n" " (se tarkottaa, että sen arvo on tyhjä) niin järkevät oletukset tarjotaan." #: src/conf.py:1042 msgid "" "Determines what filename will be used\n" " for the users database. This file will go into the directory specified " "by\n" " the supybot.directories.conf variable." msgstr "" "Määrittää mitä nimeä käytetään tiedostolle, jota käytetään " "käyttäjätietokantana. Tämä\n" " tiedosto menee hakemistoon, jonka määrittää asetusarvo supybot.directories." "conf." #: src/conf.py:1046 msgid "" "Determines how long it takes identification to\n" " time out. If the value is less than or equal to zero, identification " "never\n" " times out." msgstr "" "Määrittää kuinka pitkän ajan tunnistautuminen on voimassa. Mikäli tänä arvo " "on vähemmän\n" " tai yhtäsuuri kuin nolla, tunnistautuminen ei ikinä vanhene." #: src/conf.py:1050 msgid "" "Determines whether the bot will allow users to\n" " unregister their users. This can wreak havoc with already-existing\n" " databases, so by default we don't allow it. Enable this at your own " "risk.\n" " (Do also note that this does not prevent the owner of the bot from " "using\n" " the unregister command.)\n" " " msgstr "" "Määrittää salliiko botti käyttäjien poistaa tunnuksensa botista. Tämä voi " "aiheuttaa\n" " kaaosta tietokannoilla, jotka ovat jo olemassa, joten oletuksena me emme " "salli sitä.\n" " Tämä asetusarvo otetaan käyttöön omalla vastuulla. (Huomaa, ettei tämä " "asetusarvo estä\n" " botin omistajaa käyttänästä \"unregister\" komentoa.)" #: src/conf.py:1059 msgid "" "Determines what filename will be used\n" " for the ignores database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Määrittää nimen tiedostolle, jota käytetään huomioitta jättämisen seurannan\n" " tietokantana. Tämä tiedosto laitetaan hakemistoon, jonka määrittää " "asetusarvo\n" " supybot.directories.conf." #: src/conf.py:1065 msgid "" "Determines what filename will be used\n" " for the channels database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Määrittää nimen tiedostolle, jota käytetään kanavatietokantana. Tämä " "tiedosto laitetaan\n" " hakemistoon, joka määritetään asetusarvolla supybot.directories.conf." #: src/conf.py:1071 #, fuzzy msgid "" "Determines what filename will be used\n" " for the networks database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Määrittää nimen tiedostolle, jota käytetään huomioitta jättämisen seurannan\n" " tietokantana. Tämä tiedosto laitetaan hakemistoon, jonka määrittää " "asetusarvo\n" " supybot.directories.conf." #: src/conf.py:1103 #, fuzzy msgid "" "Determines whether the bot will require user\n" " registration to use 'add' commands in database-based Supybot\n" " plugins." msgstr "" "Määrittää puolustaako botti itseään \n" " viallisten komentojen tulvaa vastaan." #: src/conf.py:1107 msgid "" "Determines whether database-based plugins that\n" " can be channel-specific will be so. This can be overridden by " "individual\n" " channels. Do note that the bot needs to be restarted immediately after\n" " changing this variable or your db plugins may not work for your " "channel;\n" " also note that you may wish to set\n" " supybot.databases.plugins.channelSpecific.link appropriately if you " "wish\n" " to share a certain channel's databases globally." msgstr "" "Määrittää ovatko tietokanta-pohjaiset lisäosat, jotka voivat olla " "kanavakohtaisia,\n" " kanavakohtaisia. Yksittäiset kanavat voivat ohittaa tämän. Huomaa, että " "botti täytyy\n" " käynnistää uudelleen välittömästi, kun tätä asetusarvoa on muutettu tai " "muuten\n" " tietokanta-pohjaiset lisäosat eivät välttämättä toimi kanavalla; huomaa " "myös, että\n" " supybot.databases-plugins.channelSpecific.link voidaan myös tahtoa asettaa " "sopivaksi,\n" " mikäli tietyn kanavan tietokannat, tahdotaan jakaa globaalisti." #: src/conf.py:1115 msgid "" "Determines what channel global\n" " (non-channel-specific) databases will be considered a part of. This is\n" " helpful if you've been running channel-specific for awhile and want to " "turn\n" " the databases for your primary channel into global databases. If\n" " supybot.databases.plugins.channelSpecific.link.allow prevents linking, " "the\n" " current channel will be used. Do note that the bot needs to be " "restarted\n" " immediately after changing this variable or your db plugins may not " "work\n" " for your channel." msgstr "" "Määrittää mihin kanavaan kuuluviksi globaalit (eivät kanavakohtaiset) " "tietokannat\n" " määritetään kuuluviksi. Tämä on avuliasta, mikäli kanavakohtaisia " "tietokantoja on\n" " käytetty pitkän aikaa ja ensisijaisen kanavan tietokannat tahdotaan " "muuttaa\n" " globaaleiksi tietokannoiksi. Jos supybot.databases.plugins.channelSpecific." "link.allow\n" " estää linkittämisen, nykyistä kanavaa käytetään. Huomaa, että botti täytyy " "käynnistää\n" " uudelleen välittömästi tämän asetusarvon muuttamisen jälkeen tai tietokanta " "lisäosat\n" " eivät välttämättä toimi kanavalla." #: src/conf.py:1124 msgid "" "Determines whether another channel's global\n" " (non-channel-specific) databases will be allowed to link to this " "channel's\n" " databases. Do note that the bot needs to be restarted immediately " "after\n" " changing this variable or your db plugins may not work for your " "channel.\n" " " msgstr "" "Määrittää onko toisen kanavan globaaleiden (ei kanavakohtaisten) " "tietokantojen\n" " sallittua linkittyä tämän kanavan tietokantoihin. Huomaa, että botti täytyy " "käynnistää\n" " uudelleen välittömästi tämän asetusarvon muuttamisen jälkeen tai tietokanta " "lisäosat\n" " eivät välttämättä toimi kanavalla." #: src/conf.py:1142 msgid "" "Determines\n" " whether CDB databases will be allowed as a database implementation." msgstr "" "Määrittää sallitaanko CDB tietokantojen käyttö tietokantaimplementaationa." #: src/conf.py:1145 msgid "" "Determines how often CDB databases will have\n" " their modifications flushed to disk. When the number of modified " "records\n" " is greater than this fraction of the total number of records, the " "database\n" " will be entirely flushed to disk." msgstr "" "Määrittää, kuinka usein DDB lisäosat tallentavat muutoksensa levylle. Kun " "muokattujen\n" " tallenteiden määrä on suurempi kuin tämä murto-osa tallenteiden " "yhteismäärästä,\n" " tietokanta tallennetaan levylle kokonaan." #: src/conf.py:1237 msgid "" "Determines what will be used as the\n" " default banmask style." msgstr "Määrittää mitä käytetään oletus porttikiellon anto tyylinä." #: src/conf.py:1241 #, fuzzy msgid "" "Determines whether the bot will strictly\n" " follow the RFC; currently this only affects what strings are\n" " considered to be nicks. If you're using a server or a network that\n" " requires you to message a nick such as services@this.network.server\n" " then you you should set this to False." msgstr "" "Määrittää noudattaako botti tiukasti RFC standardeja; tällä hetkellä tämä " "vaikuttaa\n" " siihen mitten merkkiketjujen uskotaan olevan nimimerkkejä. Jos käytät " "palvelinta, tai\n" " verkkoa, joka vaatii viestien lähetyksen nimimerkille, kuten esimerkiksi\n" " services@tämän.verkon.palvelin, niin tämä pitäisi asettaa arvoon \"False\"." #: src/conf.py:1248 msgid "" "Determines whether the bot will enable\n" " draft/experimental extensions of the IRC protocol. Setting this to True\n" " may break your bot at any time without warning and/or break your\n" " configuration irreversibly. So keep it False unless you know what you " "are\n" " doing." msgstr "" #: src/conf.py:1255 msgid "" "Determines what certificate file (if any) the bot\n" " will use connect with SSL sockets by default." msgstr "" "Määrittää mitä SSL-varmennetta (jos mitään) botti käyttää oletuksena\n" " yhdistäessään SSL-solmuilla." #: src/conf.py:1259 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. Many people might choose +i; " "some\n" " networks allow +x, which indicates to the auth services on those " "networks\n" " that you should be given a fake host." msgstr "" "Määrittää mitä käyttäjätiloja botti pyytää palvelimelta, kun se muodostaa " "yhteyden.\n" " Monet ihmiset saattavat valita tilan +i; jotkut verkot sallivat tilan +x, " "joka\n" " ilmaisee tuon verkon tunnistautumispalveluille, että käyttäjälle pitäisi " "antaa\n" " väärennetty isäntä." #: src/conf.py:1265 #, fuzzy msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv4." msgstr "" "Määrittää mihin vhostiin botti sitoutuu, ennen kuin ottaa yhdeyden (IRC, " "HTTP\n" " jne.) palvelimeen käyttäen IPv4:ää." #: src/conf.py:1269 #, fuzzy msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv6." msgstr "" "Määrittää mihin vhostiin botti sitoutuu, ennen kuin ottaa yhdeyden (IRC, " "HTTP\n" " jne.) palvelimeen käyttäen IPv6:tta." #: src/conf.py:1273 #, fuzzy msgid "" "Determines how many old messages the bot will\n" " keep around in its history. Changing this variable will not take " "effect\n" " on a network until it is reconnected." msgstr "" "Määrittää kuinka vanhoja viestejä botti pitää historiassaan. Tämän " "asetusarvon\n" " muuttaminen ei vaikuta ennen uudelleenkäynnistystä." #: src/conf.py:1278 #, fuzzy msgid "" "A floating point number of seconds to throttle\n" " queued messages -- that is, messages will not be sent faster than once " "per\n" " throttleTime seconds." msgstr "" "Liukuluku sekunteja, joilla jarrutetaan jonossa olevia viestejä. -- Tämä " "estää viestejä\n" " tulemasta lähetetyiksi nopeammin kuin kerran throttleTime sekunteja." #: src/conf.py:1283 msgid "" "Determines whether the bot will send PINGs to\n" " the server it's connected to in order to keep the connection alive and\n" " discover earlier when it breaks. Really, this option only exists for\n" " debugging purposes: you always should make it True unless you're " "testing\n" " some strange server issues." msgstr "" "Määrittää lähettääkö botti PINGejä palvelimelle, johon se on muodostanut " "yhdeyden\n" " pitääkseen yhdeyden elossa ja saadakseen tietää aiemmin, jos se hajoaa. " "Oikeasti, tämä\n" " asetus on olemassa vain debuggaus tarkoituksia varten: tämä pitäisi aina " "olla \"True\",\n" " mikäli et kokeile omituisia palvelinongelmia." #: src/conf.py:1290 msgid "" "Determines the number of seconds between sending\n" " pings to the server, if pings are being sent to the server." msgstr "" "Määrittää määrän sekunteja palvelimelle lähetettävien pingien välissä, jos " "pingejä\n" " lähetetään palvelimelle." #: src/conf.py:1295 msgid "" "Determines whether the bot will refuse\n" " duplicated messages to be queued for delivery to the server. This is a\n" " safety mechanism put in place to prevent plugins from sending the same\n" " message multiple times; most of the time it doesn't matter, unless " "you're\n" " doing certain kinds of plugin hacking." msgstr "" "Määrittää kieltäytyykö botti pistämästä viestien kaksoiskappaleita jonoon " "palvelimelle\n" " toimitettaviksi. Tämä on turvallisuusmekanismi, joka on asetettu estämään " "lisäosia\n" " lähettämästä samaa viestiä palvelimelle monia kertoja; yleensä sillä ei ole " "merkitystä\n" " mikäli et ole tekemässä lisäosien hakkerointia." #: src/conf.py:1303 msgid "" "Determines how many seconds must elapse between\n" " JOINs sent to the server." msgstr "" "Määrittää, kuinka monta sekuntia täytyy kulua palvelimelle lähetettyjen \n" " JOINien välissä." #: src/conf.py:1311 msgid "" "Determines how many bytes the bot will\n" " 'peek' at when looking through a URL for a doctype or title or " "something\n" " similar. It'll give up after it reads this many bytes, even if it " "hasn't\n" " found what it was looking for." msgstr "" "Määrittää kuinka monta bittiä botti \"kurkistaa\" katsoessaan URL:än läpi " "etsien doctypeä\n" " tai otsikkoa tai jotakin sinnepäin. Se luovuttaa kun on lukenut tämän " "määrän bittejä,\n" " vaikka ei löytäisikään etsimäänsä." #: src/conf.py:1335 #, fuzzy msgid "" "Determines what HTTP proxy all HTTP requests should go\n" " through. The value should be of the form 'host:port'." msgstr "" "Määrittää minkä välityspalvelimen läpi kaikkien HTTP pyyntöjen pitäisi " "mennä. \n" " Arvon pitäisi olla muodossa 'isäntä:portti'." #: src/conf.py:1375 msgid "" "If set, the Accept-Language HTTP header will be set to this\n" " value for requests. Useful for overriding the auto-detected language " "based on\n" " the server's location." msgstr "" #: src/conf.py:1381 msgid "" "If set, the User-Agent HTTP header will be set to a randomly\n" " selected value from this comma-separated list of strings for requests." msgstr "" #: src/conf.py:1389 msgid "" "Determines whether server certificates\n" " will be verified, which checks whether the server certificate is signed\n" " by a known certificate authority, and aborts the connection if it is " "not.\n" " This is assumed to be True of serverFingerprints or " "authorityCertificate\n" " is set." msgstr "" #: src/conf.py:1416 msgid "" "If true, uses IPV6_V6ONLY to disable\n" " forwaring of IPv4 traffic to IPv6 sockets. On *nix, has the same\n" " effect as setting kernel variable net.ipv6.bindv6only to 1." msgstr "" #: src/conf.py:1420 msgid "" "Space-separated list of IPv4 hosts the HTTP server\n" " will bind." msgstr "" "Välilyönneille eroiteltu lista IPv4-osoitteista, joita HTTP-palvelin " "kuuntelee." #: src/conf.py:1423 msgid "" "Space-separated list of IPv6 hosts the HTTP server will\n" " bind." msgstr "" "Välilyönneilla erotettu lista IPv6-osoitteista, joita HTTP-palvelin " "kuuntelee." #: src/conf.py:1426 msgid "" "Determines what port the HTTP server will\n" " bind." msgstr "" "Määrittää mihin porttiin HTTP palvelin \n" " sitoutuu." #: src/conf.py:1429 msgid "" "Determines whether the server will stay\n" " alive if no plugin is using it. This also means that the server will\n" " start even if it is not used." msgstr "" "Määrittää pysyykö palvelin elossa, vaikka yksikään \n" " lisäosa ei käytä sitä. Tämä tarkoittaa, että palvelin käynnistyy myös, " "mikäli sitä \n" " ei käytetä." #: src/conf.py:1433 #, fuzzy msgid "" "Determines the path of the file served as\n" " favicon to browsers." msgstr "Määrittää polun tiedostoon, joka annetaan faviconina selaimille." #: src/conf.py:1436 msgid "" "Determines the public URL of the server.\n" " By default it is http://:/, but you will want to change\n" " this if there is a reverse proxy (nginx, apache, ...) in front of\n" " the bot." msgstr "" #: src/conf.py:1446 msgid "" "Determines whether the bot will ignore\n" " unidentified users by default. Of course, that'll make it\n" " particularly hard for those users to register or identify with the bot\n" " without adding their hostmasks, but that's your problem to solve." msgstr "" "Määrittää jättääkö botti tunnistautumattomat käyttäjät huomiotta.\n" " Tietysti se tekee heidän rekisteröitymisestään tai tunnistautumisestaan " "botin\n" " kanssa vaikeaksi, ellei heillä ole lisättyjä hostmaskeja, mutta se on sinun " "ongelmasi." #: src/conf.py:1453 msgid "" "A string that is the external IP of the bot. If this is the\n" " empty string, the bot will attempt to find out its IP dynamically " "(though\n" " sometimes that doesn't work, hence this variable). This variable is not " "used\n" " by Limnoria and its built-in plugins: see supybot.protocols.irc.vhost /\n" " supybot.protocols.irc.vhost6 to set the IRC bind host, and\n" " supybot.servers.http.hosts4 / supybot.servers.http.hosts6 to set the " "HTTP\n" " server bind host." msgstr "" #: src/conf.py:1472 msgid "" "Determines what the default timeout for socket\n" " objects will be. This means that *all* sockets will timeout when this " "many\n" " seconds has gone by (unless otherwise modified by the author of the " "code\n" " that uses the sockets)." msgstr "" "Määrittää mikä on oletus aikakatkaisuaika solmuobjekteille. Se takoittaa, " "että *kaikki*\n" " solmut tekevät aikakatkaisun, kun nämä sekuntit olevat kuluneet (paitsi jos " "solmuja\n" " käyttävän koodin kirjoittaja on erikseen muokannut tätä)." #: src/conf.py:1478 msgid "" "Determines what file the bot should write its PID\n" " (Process ID) to, so you can kill it more easily. If it's left unset (as " "is\n" " the default) then no PID file will be written. A restart is required " "for\n" " changes to this variable to take effect." msgstr "" "Määrittää mihin tiedostoon botin pitäisi kirjoittaa PIDinsä \n" " (Prosessi ID:n), jotta voit tappaa sen helpommin. Jos jätetty tyhjäksi " "(kuten se on \n" " oletuksena) niin silloin PID tiedostoa ei kirjoiteta. Uudelleen " "käynnistys on vaadittu\n" " tämän asetusarvon muutosten vaikutuksen aikaansaamiseksi." #: src/conf.py:1488 msgid "" "Determines whether the bot will automatically\n" " thread all commands." msgstr "" "Määrittää ketjuttaako botti kaikki\n" " komennot." #: src/conf.py:1491 msgid "" "Determines whether the bot will automatically\n" " flush all flushers *very* often. Useful for debugging when you don't " "know\n" " what's breaking or when, but think that it might be logged." msgstr "" "Määrittää kuinka usein botti tallentaa automaattisesti\n" " kaikki puskurit *erittäin* usein. Hyödyllinen bugien korjaamiseen, kun " "et tiedä\n" " mikä hajoaa ja milloin, mutta uskot, että se saattaa tallentua lokiin." #: src/httpserver.py:62 msgid "Supybot Web server index" msgstr "Supybot verkkopalvelin indeksi." #: src/httpserver.py:67 msgid "Here is a list of the plugins that have a Web interface:" msgstr "Tässä on lista lisäosista, joilla on verkko käyttöliittymä:" #: src/httpserver.py:245 msgid "" "\n" " This is a default response of the Supybot HTTP server. If you see this\n" " message, it probably means you are developing a plugin, and you have\n" " neither overriden this message or defined an handler for this query." msgstr "" "\n" "Tämä on Supybotin HTTP-palvelimen oletusvastaus. Jos näet tämän viestin, se " "luultavasti\n" " tarkoittaa, että lisäosaa kehitetään, eikä tätä viestiä ole ohitettu eikä " "tälle\n" " pyynnölle ole määritetty käsittelijää." #: src/httpserver.py:291 msgid "" "\n" " I am a pretty clever IRC bot, but I suck at serving Web pages, " "particulary\n" " if I don't know what to serve.\n" " What I'm saying is you just triggered a 404 Not Found, and I am not\n" " trained to help you in such a case." msgstr "" "\n" " Olen aika älykäs IRC botti, mutta olen huono verkkosivujen " "tarjoamisessa, erityisesti, jos en\n" " tiedä mitä minun pitäisi tarjota.\n" " Mitä minä olen kertomassa on vain, että laukaisit 404 sivua ei löydy " "virheen, ja minua ei ole koulutettu\n" " auttamaan sinua sellaisessa tapauksessa." #: src/httpserver.py:310 msgid "Request not handled." msgstr "Pyyntöä ei hyväksytty." #: src/httpserver.py:317 msgid "No plugins available." msgstr "Ei lisäosia saatavilla." #: src/httpserver.py:335 src/httpserver.py:353 src/httpserver.py:391 msgid "Request not handled" msgstr "Pyyntöä ei käsitelty." #: src/httpserver.py:378 #, fuzzy msgid "No favicon set." msgstr "Faviconia ei ole asetettu." #: src/ircutils.py:573 msgid "is an op on %L" msgstr "" #: src/ircutils.py:575 msgid "is a halfop on %L" msgstr "" #: src/ircutils.py:577 msgid "is voiced on %L" msgstr "" #: src/ircutils.py:580 msgid "is also on %L" msgstr "" #: src/ircutils.py:582 msgid "is on %L" msgstr "" #: src/ircutils.py:585 msgid "isn't on any publicly visible channels" msgstr "" #: src/ircutils.py:593 src/ircutils.py:594 src/ircutils.py:600 msgid "" msgstr "" #: src/ircutils.py:602 #, fuzzy msgid " %s is away: %s." msgstr "%s ei ole kelvollinen %s." #: src/ircutils.py:607 msgid " identified" msgstr "" #: src/ircutils.py:613 msgid "%s (%s) has been%s on server %s since %s (idle for %s). %s %s.%s" msgstr "" #: src/ircutils.py:617 msgid "%s (%s) has been%s on server %s and disconnected on %s." msgstr "" #: src/questions.py:61 msgid "Sorry, that response was not an option." msgstr "Anteeksi, mutta tuo vastaus ei ollut vaihtoehto." #: src/questions.py:110 msgid "Sorry, you must enter a value." msgstr "Anteeksi, mutta sinun täytyy antaa arvo." #: src/questions.py:130 msgid "Enter password: " msgstr "Anna salasana: " #: src/questions.py:132 msgid "Re-enter password: " msgstr "Anna salasana uudelleen: " #: src/questions.py:145 msgid "Passwords don't match." msgstr "Salasanat eivät täsmää." #: src/registry.py:219 #, fuzzy msgid "%r is not a valid entry in %r" msgstr "%s ei ole kelvollinen %s." #: src/registry.py:568 msgid "Value must be either True or False (or On or Off), not %r." msgstr "" "Arvon täytyy olla \"True\" tai \"False (tai \"On\" tai \"Off\"), ei %r." #: src/registry.py:585 msgid "Value must be an integer, not %r." msgstr "Arvon täytyy olla kokonaisluku, eikä %r." #: src/registry.py:595 msgid "Value must be a non-negative integer, not %r." msgstr "Arvon täytyy olla positiivinen kokonaisluku, ei %r." #: src/registry.py:604 msgid "Value must be positive (non-zero) integer, not %r." msgstr "Arvon täytyy olla positiivinen (muu kuin nolla) kokonaisluku eikä %r." #: src/registry.py:613 msgid "Value must be a floating-point number, not %r." msgstr "Arvon täytyy olla likuluku, ei %r." #: src/registry.py:629 msgid "Value must be a floating-point number greater than zero, not %r." msgstr "Arvon täytyy olla nollaa suurempi liukuluku, ei %r." #: src/registry.py:640 #, fuzzy msgid "Value must be a floating point number in the range [0, 1], not %r." msgstr "Arvon täytyy olla liukuluku alueella [0,1] eikä %r." #: src/registry.py:655 #, fuzzy msgid "Value should be a valid Python string, not %r." msgstr "Arvon täytyy olla kelvollinen Python merkkijono eikä %r." #: src/registry.py:693 msgid "Valid values include %L." msgstr "Kelvolliset arvot ovat %L." #: src/registry.py:695 msgid "Valid values include %L, not %%r." msgstr "Kelvolliset arvot ovat %L, eikä %%r." #: src/registry.py:769 msgid "Value must be a valid regular expression, not %r." msgstr "Arvon täytyy olla kelvollinen säännöllinen lauseke, eikä %r." #: src/utils/gen.py:113 msgid "year" msgstr "vuosi" #: src/utils/gen.py:116 msgid "week" msgstr "viikko" #: src/utils/gen.py:119 msgid "day" msgstr "päivä" #: src/utils/gen.py:122 msgid "hour" msgstr "tunti" #: src/utils/gen.py:126 msgid "minute" msgstr "minuutti" #: src/utils/gen.py:129 msgid "second" msgstr "sekunti" #: src/utils/gen.py:138 #, fuzzy msgid "%s ago" msgstr "%s sitten" #: src/utils/str.py:349 msgid "and" msgstr "ja" #~ msgid "" #~ "Determines what driver module the \n" #~ " bot will use. The default is Socket which is simple and stable \n" #~ " and supports SSL. Twisted doesn't work if the IRC server which \n" #~ " you are connecting to has IPv6 (most of them do)." #~ msgstr "" #~ "Määrittää botin käyttämän ajurimoduulin. Oletus on Socket, joka on\n" #~ " yksinkertainen ja vakaa ja tukee SSL:ää. Twisted ei toimi mikäli " #~ "yhdistettävällä\n" #~ " IRC-palvelimella on IPv6 (useimmilla niistä on)." #~ msgid "" #~ "Determines what plugins will\n" #~ " be loaded." #~ msgstr "Määrittää mitkä lisäosat ladataan." #~ msgid "" #~ "A string that is the external IP of the bot. If this is the\n" #~ " empty string, the bot will attempt to find out its IP dynamically " #~ "(though\n" #~ " sometimes that doesn't work, hence this variable)." #~ msgstr "" #~ "Merkkiketju, joka on botin ulkoinen IP osoite. Jos tämä on\n" #~ " tyhjä merkkiketju, botti yrittää löytää IP osoiteensa dynaamisesti " #~ "(vaikko\n" #~ " joskus se ei toimi, siksi tämä asetusarvo)." #~ msgid "" #~ "Determines what driver module the bot\n" #~ " will use. Socket, a simple driver based on timeout sockets, is used " #~ "by\n" #~ " default because it's simple and stable. Twisted is very stable and " #~ "simple,\n" #~ " and if you've got Twisted installed, is probably your best bet." #~ msgstr "" #~ "Määrittää mitä ajurimoduulia botti käyttää. Socket, yksinkertainen ajuri, " #~ "joka perustuu\n" #~ " aikakatkaisu sokkeleihin, on käytössä oletuksena, koska se on " #~ "yksinkertainen ja vakaa.\n" #~ " Twisted on erittäin vakaa ja yksinkertainen, ja jos Twisted on " #~ "asennettuna, se on\n" #~ " luultavasti paras vaihtoehto." #~ msgid "" #~ "Determines the content of the robots.txt file,\n" #~ " served on the server to search engine." #~ msgstr "" #~ "Määrittää robots.txt tiedoston sisällön, joka laitetaan palvelimelle " #~ "annettavaksi\n" #~ " hakukoneille." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/fr.po0000644000175000017500000022720114535072470014515 0ustar00valval# Valentin Lorentz , 2012. msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2019-11-24 12:41+0100\n" "Last-Translator: \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 2.2.1\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: src/callbacks.py:217 msgid "Error: " msgstr "Erreur : " #: src/callbacks.py:233 msgid "Error: I tried to send you an empty message." msgstr "Erreur : J'ai essayé de vous envoyer un message vide." #: src/callbacks.py:361 msgid "" "Missing \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "\"%s\" manquant. Vous devriez encadrer vos arguments par des guillements " "pour éviter que les crochets ne soient évalués comme étant des commandes " "imbriquées." #: src/callbacks.py:390 msgid "" "\"|\" with nothing preceding. I obviously can't do a pipe with nothing " "before the |." msgstr "" "\"|\" avec rien ne le précédant. Je ne peux évidtemment pas faire un pipe " "avec rien avant le |." #: src/callbacks.py:398 msgid "" "Spurious \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "\"%s\" en trop. Vous devriez encadrer vos arguments par des guillements pour " "éviter que les crochets ne soient évalués comme étant des commandes " "imbriquées." #: src/callbacks.py:407 msgid "" "\"|\" with nothing following. I obviously can't do a pipe with nothing " "after the |." msgstr "" "\"|\" avec rien ne le suivant. Je ne peux évidtemment pas faire un pipe avec " "rien après le |." #: src/callbacks.py:623 msgid "%s is not a valid %s." msgstr "%s n'est pas un(e) %s valide." #: src/callbacks.py:625 msgid "That's not a valid %s." msgstr "Ce n'est pas un(e) %s valide." #: src/callbacks.py:794 msgid "(XX more messages)" msgstr "(XX messages supplémentaires)" #: src/callbacks.py:849 msgid "more message" msgstr "message supplémentaire" #: src/callbacks.py:851 msgid "more messages" msgstr "messages supplémentaires" #: src/callbacks.py:986 msgid "You've attempted more nesting than is currently allowed on this bot." msgstr "" "Vous avez essayé de faire plus d'imbrication que ce qui est actuellement " "autorisé sur ce bot." #: src/callbacks.py:1171 msgid "" "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." msgstr "" "La commande %q est disponible dans les plugins %L. Veuillez spécifier dans " "quel plugin se trouve la commande que vous souhaitez appeler, en ajoutant le " "nom du plugin avant %q." #: src/callbacks.py:1356 msgid "" "Determines what commands are currently disabled. Such\n" " commands will not appear in command lists, etc. They will appear not " "even\n" " to exist." msgstr "" "Détermine quelles commandes sont actuellement désactivées. De telles " "commandes n'apparaitront pas dans la liste des commandes, etc. Ça sera comme " "si elles n'existaient pas." #: src/callbacks.py:1595 msgid "Invalid arguments for %s." msgstr "Argument invalide pour %s" #: src/callbacks.py:1627 msgid "The %q command has no help." msgstr "La commande %q n'a pas d'aide." #: src/commands.py:284 msgid "integer" msgstr "entier" #: src/commands.py:295 msgid "non-integer value" msgstr "valeur non entière" #: src/commands.py:306 msgid "floating point number" msgstr "nombre à virgule flottante" #: src/commands.py:315 msgid "positive integer" msgstr "entier positif" #: src/commands.py:319 msgid "non-negative integer" msgstr "entier non négatif" #: src/commands.py:322 msgid "index" msgstr "index" #: src/commands.py:347 msgid "number of seconds" msgstr "nombre de secondes" #: src/commands.py:354 msgid "boolean" msgstr "booléen" #: src/commands.py:368 src/commands.py:375 src/commands.py:384 #: src/commands.py:391 src/commands.py:400 msgid "do that" msgstr "faire ça" #: src/commands.py:371 src/commands.py:378 src/commands.py:387 #: src/commands.py:394 src/commands.py:403 msgid "I'm not even in %s." msgstr "Je ne suis pas sur %s." #: src/commands.py:373 msgid "I need to be voiced to %s." msgstr "Je doit être voicé pour %s" #: src/commands.py:381 msgid "I need to be at least voiced to %s." msgstr "Je doit être au moins voice pour %s" #: src/commands.py:389 msgid "I need to be halfopped to %s." msgstr "Je doit être halfop pour %s" #: src/commands.py:397 msgid "I need to be at least halfopped to %s." msgstr "Je doit être au moins halfop pour %s" #: src/commands.py:405 msgid "I need to be opped to %s." msgstr "Je doit être opé pour %s" #: src/commands.py:411 src/commands.py:569 msgid "channel" msgstr "canal" #: src/commands.py:424 msgid "nick or hostmask" msgstr "nick ou masque d'hôte" #: src/commands.py:478 msgid "regular expression" msgstr "expression régulière" #: src/commands.py:489 src/commands.py:493 msgid "nick" msgstr "nick" #: src/commands.py:490 msgid "That nick is too long for this server." msgstr "Ce nick est trop long pour ce serveur." #: src/commands.py:501 msgid "I haven't seen %s." msgstr "Je n'ai pas vu %s." #: src/commands.py:548 src/commands.py:567 msgid "I'm not in %s." msgstr "Je ne suis pas sur %s" #: src/commands.py:552 msgid "This command may only be given in a channel that I am in." msgstr "" "Cette commande ne peut être donnée que sur un canal sur lequel je suis." #: src/commands.py:565 msgid "You must be in %s." msgstr "Vous devez être sur %s" #: src/commands.py:576 msgid "%s is not in %s." msgstr "%s n'est pas sur %s" #: src/commands.py:624 msgid "You must not give the empty string as an argument." msgstr "Vous ne pouvez me donner une chaine vide comme argument." #: src/commands.py:632 msgid "You must not give a string containing spaces as an argument." msgstr "" "Vous ne pouvez me donner une chaine contenant des espaces comme argument." #: src/commands.py:642 msgid "This message must be sent in a channel." msgstr "Ce message doit être envoyé sur un canal." #: src/commands.py:674 src/commands.py:681 msgid "url" msgstr "url" #: src/commands.py:687 msgid "IRI" msgstr "" #: src/commands.py:693 msgid "email" msgstr "courriel" #: src/commands.py:703 src/commands.py:711 msgid "http url" msgstr "URL HTTP" #: src/commands.py:718 msgid "command name" msgstr "nom de commande" #: src/commands.py:726 msgid "ip" msgstr "IP" #: src/commands.py:732 msgid "letter" msgstr "lettre" #: src/commands.py:764 msgid "plugin" msgstr "plugin" #: src/commands.py:772 msgid "irc color" msgstr "couleur IRC" #: src/conf.py:130 msgid "" "Determines whether this plugin is loaded\n" " by default." msgstr "Détermine si ce plugin est chargé par défaut." #: src/conf.py:134 msgid "" "Determines whether this plugin is\n" " publicly visible." msgstr "Détermine si ce plugin est visible publiquement" #: src/conf.py:231 msgid "Determines the bot's default nick." msgstr "Détermine le nick par défaut du bot." #: src/conf.py:234 msgid "" "Determines what alternative\n" " nicks will be used if the primary nick (supybot.nick) isn't available. " "A\n" " %s in this nick is replaced by the value of supybot.nick when used. If " "no\n" " alternates are given, or if all are used, the supybot.nick will be " "perturbed\n" " appropriately until an unused nick is found." msgstr "" "Détermine quels nicks alternatifs peuvent être utilisés si le nick " "principal (supybot.nick) n'est pas disponible. Un %s dans ce nick est " "remplacé par la valeur de supybot.nick. Si aucune alternative n'est " "donnée, ou si elles sont toutes déjà utilisées, supybot.nick sera modifié " "jusqu'à ce qu'un nick non utilisé soit trouvé." #: src/conf.py:241 msgid "" "Determines the bot's ident string, if the server\n" " doesn't provide one by default." msgstr "" "Détermine l'ident du bot (ce qui précède le @ dans le masque d'hôte), si " "le serveur n'en fourni par une par défaut." #: src/conf.py:258 msgid "" "Determines the real name which the bot sends to\n" " the server. A standard real name using the current version of the bot\n" " will be generated if this is left empty." msgstr "" "Détermine le nom réel que le bot envoie au serveur. Un nom standard " "contenant la version actuelle du bot sera généré si vous laisser cette " "variable vide." #: src/conf.py:267 msgid "Determines what networks the bot will connect to." msgstr "Détermine à quels réseaux le bot se connecte." #: src/conf.py:377 msgid "" "Determines what password will be used on %s. Yes, we know that\n" " technically passwords are server-specific and not network-specific,\n" " but this is the best we can do right now." msgstr "" "Détermine quel mot de passe sera utilisé sur %s. Oui, nous savons que " "les mots de passe sont en fait spécifiques aux serveurs et non et " "réseaux, mais nous pensons que c'est mieux comme ça." #: src/conf.py:381 msgid "" "Space-separated list of servers the bot will connect to for %s.\n" " Each will be tried in order, wrapping back to the first when the " "cycle\n" " is completed." msgstr "" "Liste des serveurs (éléments séparés par des espaces) auxquels le bot se " "connectera pour %s. Chacun sera essayé dans l’ordre, retournant au début " "lorsque le cycle est terminé." #: src/conf.py:385 msgid "Space-separated list of channels the bot will join only on %s." msgstr "" "Liste (éléments séparés par des espaces) des salons que le bot rejoindra sur " "ce réseau %s." #: src/conf.py:389 msgid "" "Determines whether the bot will attempt to connect with SSL\n" " sockets to %s." msgstr "" "Détermine si le bot tentera de se connecter avec des sockets SSL à %s." #: src/conf.py:392 msgid "" "Space-separated list\n" " of fingerprints of trusted certificates for this network.\n" " Supported hash algorithms are: %L.\n" " If non-empty, Certification Authority signatures will not be used " "to\n" " verify certificates." msgstr "" "Liste séparée par des espaces d'empreintes de certificats de confiance pour " "ce réseau. Les algorithmes de hashage supportés sont : %L. Si non-vide, les " "signatures d'Autorités de Certification ne seront pas utilisées pour " "vérifier les certificats." #: src/conf.py:398 msgid "" "A certificate that is trusted to verify\n" " certificates of this network (aka. Certificate Authority)." msgstr "" "Un certificat de confiance pour vérifier les certificats de ce réseau (c'est-" "à-dire une Autorité de Certification)." #: src/conf.py:401 msgid "Deprecated config value, keep it to False." msgstr "Valeur dépréciée, laissez-la à False." #: src/conf.py:404 msgid "" "Determines what certificate file (if any) the bot will use to\n" " connect with SSL sockets to %s." msgstr "" "Détermine quel fichier de certificat (s’il y en a un) le bot utilisera pour " "se connecter par SSL à %s." #: src/conf.py:407 msgid "" "Determines what key (if any) will be used to join the\n" " channel." msgstr "" "Détermine quelle clef (s'il y en a) sera utilisée pour rejoindre le " "canal." #: src/conf.py:409 msgid "" "Determines\n" " what nick the bot will use on this network. If empty, defaults to\n" " supybot.nick." msgstr "" "Détermine quel nick le bot utilisera sur ce réseau. Si cette variable de " "configuration est vide, le nick par défaut sera pris dans supybot.nick." #: src/conf.py:412 msgid "" "Determines\n" " the bot's ident string, if the server doesn't provide one by " "default.\n" " If empty, defaults to supybot.ident." msgstr "" "Détermine l'ident du bot (ce qui précède le @ dans le masque d'hôte), si " "le serveur n'en fourni par une par défaut." #: src/conf.py:415 msgid "" "Determines\n" " the real name which the bot sends to the server. If empty, defaults " "to\n" " supybot.user" msgstr "" "Détermine quel nick le bot utilisera sur ce réseau. Si cette variable de " "configuration est vide, le nick par défaut sera pris dans supybot.nick." #: src/conf.py:419 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. If empty, defaults to\n" " supybot.protocols.irc.umodes" msgstr "" "Détermine quels modes utilisateur le bot demandera au serveur lorsqu'il s'y " "connectera. Si cette variable de configuration est vide, le nick par défaut " "sera pris dans supybot.protocols.irc.umodes." #: src/conf.py:424 #, fuzzy msgid "" "Determines what SASL username will be used on %s. This should\n" " be the bot's account name." msgstr "" "Détermine quel nom d'utilisateur SASL sera utilisé sur %s. Ce doit être le " "nom du compte du bot. À cause de la façon dont SASL fonctionne, vous ne " "pouvez pas utiliser un nick groupé." #: src/conf.py:427 msgid "Determines what SASL password will be used on %s." msgstr "Détermine quel mot de passe SASL sera utilisé sur %s." #: src/conf.py:430 msgid "" "Determines what SASL ECDSA key (if any) will be used on %s.\n" " The public key must be registered with NickServ for SASL\n" " ECDSA-NIST256P-CHALLENGE to work." msgstr "" "Détermine quelle clé ECDSA sera utilisée pour l'authentification par SASL " "sur %s.\n" "La clé publique doit être enregistrée avec NickServ avec SASL ECDSA-NIST256P-" "CHALLENGE pour qu'elle puisse être utilisée." #: src/conf.py:435 #, fuzzy msgid "" "Determines what SASL mechanisms will be tried and in which order.\n" " " msgstr "Détermine quels mécanismes SASL seront utilisés et dans quel ordre." #: src/conf.py:438 msgid "" "Determines whether the bot will abort the connection if the\n" " none of the enabled SASL mechanism succeeded." msgstr "" "Détermine si le bot doit terminer la connexion si aucun des mécanismes SASL " "activés ne fonctionne." #: src/conf.py:441 #, fuzzy msgid "" "If not empty, determines the hostname:port of the socks proxy that\n" " will be used to connect to this network." msgstr "" "Si non vide, détermine l'hôte du proxy socks qui sera utilisé pour se " "connecter à ce réseau." #: src/conf.py:461 msgid "Determines how urls should be formatted." msgstr "Détermine comment les URLs seront formatées." #: src/conf.py:469 msgid "" "Determines how timestamps\n" " printed for human reading should be formatted. Refer to the Python\n" " documentation for the time module to see valid formatting characters " "for\n" " time formats." msgstr "" "Détermine quels timestamps sont affichés pour être lus par des humaines. " "Référez-vous à la documentation Python sur le module time pour plus " "d'information sur les formats valides." #: src/conf.py:484 msgid "" "Determines whether elapsed times will be given\n" " as \"1 day, 2 hours, 3 minutes, and 15 seconds\" or as \"1d 2h 3m 15s\"." msgstr "" "Détermine si un utilise des temps plus courts, c'est à dire par exemple " "\"1d 2h 3m 15s\" au lieu de \"1 day, 2 hours, 3 minutes, and 15 seconds" "\"." #: src/conf.py:495 msgid "" "Maximum number of items in a list\n" " before the end is replaced with 'and others'. Set to 0 to always\n" " show the entire list." msgstr "" "Nombre maximum d'éléments dans une liste avant que la fin de la liste soit " "remplacée par 'et autres'. Définir à 0 pour toujours afficher la liste " "complète." #: src/conf.py:512 msgid "other" msgstr "autre" #: src/conf.py:517 msgid "" "Determines the absolute maximum length of\n" " the bot's reply -- no reply will be passed through the bot with a " "length\n" " greater than this." msgstr "" "Détermine la longueur maximum absolue des réponses du bot ; le bot " "n'enverra aucune réponse qui dépassera ce nombre." #: src/conf.py:522 msgid "" "Determines whether the bot will break up long\n" " messages into chunks and allow users to use the 'more' command to get " "the\n" " remaining chunks." msgstr "" "Détermine si le bot séparera les longs messages en morceaux et permettra " "aux utilisateurs et utilisatrices d'utiliser la commande 'more' pour " "récupérer les morceaux restants." #: src/conf.py:527 msgid "" "Determines what the maximum number of\n" " chunks (for use with the 'more' command) will be." msgstr "" "Détermine quel est le nombre maximum de morceaux (que l'on récupère avec " "la commande 'more')." #: src/conf.py:531 msgid "" "Determines how long individual chunks\n" " will be. If set to 0, uses our super-tweaked,\n" " get-the-most-out-of-an-individual-message default." msgstr "" "Détermine quelle est la longueur maximale des morceaux. Définir cette " "variable à 0 permet d'utiliser notre super algorithme pour optimiser " "cette longueur en fonction des paramètres du serveur." #: src/conf.py:536 msgid "" "Determines how many mores will be sent\n" " instantly (i.e., without the use of the more command, immediately when\n" " they are formed). Defaults to 1, which means that a more command will " "be\n" " required for all but the first chunk." msgstr "" "Détermine combien de 'more's sont envoyés immédiatement (c'est à dire, " "avant l'utilisation de la commande 'more', juste après que l'utilisateur/" "trice ait envoyé la commande. Par défaut, cela vaut 1, ce qui signifie " "que seul le premier morceau est envoyé. Avant l'appel de 'more'." #: src/conf.py:542 msgid "" "Determines whether the bot will send\n" " multi-message replies in a single message. This defaults to True \n" " in order to prevent the bot from flooding. If this is set to False\n" " the bot will send multi-message replies on multiple lines." msgstr "" "Détermine si le bot enverra des réponses multi-messages dans un seul " "message ou dans plusieurs. Cela vaut True par défaut pour empêcher le bot de " "flooder. Si cela vaut False, alors le bot enverra les réponses multi-" "messages sur plusieurs lignes." #: src/conf.py:548 msgid "" "Determines whether the bot will reply with an\n" " error message when it is addressed but not given a valid command. If " "this\n" " value is False, the bot will remain silent, as long as no other plugins\n" " override the normal behavior." msgstr "" "Détermine si le bot répondra avec un message d'erreur lorsqu'un message " "lui est adressé, mais qu'il ne s'agit pas d'une commande valide. Si cette " "valeur est à False, le bot restera silencieux, du moment qu'aucun plugin " "ne modifie le comportement normal." #: src/conf.py:555 msgid "" "Determines whether error messages that result\n" " from bugs in the bot will show a detailed error message (the uncaught\n" " exception) or a generic error message." msgstr "" "Détermine si les messages d'erreur résultant de bugs seront affichés dans " "un message d'erreur (l'exception non interceptée) ou si un message " "d'erreur générique est utilisé." #: src/conf.py:559 msgid "" "Determines whether the bot will send error\n" " messages to users in private. You might want to do this in order to " "keep\n" " channel traffic to minimum. This can be used in combination with\n" " supybot.reply.error.withNotice." msgstr "" "Détermine si le bot enverra les messages d'erreur en privé. Vous pouvez " "vouloir ceci dans le but de limiter le traffic de données sur le canal. " "Vous pouvez utiliser ceci en combinaison avec supybot.reply.error." "withNotice." #: src/conf.py:564 #, fuzzy msgid "" "Determines whether the bot will send error\n" " messages to users via NOTICE instead of PRIVMSG. You might want to do " "this\n" " so users can ignore NOTICEs from the bot and not have to see error\n" " messages; or you might want to use it in combination with\n" " supybot.reply.error.inPrivate so private errors don't open a query " "window\n" " in most IRC clients." msgstr "" "Détermine si le bot enverra les erreurs par NOTICE plutôt que par " "PRIVMSG. Vous pouvez faire cela pour que les personnes qui ne veulent pas " "recevoir de message d'erreur ignorent les NOTICEs du bot. Vous pouvez " "utiliser cette fonctionnalité en combinaison avec supybot.reply." "errorInPrivate, pour que les erreurs en privé n'ouvrent pas une nouvelle " "fenêtre de requête, sur la plupart des clients." #: src/conf.py:571 msgid "" "Determines whether the bot will *not* provide\n" " details in the error\n" " message to users who attempt to call a command for which they do not " "have\n" " the necessary capability. You may wish to make this True if you don't " "want\n" " users to understand the underlying security system preventing them from\n" " running certain commands." msgstr "" "Détermine si le bot n'enverra *pas* un message d'erreur aux personnes qui " "tentent d'appeler une commande pour laquelle elles n'ont pas de capacité " "suffisante. Vous pouvez le mettre à True si vous ne voulez pas que les " "utilisateurs/trices comprennent le système sous-jacent de capacités pour " "lancer certaines commandes." #: src/conf.py:579 msgid "" "Determines whether the bot will reply\n" " privately when replying in a channel, rather than replying to the " "whole\n" " channel." msgstr "" "Détermine si le bot répondra en privé, plutôt que de répondre sur le " "canal." #: src/conf.py:584 msgid "" "Determines whether the bot will reply with a\n" " notice when replying in a channel, rather than replying with a privmsg " "as\n" " normal." msgstr "" "Détermine si le bot répondra par notice sur un canal plutôt que par " "privmsg comme d'habitude" #: src/conf.py:590 msgid "" "Determines whether the bot will reply with a\n" " notice when it is sending a private message, in order not to open a /" "query\n" " window in clients." msgstr "" "Détermine si le bot répondra avec une notice lorsque l'on s'adresse à lui " "en privé, pour éviter de devoir ouvrir une fenêtre /query." #: src/conf.py:595 msgid "" "Determines whether the bot will always prefix\n" " the user's nick to its reply to that user's command." msgstr "" "Détermine si le bot préfixera toujours ses réponses par le nick de " "l’utilisateur ayant invoqué la commande." #: src/conf.py:599 msgid "" "Determines whether the bot should attempt to\n" " reply to all messages even if they don't address it (either via its " "nick\n" " or a prefix character). If you set this to True, you almost certainly " "want\n" " to set supybot.reply.whenNotCommand to False." msgstr "" "Détermine si le bot tentera de répondre à tous les messages, même si ils " "ne lui sont pas adressés (par son nick ou par un caractère de préfixe). " "Si vous définissez ceci à True, vous voudrez probablement mettre supybot." "reply.whenNotCommand à False." #: src/conf.py:605 msgid "" "Determines whether the bot will allow you to\n" " send channel-related commands outside of that channel. Sometimes " "people\n" " find it confusing if a channel-related command (like Filter.outfilter)\n" " changes the behavior of the channel but was sent outside the channel\n" " itself." msgstr "" "Détermine si le bot vous permettra d'envoyer des commandes liées à un " "canal en-dehors de ce canal. Parfois, certaines personnes confondent " "lorsqu'une commande liée à un canal (comme Filter.outfilter) changent le " "comportement du canal, alors que la commande est envoyée en-dehors du " "canal." #: src/conf.py:612 msgid "" "Determines whether the bot will unidentify\n" " someone when that person changes their nick. Setting this to True\n" " will cause the bot to track such changes. It defaults to False for a\n" " little greater security." msgstr "" "Détermine si le bot will désidentifiera une personne lorsque celle-ci " "change son nick. Mettre ceci à True fera que le bot traquera ces " "changements. Il est par défaut à False pour améliorer légèrement la " "sécurité." #: src/conf.py:618 msgid "" "Determines whether the bot will always join a\n" " channel when it's invited. If this value is False, the bot will only " "join\n" " a channel if the user inviting it has the 'admin' capability (or if " "it's\n" " explicitly told to join the channel using the Admin.join command)." msgstr "" "Détermine si le bot joindra toujours les canaux sur lesquels il est " "invité. Si cette valeur est à False, le bot ne joindra un canal que si la " "personne l'invitant a la capacité 'admin' (ou si il lui a directement " "envoyé la commande Admin join)" #: src/conf.py:624 msgid "" "Supybot normally replies with the full help\n" " whenever a user misuses a command. If this value is set to True, the " "bot\n" " will only reply with the syntax of the command (the first line of the\n" " help) rather than the full help." msgstr "" "Supybot répond normalement avec une aide complète lorsque quelqu'un se " "trompe dans une commande. Si cette valeur est à True, le bot ne répondra " "qu'avec la syntaxe de la commande (la première ligne de l'aide), plutôt " "que par toute l'aide." #: src/conf.py:639 msgid "" "Determines what prefix characters the bot will\n" " reply to. A prefix character is a single character that the bot will " "use\n" " to determine what messages are addressed to it; when there are no " "prefix\n" " characters set, it just uses its nick. Each character in this string " "is\n" " interpreted individually; you can have multiple prefix chars\n" " simultaneously, and if any one of them is used as a prefix the bot will\n" " assume it is being addressed." msgstr "" "Détermine à quel caractère de préfixe le bot réagira. Un caractère de " "préfixe est un caractère servant à indiquer que le message est destiné " "au bot ; lorsqu'il n'y a pas de caractère de préfixe, vous ne pouvez " "qu'utiliser le nick du bot. Chaque caractère de cette chaîne sera " "interprété individuellement ; vous pouvez avoir plusieurs caractères " "simultanément, et un seul d'entre eux suffira pour s'adresser au bot." #: src/conf.py:648 msgid "" "Determines what strings the\n" " bot will reply to when they are at the beginning of the message. " "Whereas\n" " prefix.chars can only be one character (although there can be many of\n" " them), this variable is a space-separated list of strings, so you can\n" " set something like '@@ ??' and the bot will reply when a message is\n" " prefixed by either @@ or ??." msgstr "" "Détermine à quelles chaînes le bot répondra lorsqu'elles sont au début du " "message. Alors que prefex.chars n'est utilisé que pour un caractère seul " "(même si on peut en utiliser plusieurs), cette variable est une liste de " "chaînes séparées par des espaces, vous pouvez donc par exemple mettre " "'@@ ??' et le bot répondra lorsqu'un message est préfixé par @@ ou par ??." #: src/conf.py:655 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick, rather than with a prefix character." msgstr "" "Détermine si le bot will répondra lorsque des gens s'adresseront à lui " "par si nick, au lieu de ne répondre qu'aux caractères de préfixe." #: src/conf.py:658 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick at the end of the message, rather than at\n" " the beginning." msgstr "" "Détermine si le bot répondra lorsque les personnes mettent son nick à la " "fin d'un message, plutôt qu'au début." #: src/conf.py:662 msgid "" "Determines what extra nicks\n" " the bot will always respond to when addressed by, even if its current " "nick\n" " is something else." msgstr "" "Détermine à quels nicks supplémentaires le bot répondra lorsqu'on " "s'adressera à lui par ceux-ci, même si le nick est actuellement utilisé." #: src/conf.py:672 msgid "The operation succeeded." msgstr "Opération effectuée avec succès." #: src/conf.py:673 msgid "" "Determines what message the bot replies with when a command succeeded.\n" " If this configuration variable is empty, no success message will be\n" " sent." msgstr "" "Détermine quel message le bot répondra lorsqu'une commande a été " "effectuée avec succès. Si cette variable de configuration est vide, aucun " "message de succès ne sera envoyé." #: src/conf.py:678 msgid "" "An error has occurred and has been logged.\n" " Please contact this bot's administrator for more information." msgstr "" "Une erreur est survenue et a été logguée. Contactez l'administrateur du " "bot pour plus d'informations." #: src/conf.py:679 msgid "" "\n" " Determines what error message the bot gives when it wants to be\n" " ambiguous." msgstr "" "\n" "Détermine quel message d'erreur le bot donnera, quand il décidera de ne " "pas être plus précis." #: src/conf.py:684 msgid "" "An error has occurred and has been logged.\n" " Check the logs for more information." msgstr "" "Une erreur est survenue et a été logguée. R les logs pour plus " "d'informations." #: src/conf.py:685 msgid "" "Determines what error\n" " message the bot gives to the owner when it wants to be ambiguous." msgstr "" "Détermine quel message d'erreur le bot donnera au propriétaire lorsqu'il " "veut être ambigu." #: src/conf.py:689 msgid "" "Your hostmask doesn't match or your password\n" " is wrong." msgstr "" "Votre masque d'hôte ne correspond pas, ou votre mot de passe est " "incorrect." #: src/conf.py:690 msgid "" "Determines what message the bot replies with when\n" " someone tries to use a command that requires being identified or having " "a\n" " password and neither credential is correct." msgstr "" "Détermine quel message le bot répondra lorsque quelqu'un utiliser une " "commande qui nécessite d'être identifié." #: src/conf.py:696 msgid "" "I can't find %s in my user\n" " database. If you didn't give a user name, then I might not know what " "your\n" " user is, and you'll need to identify before this command might work." msgstr "" "Je ne peux trouver %s dans ma base de données d'utilisateurs. Si vous ne " "m'avez pas donné un nom d'utilisateur, je ne peux savoir qui est cet " "utilisateur, et vous devrez vous identifier avant que cette commande " "fonctionne." #: src/conf.py:699 msgid "" "Determines what error message the bot replies with when someone tries\n" " to accessing some information on a user the bot doesn't know about." msgstr "" "Détermine quel message d'erreur est envoyé lorsque quelqu'un essaye " "d'accéder à une information sur une personne que le bot ne connait pas." #: src/conf.py:703 msgid "" "You must be registered to use this command.\n" " If you are already registered, you must either identify (using the " "identify\n" " command) or add a hostmask matching your current hostmask (using the\n" " \"hostmask add\" command)." msgstr "" "Vous devez être enregistré(e) pour utiliser cette commande. Si vous êtes " "déjà enregistré(e), vous devez vous identifier (en utilisant la commande " "'identify') ou ajouter un masque d'hôte correspondant à votre masque " "d'hôte courant (en utilisant la commande 'hostmask add')." #: src/conf.py:706 msgid "" "Determines what error message the bot\n" " replies with when someone tries to do something that requires them to " "be\n" " registered but they're not currently recognized." msgstr "" "Détermine quel message d'erreur le bot renvoie lorsque quelqu'un essaye " "de faire quelque chose qui nécessit d'être enregistré, alors qu'il n'est " "pas reconnu." #: src/conf.py:711 msgid "" "You don't have the %s capability. If you\n" " think that you should have this capability, be sure that you are " "identified\n" " before trying again. The 'whoami' command can tell you if you're\n" " identified." msgstr "" "Vous n'avez pas la capacité %s. Si vous pensez que vous l'avez, assurez-" "vous d'être identifié(e) et réessayez. La commande 'whoami' vous dit si " "vous êtes identifié(e)." #: src/conf.py:714 msgid "" "Determines what error message is given when the bot\n" " is telling someone they aren't cool enough to use the command they tried " "to\n" " use." msgstr "" "Détermine quel message d'erreur est donné lorsqu'une personne tente " "d'utiliser une commande à laquelle il n'y pas accès." #: src/conf.py:719 msgid "" "You're missing some capability you need.\n" " This could be because you actually possess the anti-capability for the\n" " capability that's required of you, or because the channel provides that\n" " anti-capability by default, or because the global capabilities include\n" " that anti-capability. Or, it could be because the channel or\n" " supybot.capabilities.default is set to False, meaning that no commands " "are\n" " allowed unless explicitly in your capabilities. Either way, you can't " "do\n" " what you want to do." msgstr "" "Il vous manque une capacité. Ceci peut être dû au fait que vous possédez " "actuellement une anti-capacité quant à la commande que vous tentez " "d'utiliser, ou que le canal fourni cette anti-capacité par défaut, ou " "parce que les capacités globales incluent cette anti-capacité. Cela " "peut également être du au fait que supybot.capabilities.default est " "défini à False, ce qui signifie qu'une commande n'est autorisée par " "défaut (elles doivent être autorisées une par une par un administrateur). " "En clair, vous ne pouvez pas le faire." #: src/conf.py:727 msgid "" "Determines what generic error message is given when the bot is telling\n" " someone that they aren't cool enough to use the command they tried to " "use,\n" " and the author of the code calling errorNoCapability didn't provide an\n" " explicit capability for whatever reason." msgstr "" "Détermine le message d'erreur générique qui est donné par le bot pour " "dire à quelqu'un qu'il ou elle n'a pas les capacités requises pour " "utiliser une commande qu'il ou elle essaye d'utiliser, si l'auteur du " "code appelant errorNoCapability ne fourni pas d'information explicite " "sur la raison de ce refus." #: src/conf.py:733 msgid "" "That operation cannot be done in a\n" " channel." msgstr "Cette opération ne peut être faite sur un canal." #: src/conf.py:734 msgid "" "Determines what error messages the bot sends to people\n" " who try to do things in a channel that really should be done in\n" " private." msgstr "" "Détermine quel message d'erreur le bot envoie aux personnes qui tentent " "de faire quelque chose sur un canal, alors que cela doit être fait en " "privé." #: src/conf.py:739 msgid "" "This may be a bug. If you think it is,\n" " please file a bug report at\n" " ." msgstr "" "Ceci semble être un bug. Si vous pensez que c'en est un, veillez à " "envoyer un rapport de bug sur ." #: src/conf.py:742 msgid "" "Determines what message the bot sends when it thinks you've\n" " encountered a bug that the developers don't know about." msgstr "" "Détermine quel message le bot envoie quand il pense que l'on a rencontré " "un bug que les développeurs ne connaissent pas." #: src/conf.py:749 msgid "$Type #$id: $text (added by $username at $at)" msgstr "" #: src/conf.py:750 msgid "" "Format used by generic database plugins (Lart, Dunno, Prase, Success,\n" " Quote, ...) to show an entry. You can use the following variables:\n" " $type/$types/$Type/$Types (plugin name and variants), $id, $text,\n" " $at (creation time), $userid/$username/$nick (author)." msgstr "" #: src/conf.py:760 msgid "" "A floating point number of seconds to throttle\n" " snarfed URLs, in order to prevent loops between two bots snarfing the " "same\n" " URLs and having the snarfed URL in the output of the snarf message." msgstr "" "Un nombre à virgule flottante correspondant au temps (en secondes) à " "attendre avant de 'snarfer' la même URL, dans le but d'éviter que deux " "bots entrent ainsi en boucle infinie." #: src/conf.py:765 msgid "" "Determines the number of seconds\n" " between running the upkeep function that flushes (commits) open " "databases,\n" " collects garbage, and records some useful statistics at the debugging\n" " level." msgstr "" "Détermine le nombre de secondes entre deux exécutions de la fonction " "upkeep, qui rafraichit les bases de données ouvertes, vide le cache, et " "enregistre des statistiques utiles pour le débogage." #: src/conf.py:771 msgid "" "Determines whether the bot will periodically\n" " flush data and configuration files to disk. Generally, the only time\n" " you'll want to set this to False is when you want to modify those\n" " configuration files by hand and don't want the bot to flush its current\n" " version over your modifications. Do note that if you change this to " "False\n" " inside the bot, your changes won't be flushed. To make this change\n" " permanent, you must edit the registry yourself." msgstr "" "Détermine si le bot rafraichira périodiquement les fichiers de données et " "de configuration sur le disque. Générallement, on ne défini ceci à False " "que lorsque l'on veut modifier ces fichiers de configuration à la main, " "et que l'on ne veut pas que le bot supprime les modifications en écrivant " "par-dessus. Notez que si vous définissez ceci à False, les changements " "apportés aux fichiers n'auront pas pour autant une influence sur le bot. " "Si vous voulez rendre ce changement définitif, vous devez éditer le " "registre vous-même." #: src/conf.py:797 msgid "" "Determines what characters are valid for quoting\n" " arguments to commands in order to prevent them from being tokenized.\n" " " msgstr "" "Détermine quels caractères sont valides pour citer des arguments à des " "commandes, pour leur éviter d'être tokénisés." #: src/conf.py:804 msgid "" "Determines whether the bot will allow nested\n" " commands, which rule. You definitely should keep this on." msgstr "" "Détermine si le bot autorisera les commandes commandes imbriquées sur ce " "bot, ce qui est une fonctionnalité génial. Vous devriez vraiment garder " "ceci actif." #: src/conf.py:807 msgid "" "Determines what the maximum number of\n" " nested commands will be; users will receive an error if they attempt\n" " commands more nested than this." msgstr "" "Détermine le nombre maximum de commandes imbriquées ; les utilisateurs " "recevront une erreur si ils ou elles tentent d'en mettre plus." #: src/conf.py:816 msgid "" "Supybot allows you to specify what brackets\n" " are used for your nested commands. Valid sets of brackets include\n" " [], <>, {}, and (). [] has strong historical motivation, but <> or\n" " () might be slightly superior because they cannot occur in a nick.\n" " If this string is empty, nested commands will not be allowed in this\n" " channel." msgstr "" "Supybot vous permet de spécifier quels crochets sont utilisés pour vos " "commandes imbriquées. Sont disponibles : [], <>, {}, et (). [] est très " "largement le plus utilisé, car leur utilisation est la plus simple pour " "des claviers qwerty. <> et () tirent leur avantage du fait qu'ils ne " "peuvent apparaître dans un nick. Si cette chaîne est vide, les commandes " "imbriquées ne seront pas autorisées sur ce canal." #: src/conf.py:823 msgid "" "Supybot allows nested commands. Enabling this\n" " option will allow nested commands with a syntax similar to UNIX pipes, " "for\n" " example: 'bot: foo | bar'." msgstr "" "Supybot permet les commandes imbriquées. Activer cette option permettra " "aux commandes imbriquées d'utiliser une syntaxe similaire aux 'pipes' " "UNIX, par exemple : 'bot: foo | bar'." #: src/conf.py:828 msgid "" "Determines what commands have default\n" " plugins set, and which plugins are set to be the default for each of " "those\n" " commands." msgstr "" "Détermine, lorsque l'on appelle une commande appartenant à plusieurs " "plugins, lequel sera choisi par défaut pour chacune de ces commandes." #: src/conf.py:834 msgid "" "Determines what plugins automatically get precedence over all\n" " other plugins when selecting a default plugin for a command. By\n" " default, this includes the standard loaded plugins. You probably\n" " shouldn't change this if you don't know what you're doing; if you " "do\n" " know what you're doing, then also know that this set is\n" " case-sensitive." msgstr "" "Détermine quels plugins sont prioritaires sur tous les autres lorsque " "l'on recherche le plugin par défaut d'une commande. Par défaut, cela " "inclu les plugins standards chargés. Vous ne devriez probablement pas " "changer ceci, à moins de savoir ce que vous faites ; si vous savez ce " "que vous faites, sachez que les noms de plugins sont sensibles à la " "classe." #: src/conf.py:844 msgid "" "Allows this bot's owner user to use commands\n" " that grants them shell access. This config variable exists in case you " "want\n" " to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)\n" " from gaining shell access to the bot's server by impersonating the " "owner.\n" " Setting this to False also disables plugins and commands that can be\n" " used to indirectly gain shell access." msgstr "" "Autorise les propriétaires du bot à utiliser des commandes qui leur donne un " "accès shell. Cette variable de configuration existe au cas où vous voulez " "empêcher des attaques MITM du réseau IRC lui-même (IRCd or IRCops " "vulnérables) de permettre un accès shell au serveur du bot en se faisant " "passer pour les propriétaires. Définir ceci à False désactive aussi les " "plugins et commandes qui peuvent être utilisées pour indirectement gagner un " "accès shell." #: src/conf.py:859 msgid "" "Determines the interval used for\n" " the history storage." msgstr "Détermine l’intervalle utilisé pour le stockage de l’historique." #: src/conf.py:862 msgid "" "Determines whether the bot will defend itself\n" " against command-flooding." msgstr "" "Détermine si le bot se défendra contre les attaques par flood de " "commandes." #: src/conf.py:865 msgid "" "Determines how many commands users are\n" " allowed per minute. If a user sends more than this many commands in " "any\n" " 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.punishment seconds." msgstr "" "Détermine le nombre maximum de commandes qu'une personne peut envoyer en " "une minute. Si une personne dépasse cette limite, elle sera ignoré " "supybot.abuse.flood.command.punishment secondes." #: src/conf.py:870 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with commands." msgstr "" "Détermine combien de secondes le bot ignorera les personnes qui le " "floodent de commandes." #: src/conf.py:873 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for command flooding." msgstr "" "Détermine si le bot notifiera les personnes lorsqu'elles se font ignorer " "pour flood de commande." #: src/conf.py:877 msgid "" "Determines whether the bot will defend itself\n" " against invalid command-flooding." msgstr "" "Détermine si le bot se défendra contre le flood de commandes invalides." #: src/conf.py:880 msgid "" "Determines how many invalid commands users\n" " are allowed per minute. If a user sends more than this many invalid\n" " commands in any 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.invalid.punishment seconds. Typically, " "this\n" " value is lower than supybot.abuse.flood.command.maximum, since it's far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" "Détermine le nombre maximum de commandes invalies qu'une personne peut " "envoyer en une minute. Si une personne dépasse cette limite, elle sera " "ignoré supybot.abuse.flood.command.invalid.punishment secondes. " "Typiquement, cette valeur est plus petite que value is lower than " "supybot.abuse.flood.command.maximum, car il est plus probable que les " "personnes floodent avec des commandes invalides qu'avec des commandes " "valides." #: src/conf.py:888 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with invalid commands. Typically, this\n" " value is higher than supybot.abuse.flood.command.punishment, since it's " "far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" "Détermine combien de secondes le bot ignorera les personnes qui le " "floodent de commandes invalides. Typiquement,cette valeur est plus " "grande que value is lower than supybot.abuse.flood.command.punishment, " "car il est plus probable que les personnes floodent avec des commandes " "invalides qu'avec des commandes valides." #: src/conf.py:894 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for invalid command flooding." msgstr "" "Détermine si le bot notifiera les personnes lorsqu'elles se font ignorer " "pour flood de commande." #: src/conf.py:903 msgid "" "Determines the default length of time a\n" " driver should block waiting for input." msgstr "" "Détermine la durée par défaut durant laquelle un moteur réseau peut être " "bloquant, en attente de données entrantes." #: src/conf.py:911 msgid "" "Determines what driver module the \n" " bot will use. Current, the only (and default) driver is Socket." msgstr "" #: src/conf.py:915 msgid "" "Determines the maximum time the bot will\n" " wait before attempting to reconnect to an IRC server. The bot may, of\n" " course, reconnect earlier if possible." msgstr "" "Détermine le temps maximum durant lequel le bot attendra avant de se " "reconnecter à un serveur IRC. Le bot peut, bien sûr, se reconnecter plus " "tôt, si c'est possible." #: src/conf.py:967 msgid "" "Determines what directory configuration data is\n" " put into." msgstr "Détermine dans quel répertoire les données de configuration sont." #: src/conf.py:970 msgid "Determines what directory data is put into." msgstr "Détermine dans quel répertoire les données sont." #: src/conf.py:972 msgid "" "Determines what directory backup data is put\n" " into. Set it to /dev/null to disable backup (it is a special value,\n" " so it also works on Windows and systems without /dev/null)." msgstr "" "Détermine dans quel dossier les sauvegardes seront effectuées. Définissez " "cette variable à /dev/null pour désactiver la sauvegarde (c'est une valeur " "spéciale, donc elle marchera également sur Windows et les systèmes qui n'ont " "pas de /dev/null)." #: src/conf.py:979 msgid "" "Determines what directory temporary files\n" " are put into." msgstr "Détermine dans quel répertoire les fichiers temporaires sont." #: src/conf.py:982 msgid "" "Determines what directory files of the\n" " web server (templates, custom images, ...) are put into." msgstr "" "Détermine dans quel répertoire seront les fichiers du serveur web " "(templates, images personnalisées, ...)." #: src/conf.py:995 msgid "" "Determines what directories\n" " the bot will look for plugins in. Accepts a comma-separated list of\n" " strings.\n" " This means that to add another directory, you can nest the former value " "and\n" " add a new one. E.g. you can say: bot: 'config supybot.directories." "plugins\n" " [config supybot.directories.plugins], newPluginDirectory'." msgstr "" "Détermine dans quel répertoire le bot recherchera les plugins. Accepte " "une liste de chaînes séparées par des virgules. Ce qui signifie que si " "vous voulez ajouter un autre répertoire, vous pouvez imbriquer la forme " "actuelle la nouvelle. Par exemple, vous pouvez dire : 'config supybot." "directories.plugins [config supybot.directories.plugins], " "nouveauRepertoireDePlugins'." #: src/conf.py:1003 msgid "" "List of all plugins that were\n" " ever loaded. Currently has no effect whatsoever. You probably want to " "use\n" " the 'load' or 'unload' commands, or edit supybot.plugins.\n" " instead of this." msgstr "" #: src/conf.py:1008 msgid "" "Determines whether the bot will always load\n" " important plugins (Admin, Channel, Config, Misc, Owner, and User)\n" " regardless of what their configured state is. Generally, if these " "plugins\n" " are configured not to load, you didn't do it on purpose, and you still\n" " want them to load. Users who don't want to load these plugins are " "smart\n" " enough to change the value of this variable appropriately :)" msgstr "" "Détermine si le bot chargera toujours les plugins importants (Admin, " "Channel, Config, Misc, Owner, et User), peu importe l'état de la " "configuration. Généralement, si ces plugins sont configurés pour ne pas " "être chargés, vous ne pourrez plus les charger, et finalement, vous " "voudrez le faire. Les personnes qui ne souhaitent pas charger ces plugins " "sont suffisement intelligents pour changer la valeur de cette variable de " "façon appropriée :)" #: src/conf.py:1036 msgid "" "Determines what databases are available for use. If this\n" " value is not configured (that is, if its value is empty) then sane " "defaults\n" " will be provided." msgstr "" "Détermine quelles bases de données sont disponibles. si cette valeur " "n'est pas configurée (c'est à dire si sa valeur est vide), alors une " "manière par défaut saine sera utilisée." #: src/conf.py:1042 msgid "" "Determines what filename will be used\n" " for the users database. This file will go into the directory specified " "by\n" " the supybot.directories.conf variable." msgstr "" "Détermine quel nom de fichier sera utilisé pour la base de données des " "utilisateurs. Ce fichiers ira dans le répertoire spécifié par la variable " "supybot.directories.conf." #: src/conf.py:1046 msgid "" "Determines how long it takes identification to\n" " time out. If the value is less than or equal to zero, identification " "never\n" " times out." msgstr "" "Détermine combien de temps au maximum une personne peut rester identifié. " "Si cette valeur est inférieure ou égale à zéro, l'identification " "n'expirera jamais." #: src/conf.py:1050 msgid "" "Determines whether the bot will allow users to\n" " unregister their users. This can wreak havoc with already-existing\n" " databases, so by default we don't allow it. Enable this at your own " "risk.\n" " (Do also note that this does not prevent the owner of the bot from " "using\n" " the unregister command.)\n" " " msgstr "" "Détermine si le bot autorisera les utilisateurs à se désenregistrer. Ceci " "peut provoquer des dégâts avec des bases de données déjà existantes, " "donc, nous ne l'autorisons pas par défaut. Activez ceci à vos risques et " "périls. (Notez également que ceci n'empêche pas le propriétaire du bot de " "se désenregistrer.)" #: src/conf.py:1059 msgid "" "Determines what filename will be used\n" " for the ignores database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Détermine quel nom de fichier sera utilisé pour stocker la base de " "données d'ignorance. Ce fichiers ira dans le répertoire spécifié par la " "variable supybot.directories.conf." #: src/conf.py:1065 msgid "" "Determines what filename will be used\n" " for the channels database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Détermine quel nom de fichier sera utilisé pour stocker la base de " "données de canaux. Ce fichiers ira dans le répertoire spécifié par la " "variable supybot.directories.conf." #: src/conf.py:1071 #, fuzzy msgid "" "Determines what filename will be used\n" " for the networks database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Détermine quel nom de fichier sera utilisé pour stocker la base de " "données d'ignorance. Ce fichiers ira dans le répertoire spécifié par la " "variable supybot.directories.conf." #: src/conf.py:1103 msgid "" "Determines whether the bot will require user\n" " registration to use 'add' commands in database-based Supybot\n" " plugins." msgstr "" "Détermine si le bot requiert que les utilisateurs/trices soient enregistrées " "pour utiliser les commandes 'add' des plugins Supybot basés sur des bases de " "données." #: src/conf.py:1107 msgid "" "Determines whether database-based plugins that\n" " can be channel-specific will be so. This can be overridden by " "individual\n" " channels. Do note that the bot needs to be restarted immediately after\n" " changing this variable or your db plugins may not work for your " "channel;\n" " also note that you may wish to set\n" " supybot.databases.plugins.channelSpecific.link appropriately if you " "wish\n" " to share a certain channel's databases globally." msgstr "" "Détermine si des plugins basés sur une base de données peuvent être " "spécifiques à un canal. Ceci peut être surchargé pour chaque canal. Notez " "que le bot doit être redémarré immédiatement après avoir changé cette " "variable, ou vos plugins basés sur une base de données pourraient ne pas " "marcher pour votre canal ; notez également qui vous pouvez vouloir " "définir supybot.databases.plugins.channelSpecific.link de manière " "appropriée si vous voulez partager les bases de données de certains " "canaux." #: src/conf.py:1115 msgid "" "Determines what channel global\n" " (non-channel-specific) databases will be considered a part of. This is\n" " helpful if you've been running channel-specific for awhile and want to " "turn\n" " the databases for your primary channel into global databases. If\n" " supybot.databases.plugins.channelSpecific.link.allow prevents linking, " "the\n" " current channel will be used. Do note that the bot needs to be " "restarted\n" " immediately after changing this variable or your db plugins may not " "work\n" " for your channel." msgstr "" "Détermine quelles bases de données de canaux seront considérées comme " "faisant partie de la base de données globale. Ceci est utile si vous " "utilisez une base de données spécifique à un canal depuis longtemps, et " "que vous voulez faire passer les bases de données de votre canal " "principal en bases de données globales. Si supybot.databases.plugins." "channelSpecific.link.allow empêche la liaison, le canal courant sera " "utilisé. Notez que le bot doit être redémarré immédiatement après avoir " "changé cette variable ou vos plugins basés sur une base de données " "pourraient ne pas fonctionner." #: src/conf.py:1124 msgid "" "Determines whether another channel's global\n" " (non-channel-specific) databases will be allowed to link to this " "channel's\n" " databases. Do note that the bot needs to be restarted immediately " "after\n" " changing this variable or your db plugins may not work for your " "channel.\n" " " msgstr "" "Détermine si les bases de données globales d'un autre canal peuvent êtres " "liées à celles de ce canal. Notez que le bot doit être redémarré " "immédiatement après avoir changé cette variable ou vos plugins basés sur " "une base de données pourraient ne pas marcher pour votre canal." #: src/conf.py:1142 msgid "" "Determines\n" " whether CDB databases will be allowed as a database implementation." msgstr "" "Détermine si les bases de données CDB seront autorisées comme " "implémentation de base de données." #: src/conf.py:1145 msgid "" "Determines how often CDB databases will have\n" " their modifications flushed to disk. When the number of modified " "records\n" " is greater than this fraction of the total number of records, the " "database\n" " will be entirely flushed to disk." msgstr "" "Détermine la fréquence de sauvegarde des bases de données CDB sur le " "disque. Lorsque le nombre d'enregistrements modifiés est plus grand que " "le nombre d'enregistrements non modifiés, la base de données sera " "entièrement enregistrée sur le disque." #: src/conf.py:1237 msgid "" "Determines what will be used as the\n" " default banmask style." msgstr "Détermine le style de masque de bannissement utilisé par défaut." #: src/conf.py:1241 msgid "" "Determines whether the bot will strictly\n" " follow the RFC; currently this only affects what strings are\n" " considered to be nicks. If you're using a server or a network that\n" " requires you to message a nick such as services@this.network.server\n" " then you you should set this to False." msgstr "" "Détermine si le bot suivra strictement la RFC ; actuellement, cela " "n'affecte que les chaînes considérées comme des nicks. Si vous utilisez " "un serveur ou un réseau qui requiert que vous envoyiez un message à un " "nick tel queservices@this.network.server, vous devriez défini ceci à " "False." #: src/conf.py:1248 msgid "" "Determines whether the bot will enable\n" " draft/experimental extensions of the IRC protocol. Setting this to True\n" " may break your bot at any time without warning and/or break your\n" " configuration irreversibly. So keep it False unless you know what you " "are\n" " doing." msgstr "" #: src/conf.py:1255 msgid "" "Determines what certificate file (if any) the bot\n" " will use connect with SSL sockets by default." msgstr "" "Détermine quel fichier de certificat (s’il y en a un) le bot utilisera par " "défaut pour se connecter par SSL." #: src/conf.py:1259 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. Many people might choose +i; " "some\n" " networks allow +x, which indicates to the auth services on those " "networks\n" " that you should be given a fake host." msgstr "" "Détermine quels modes d'utilisateur le bot demandera au serveur lorsqu'il " "s'y connecte. Certaines personnes voudront le +i ; certains réseaux " "autorisent +x, ce qui indique aux services du réseau que l'on veut " "'cloaker' (masquer) notre masque d'hôte." #: src/conf.py:1265 msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv4." msgstr "" "Détermine quelle vhost le bot bindera avant de se connecter à un serveur " "(IRC, HTTP, ...) par IPv4." #: src/conf.py:1269 msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv6." msgstr "" "Détermine quelle vhost le bot bindera avant de se connecter à un serveur " "(IRC, HTTP, ...) par IPv6." #: src/conf.py:1273 msgid "" "Determines how many old messages the bot will\n" " keep around in its history. Changing this variable will not take " "effect\n" " on a network until it is reconnected." msgstr "" "Détermine combien de vieux messages le bot gardera dans son historique. " "Changer cette variable ne prend effet qu'après pour un réseau qu'après une " "reconnexion." #: src/conf.py:1278 msgid "" "A floating point number of seconds to throttle\n" " queued messages -- that is, messages will not be sent faster than once " "per\n" " throttleTime seconds." msgstr "" "Un nombre à virgule flottante, correspondant à la mise en attente des " "messages à envoyé ; c'est à dire que les messages ne seront pas envoyé " "plus vite que 1 par throttleTime secondes." #: src/conf.py:1283 msgid "" "Determines whether the bot will send PINGs to\n" " the server it's connected to in order to keep the connection alive and\n" " discover earlier when it breaks. Really, this option only exists for\n" " debugging purposes: you always should make it True unless you're " "testing\n" " some strange server issues." msgstr "" "Détermine si le bot enverra des PINGs au serveurs auxquels il est " "connecté dans le but de garder la connexion active et détecter plus tôt " "lorsqu'elle est rompue. En fait, cette option existe surtout pour le " "débogage : vous devriez toujours laisser cette option à True excepté si " "il vous arrive des trucs bizarres avec le serveur." #: src/conf.py:1290 msgid "" "Determines the number of seconds between sending\n" " pings to the server, if pings are being sent to the server." msgstr "" "Détermine le nombre de secondes entre deux envois de PING au serveur, si " "les pings sont envoyés au serveur." #: src/conf.py:1295 msgid "" "Determines whether the bot will refuse\n" " duplicated messages to be queued for delivery to the server. This is a\n" " safety mechanism put in place to prevent plugins from sending the same\n" " message multiple times; most of the time it doesn't matter, unless " "you're\n" " doing certain kinds of plugin hacking." msgstr "" "Détermine si le bot refusera de dupliquer les messages à mettre dans la " "queue d'envoi un serveur. C'est un mécanisme de sécurité mis en place " "pour éviter que les plugins envoient le même message plusieurs fois ; la " "plupart du temps, vous n'avez pas à vous en préoccuper, à moins que vous " "ne bidouilliez des plugins." #: src/conf.py:1303 msgid "" "Determines how many seconds must elapse between\n" " JOINs sent to the server." msgstr "" "Détermine combien de secondes doivent s'écouler entre deux envois de JOIN " "au serveur." #: src/conf.py:1311 msgid "" "Determines how many bytes the bot will\n" " 'peek' at when looking through a URL for a doctype or title or " "something\n" " similar. It'll give up after it reads this many bytes, even if it " "hasn't\n" " found what it was looking for." msgstr "" "Détermine combien d'octets le bot 'regarder' lorsqu'il cherchera le " "doctype, le title, ou autre chose dans une URL. Après avoir lu ces " "octets, le bot abandonnera sa recherche." #: src/conf.py:1335 msgid "" "Determines what HTTP proxy all HTTP requests should go\n" " through. The value should be of the form 'host:port'." msgstr "" "Détermine par quel proxy toutes les requêtes HTTP devraient passer. Cette " "valeur doit être de la forme 'host:port'." #: src/conf.py:1375 msgid "" "If set, the Accept-Language HTTP header will be set to this\n" " value for requests. Useful for overriding the auto-detected language " "based on\n" " the server's location." msgstr "" #: src/conf.py:1381 msgid "" "If set, the User-Agent HTTP header will be set to a randomly\n" " selected value from this comma-separated list of strings for requests." msgstr "" #: src/conf.py:1389 #, fuzzy msgid "" "Determines whether server certificates\n" " will be verified, which checks whether the server certificate is signed\n" " by a known certificate authority, and aborts the connection if it is " "not.\n" " This is assumed to be True of serverFingerprints or " "authorityCertificate\n" " is set." msgstr "" "Détermine si les certificats serveurs sont vérifiés, ce qui vérifie que le " "certificat est signé par une autorité de certification reconnue, et termine " "la connexion si ce n'est pas le cas." #: src/conf.py:1416 msgid "" "If true, uses IPV6_V6ONLY to disable\n" " forwaring of IPv4 traffic to IPv6 sockets. On *nix, has the same\n" " effect as setting kernel variable net.ipv6.bindv6only to 1." msgstr "" "Si vrai, utilise IPV6_V6ONLY pour désactive le transfère du traffic IPv4 à " "des sockets IPv6. Sur *nix, cela a le même effet que de définir l'option " "noyau net.ipv6.bindv6only à 1" #: src/conf.py:1420 msgid "" "Space-separated list of IPv4 hosts the HTTP server\n" " will bind." msgstr "" "Liste d'adresses IPv4 séparées par des espaces que le serveur HTTP va « " "binder »." #: src/conf.py:1423 msgid "" "Space-separated list of IPv6 hosts the HTTP server will\n" " bind." msgstr "" "Liste d'adresses IPv6 séparées par des espaces que le serveur HTTP va « " "binder »." #: src/conf.py:1426 msgid "" "Determines what port the HTTP server will\n" " bind." msgstr "Détermine à quel port le serveur HTTP va s'attacher." #: src/conf.py:1429 msgid "" "Determines whether the server will stay\n" " alive if no plugin is using it. This also means that the server will\n" " start even if it is not used." msgstr "" "Détermine si le serveur restera lancé si aucun plugin ne l'utilise. Cela " "signifie également que le serveur va démarrer même si il n'est pas utilisé." #: src/conf.py:1433 msgid "" "Determines the path of the file served as\n" " favicon to browsers." msgstr "Détermine le chemin du fichier servi comme favicon aux navigateurs." #: src/conf.py:1436 msgid "" "Determines the public URL of the server.\n" " By default it is http://:/, but you will want to change\n" " this if there is a reverse proxy (nginx, apache, ...) in front of\n" " the bot." msgstr "" #: src/conf.py:1446 msgid "" "Determines whether the bot will ignore\n" " unidentified users by default. Of course, that'll make it\n" " particularly hard for those users to register or identify with the bot\n" " without adding their hostmasks, but that's your problem to solve." msgstr "" "Détermine si le bot ignorera par défaut les personnes qui ne sont pas " "enregistrées. Bien sûr, cela rendra très difficile pour les utilisateurs/" "trices de s'enregistrer ou de s'identifier, mais c'est votre problème." #: src/conf.py:1453 msgid "" "A string that is the external IP of the bot. If this is the\n" " empty string, the bot will attempt to find out its IP dynamically " "(though\n" " sometimes that doesn't work, hence this variable). This variable is not " "used\n" " by Limnoria and its built-in plugins: see supybot.protocols.irc.vhost /\n" " supybot.protocols.irc.vhost6 to set the IRC bind host, and\n" " supybot.servers.http.hosts4 / supybot.servers.http.hosts6 to set the " "HTTP\n" " server bind host." msgstr "" "Une chaine qui est l'adresse IP externe du bot. Si c'est la chaine vide, le " "bot essayera de détermine son adresse IP dynamiquement (ce qui ne marche pas " "toujours, d'où cette variable). Cette variable n'est pas utilisée par " "Limnoria ou ses plugins par défaut : voyez supybot.protocols.irc.vhost / " "supybot.protocols.irc.vhost6 pour définir le « bind host » pour IRC, et " "supybot.servers.http.hosts4 / supybot.servers.http.hosts6 pour HTTP." #: src/conf.py:1472 msgid "" "Determines what the default timeout for socket\n" " objects will be. This means that *all* sockets will timeout when this " "many\n" " seconds has gone by (unless otherwise modified by the author of the " "code\n" " that uses the sockets)." msgstr "" "Détermine quel timeout est utilisé par défaut pour les sockets. Ceci " "signifie que *toutes* les sockets expireront au bout de cette durée (à " "moins que l'auteur du code n'ait changé cette valeur)." #: src/conf.py:1478 msgid "" "Determines what file the bot should write its PID\n" " (Process ID) to, so you can kill it more easily. If it's left unset (as " "is\n" " the default) then no PID file will be written. A restart is required " "for\n" " changes to this variable to take effect." msgstr "" "Détermine dans quel fichier le bot écrit son PID (Process ID), ce qui " "vous permet de le killer plus facilement. Si cette valeur est laissée " "vide, le PID ne sera écrit dans aucun fichier. Un redémarrage est requis " "pour que cette variable prenne effet." #: src/conf.py:1488 msgid "" "Determines whether the bot will automatically\n" " thread all commands." msgstr "Détermine si le bot threadera automatiquement toutes les commandes." #: src/conf.py:1491 msgid "" "Determines whether the bot will automatically\n" " flush all flushers *very* often. Useful for debugging when you don't " "know\n" " what's breaking or when, but think that it might be logged." msgstr "" "Détermine si le bot videra automatiquement le cache *très* souvent. Utile " "pour déboguer lorsque vous ne savez pas ce qui ne marche pas, mais que " "vous pensez que ça peut être loggué." #: src/httpserver.py:62 msgid "Supybot Web server index" msgstr "Index du serveur Web de Supybot" #: src/httpserver.py:67 msgid "Here is a list of the plugins that have a Web interface:" msgstr "Voici une liste des plugins qui ont une interface Web :" #: src/httpserver.py:245 msgid "" "\n" " This is a default response of the Supybot HTTP server. If you see this\n" " message, it probably means you are developing a plugin, and you have\n" " neither overriden this message or defined an handler for this query." msgstr "" "\n" " Ceci est la réponse par défaut du serveur HTTP de Supybot. Si vous " "voyez ce message, cela signifie probablement que vous êtes en train de " "développer un plugin, et que vous n'avez ni outrepassé ce message ni " "défini un gestionnaire pour cette requête." #: src/httpserver.py:291 msgid "" "\n" " I am a pretty clever IRC bot, but I suck at serving Web pages, " "particulary\n" " if I don't know what to serve.\n" " What I'm saying is you just triggered a 404 Not Found, and I am not\n" " trained to help you in such a case." msgstr "" "\n" " Je suis un robot IRC très intelligent, mais je suis nul dès qu'il s'agit " "de servir des pages Web, en particulier lorsque je ne sais pas quoi " "servir. Ce que j'essaye de dire, c'est que vous venez tout juste de " "déclencher une erreur 404 Not Found, and je ne suis pas entraîné à vous " "aider dans un tel cas." #: src/httpserver.py:310 msgid "Request not handled." msgstr "Requête non gérée." #: src/httpserver.py:317 msgid "No plugins available." msgstr "Aucun plugin disponible." #: src/httpserver.py:335 src/httpserver.py:353 src/httpserver.py:391 msgid "Request not handled" msgstr "Requête non gérée." #: src/httpserver.py:378 msgid "No favicon set." msgstr "Pas de favicon définie." #: src/ircutils.py:573 msgid "is an op on %L" msgstr "est op sur %L" #: src/ircutils.py:575 msgid "is a halfop on %L" msgstr "est halfop sur %L" #: src/ircutils.py:577 msgid "is voiced on %L" msgstr "est voice sur %L" #: src/ircutils.py:580 msgid "is also on %L" msgstr "est aussi sur %L" #: src/ircutils.py:582 msgid "is on %L" msgstr "est sur %L" #: src/ircutils.py:585 msgid "isn't on any publicly visible channels" msgstr "n'est sur aucun canal visible" #: src/ircutils.py:593 src/ircutils.py:594 src/ircutils.py:600 msgid "" msgstr "" #: src/ircutils.py:602 msgid " %s is away: %s." msgstr "%s est absent/e : %s." #: src/ircutils.py:607 msgid " identified" msgstr " identifié" #: src/ircutils.py:613 msgid "%s (%s) has been%s on server %s since %s (idle for %s). %s %s.%s" msgstr "" "%s (%s) est% sur le serveur %s depuis %s (inactif/ive pendant %s). %s %s.%s" #: src/ircutils.py:617 msgid "%s (%s) has been%s on server %s and disconnected on %s." msgstr "%s (%s) est% sur le serveur %s et s'est déconnect(e) à %s" #: src/questions.py:61 msgid "Sorry, that response was not an option." msgstr "Désolé, cette réponse n'est pas l'une des options." #: src/questions.py:110 msgid "Sorry, you must enter a value." msgstr "Désolé, vous devez entrer une valeur." #: src/questions.py:130 msgid "Enter password: " msgstr "Entrez un mot de passe : " #: src/questions.py:132 msgid "Re-enter password: " msgstr "Entrez à nouveau le mot de passe : " #: src/questions.py:145 msgid "Passwords don't match." msgstr "Les mots de passe ne correspondent pas." #: src/registry.py:219 #, fuzzy msgid "%r is not a valid entry in %r" msgstr "%s n'est pas un(e) %s valide." #: src/registry.py:568 msgid "Value must be either True or False (or On or Off), not %r." msgstr "La valeur doit être True ou False (ou On ou Off), pas %r." #: src/registry.py:585 msgid "Value must be an integer, not %r." msgstr "La valeur doit être un entrier, pas %r." #: src/registry.py:595 msgid "Value must be a non-negative integer, not %r." msgstr "La valeur doit être un entier non négatif, pas %r." #: src/registry.py:604 msgid "Value must be positive (non-zero) integer, not %r." msgstr "La valeur doit être un entier positif (non nul), pas %r." #: src/registry.py:613 msgid "Value must be a floating-point number, not %r." msgstr "La valeur doit être un nombre à virgule flottante, pas %r." #: src/registry.py:629 msgid "Value must be a floating-point number greater than zero, not %r." msgstr "" "La valeur doit être un nombre à virgule flottante plus grand que zéro, pas " "%r." #: src/registry.py:640 msgid "Value must be a floating point number in the range [0, 1], not %r." msgstr "" "La valeur doit être un nombre à virgule flottante compris entre 0 et 1, pas " "%r." #: src/registry.py:655 msgid "Value should be a valid Python string, not %r." msgstr "La valeur doit être une chaîne Python valide, pas %r." #: src/registry.py:693 msgid "Valid values include %L." msgstr "Les valeurs valides sont %L." #: src/registry.py:695 msgid "Valid values include %L, not %%r." msgstr "Les valeurs valides sont %L, pas %%r." #: src/registry.py:769 msgid "Value must be a valid regular expression, not %r." msgstr "La valeur doit être une expression rationnelle valide, pas %r." #: src/utils/gen.py:113 msgid "year" msgstr "année" #: src/utils/gen.py:116 msgid "week" msgstr "semaine" #: src/utils/gen.py:119 msgid "day" msgstr "jour" #: src/utils/gen.py:122 msgid "hour" msgstr "heure" #: src/utils/gen.py:126 msgid "minute" msgstr "minute" #: src/utils/gen.py:129 msgid "second" msgstr "seconde" #: src/utils/gen.py:138 msgid "%s ago" msgstr "il y a %s" #: src/utils/str.py:349 msgid "and" msgstr "et" #~ msgid "" #~ "Determines what driver module the \n" #~ " bot will use. The default is Socket which is simple and stable \n" #~ " and supports SSL. Twisted doesn't work if the IRC server which \n" #~ " you are connecting to has IPv6 (most of them do)." #~ msgstr "" #~ "Détermine quel module « pilote » le bot utilise pour se connecter à IRC. " #~ "Par défaut il s'agit de Socket, qui est simple et stable, et supporte " #~ "SSL. Twisted ne fonctionne pas sur le serveur IRC auquel vous vous " #~ "connectez supporte IPv6 (ce qui est le cas de la plupart)." #~ msgid "" #~ "Determines what plugins will\n" #~ " be loaded." #~ msgstr "Détermine quels plugins seront chargés." #~ msgid "" #~ "Determines what driver module the bot\n" #~ " will use. Socket, a simple driver based on timeout sockets, is used " #~ "by\n" #~ " default because it's simple and stable. Twisted is very stable and " #~ "simple,\n" #~ " and if you've got Twisted installed, is probably your best bet." #~ msgstr "" #~ "Détermine quel moteur réseau le bot utilisera. Socket, est un simple " #~ "moteur basé sur des sockets bloquantes à timeout ; c'est celui utilisé " #~ "par défaut. Twisted est très stable et simple, et, si vous avez " #~ "Twisted installé, c'est probablement le meilleur choix." #~ msgid "" #~ "A string that is the external IP of the bot. If this is the\n" #~ " empty string, the bot will attempt to find out its IP dynamically " #~ "(though\n" #~ " sometimes that doesn't work, hence this variable)." #~ msgstr "" #~ "Une chaîne qui est une IP externe du bot. Si elle est vide, le bot " #~ "recherchera cette IP dynamiquement (ce qui, parfois, ne marche pas ; " #~ "dans ce cas, utilisez cette variable)." #~ msgid "" #~ "Determines the content of the robots.txt file,\n" #~ " served on the server to search engine." #~ msgstr "" #~ "Détermine quel sera le contenu du fichier robots.txt, servi par le " #~ "serveur aux moteurs de recherche." #~ msgid "" #~ "Determines what directory backup data is put\n" #~ " into." #~ msgstr "Détermine dans quel répertoire les sauvegardes des données sont." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/fr.py0000644000175000017500000000724514535072470014533 0ustar00valval# -*- encoding: utf8 -*- ### # Copyright (c) 2010, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### """ Supybot utility functions localization in French. """ def pluralize(s): """Returns the plural of s. """ lowered = s.lower() if lowered.endswith('ou') and \ lowered in ['bijou', 'caillou', 'chou', 'genou', 'hibou', 'joujou', 'pou']: return s + 'x' elif lowered.endswith('al') and \ lowered not in ['bal', 'carnaval', 'chacal', 'festival', 'récital', 'régal', 'cal', 'étal', 'aval', 'caracal', 'val', 'choral', 'corral', 'galgal', 'gayal']: return s[0:-2] + 'aux' elif lowered.endswith('ail') and \ lowered not in ['bail', 'corail', 'émail', 'soupirail', 'travail', 'ventail', 'vitrail', 'aspirail', 'fermail']: return s[0:-3] + 'aux' elif lowered.endswith('eau'): return s + 'x' elif lowered == 'pare-feu': return s elif lowered.endswith('eu') and \ lowered not in ['bleu', 'pneu', 'émeu', 'enfeu']: # Note: when 'lieu' is a fish, it has a 's' ; else, it has a 'x' return s + 'x' else: return s + 's' def depluralize(s): """Returns the singular of s.""" lowered = s.lower() if lowered.endswith('aux') and \ lowered in ['baux', 'coraux', 'émaux', 'soupiraux', 'travaux', 'ventaux', 'vitraux', 'aspiraux', 'fermaux']: return s[0:-3] + 'ail' elif lowered.endswith('aux'): return s[0:-3] + 'al' else: return s[0:-1] def ordinal(i): """Returns i + the ordinal indicator for the number. Example: ordinal(3) => '3ème' """ i = int(i) if i == 1: return '1er' else: return '%sème' % i def be(i): """Returns the form of the verb 'être' based on the number i.""" # Note: this function is used only for the third person if i == 1: return 'est' else: return 'sont' def has(i): """Returns the form of the verb 'avoir' based on the number i.""" # Note: this function is used only for the third person if i == 1: return 'a' else: return 'ont' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/it.po0000644000175000017500000021054714535072470014527 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot-fr\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-07-05 00:13+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" #: src/callbacks.py:217 msgid "Error: " msgstr "Errore: " #: src/callbacks.py:233 msgid "Error: I tried to send you an empty message." msgstr "Errore: ho cercato di inviarti un messaggio vuoto." #: src/callbacks.py:361 msgid "" "Missing \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "\"%s\" mancante. Dovresti racchiudere gli argomenti tra virgolette per " "evitare che le parentesi vengano interpretate come comandi nidificati." #: src/callbacks.py:390 msgid "" "\"|\" with nothing preceding. I obviously can't do a pipe with nothing " "before the |." msgstr "" "\"|\" con niente che la precede. Ovviamente non posso usare una pipe senza " "nulla prima di |." #: src/callbacks.py:398 msgid "" "Spurious \"%s\". You may want to quote your arguments with double quotes in " "order to prevent extra brackets from being evaluated as nested commands." msgstr "" "\"%s\" eccedente. Dovresti racchiudere gli argomenti tra virgolette per " "evitare che le parentesi vengano interpretate come comandi nidificati." #: src/callbacks.py:407 msgid "" "\"|\" with nothing following. I obviously can't do a pipe with nothing " "after the |." msgstr "" "\"|\" con niente che la segue. Ovviamente non posso usare una pipe senza " "nulla dopo di |." #: src/callbacks.py:623 msgid "%s is not a valid %s." msgstr "%s non è un %s valido." #: src/callbacks.py:625 msgid "That's not a valid %s." msgstr "Questo non è un %s valido." #: src/callbacks.py:794 msgid "(XX more messages)" msgstr "(XX altri messaggi)" #: src/callbacks.py:849 msgid "more message" msgstr "altro messaggio" #: src/callbacks.py:851 msgid "more messages" msgstr "altri messaggi" #: src/callbacks.py:986 msgid "You've attempted more nesting than is currently allowed on this bot." msgstr "Hai tentato di nidificare più di quanto sia attualmente consentito." #: src/callbacks.py:1171 msgid "" "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." msgstr "" "Il comando %q è disponibile nei plugin %L. Specifica in quale plugin si " "trova quello che desideri usare aggiungendo il suo nome prima di %q." #: src/callbacks.py:1356 msgid "" "Determines what commands are currently disabled. Such\n" " commands will not appear in command lists, etc. They will appear not " "even\n" " to exist." msgstr "" "Determina quali comandi sono attualmente disabilitati. Questi non " "appariranno\n" " nell'elenco dei comandi, ecc.; sarà come se non esistessero." #: src/callbacks.py:1595 msgid "Invalid arguments for %s." msgstr "Argomenti non validi per %s." #: src/callbacks.py:1627 msgid "The %q command has no help." msgstr "Il comando %q non ha un help." #: src/commands.py:284 msgid "integer" msgstr "intero" #: src/commands.py:295 msgid "non-integer value" msgstr "valore non intero" #: src/commands.py:306 msgid "floating point number" msgstr "numero in virgola mobile" #: src/commands.py:315 msgid "positive integer" msgstr "intero positivo" #: src/commands.py:319 msgid "non-negative integer" msgstr "intero non negativo" #: src/commands.py:322 msgid "index" msgstr "indice" #: src/commands.py:347 msgid "number of seconds" msgstr "numero di secondi" #: src/commands.py:354 msgid "boolean" msgstr "booleano" #: src/commands.py:368 src/commands.py:375 src/commands.py:384 #: src/commands.py:391 src/commands.py:400 msgid "do that" msgstr "fare ciò" #: src/commands.py:371 src/commands.py:378 src/commands.py:387 #: src/commands.py:394 src/commands.py:403 msgid "I'm not even in %s." msgstr "Non sono in %s." #: src/commands.py:373 msgid "I need to be voiced to %s." msgstr "Devo avere il voice per %s." #: src/commands.py:381 #, fuzzy msgid "I need to be at least voiced to %s." msgstr "Devo almeno essere halfop per %s." #: src/commands.py:389 msgid "I need to be halfopped to %s." msgstr "Devo essere halfop per %s." #: src/commands.py:397 msgid "I need to be at least halfopped to %s." msgstr "Devo almeno essere halfop per %s." #: src/commands.py:405 msgid "I need to be opped to %s." msgstr "Devo essere op per %s." #: src/commands.py:411 src/commands.py:569 msgid "channel" msgstr "canale" #: src/commands.py:424 msgid "nick or hostmask" msgstr "nick o hostmask" #: src/commands.py:478 msgid "regular expression" msgstr "espressione regolare" #: src/commands.py:489 src/commands.py:493 msgid "nick" msgstr "" #: src/commands.py:490 msgid "That nick is too long for this server." msgstr "Il nick è troppo lungo per questo server." #: src/commands.py:501 #, fuzzy msgid "I haven't seen %s." msgstr "Non sono in %s." #: src/commands.py:548 src/commands.py:567 msgid "I'm not in %s." msgstr "Non sono in %s." #: src/commands.py:552 msgid "This command may only be given in a channel that I am in." msgstr "Questo comando può essere dato solo in un canale in cui sono presente." #: src/commands.py:565 msgid "You must be in %s." msgstr "Devo essere in %s." #: src/commands.py:576 msgid "%s is not in %s." msgstr "%s non è in %s." #: src/commands.py:624 msgid "You must not give the empty string as an argument." msgstr "Non puoi darmi una stringa vuota come argomento." #: src/commands.py:632 #, fuzzy msgid "You must not give a string containing spaces as an argument." msgstr "Non puoi darmi una stringa vuota come argomento." #: src/commands.py:642 msgid "This message must be sent in a channel." msgstr "Questo messaggio va inviato in canale." #: src/commands.py:674 src/commands.py:681 msgid "url" msgstr "" #: src/commands.py:687 msgid "IRI" msgstr "" #: src/commands.py:693 msgid "email" msgstr "" #: src/commands.py:703 src/commands.py:711 msgid "http url" msgstr "URL HTTP" #: src/commands.py:718 msgid "command name" msgstr "nome comando" #: src/commands.py:726 msgid "ip" msgstr "IP" #: src/commands.py:732 msgid "letter" msgstr "lettera" #: src/commands.py:764 msgid "plugin" msgstr "plugin" #: src/commands.py:772 msgid "irc color" msgstr "colore IRC" #: src/conf.py:130 msgid "" "Determines whether this plugin is loaded\n" " by default." msgstr "Determina se questo plugin sia caricato in modo predefinito." #: src/conf.py:134 msgid "" "Determines whether this plugin is\n" " publicly visible." msgstr "Determina se questo plugin sia visibile pubblicamente." #: src/conf.py:231 msgid "Determines the bot's default nick." msgstr "Determina il nick predefinito del bot." #: src/conf.py:234 msgid "" "Determines what alternative\n" " nicks will be used if the primary nick (supybot.nick) isn't available. " "A\n" " %s in this nick is replaced by the value of supybot.nick when used. If " "no\n" " alternates are given, or if all are used, the supybot.nick will be " "perturbed\n" " appropriately until an unused nick is found." msgstr "" "Determina quali nick alternativi utilizzare se il principale (supybot.nick)\n" " non è disponibile; un %s in questo nick è sostituito dal valore di " "supybot.nick.\n" " Se non è fornita alcuna alternativa, o sono tutti in uso, supybot.nick " "sarà\n" " modificato in modo appropriato finché ne trova uno non utilizzato." #: src/conf.py:241 msgid "" "Determines the bot's ident string, if the server\n" " doesn't provide one by default." msgstr "" "Determina l'ident (ciò che precede @ nella hostmask) nel caso in cui il " "server non ne fornisca una." #: src/conf.py:258 #, fuzzy msgid "" "Determines the real name which the bot sends to\n" " the server. A standard real name using the current version of the bot\n" " will be generated if this is left empty." msgstr "" "Determina il nome utente inviato al server dal bot. Se lasciato vuoto, ne " "verrà\n" " generato uno standard utilizzando la versione del bot." #: src/conf.py:267 msgid "Determines what networks the bot will connect to." msgstr "Determina su quali reti si connetterà il bot." #: src/conf.py:377 msgid "" "Determines what password will be used on %s. Yes, we know that\n" " technically passwords are server-specific and not network-specific,\n" " but this is the best we can do right now." msgstr "" "Determina quale password utilizzare su %s. Sì, sappiamo che tecnicamente le " "password\n" " sono specifiche per il server e non per la rete, ma per ora è il " "meglio che possiamo fare." #: src/conf.py:381 #, fuzzy msgid "" "Space-separated list of servers the bot will connect to for %s.\n" " Each will be tried in order, wrapping back to the first when the " "cycle\n" " is completed." msgstr "" "Determina a quali server si connetterà il bot per %s. Verranno provati in\n" " ordine tornando al primo quando il ciclo è stato completato." #: src/conf.py:385 #, fuzzy msgid "Space-separated list of channels the bot will join only on %s." msgstr "Determina in quali canali di %s entrerà il bot." #: src/conf.py:389 msgid "" "Determines whether the bot will attempt to connect with SSL\n" " sockets to %s." msgstr "Determina se il bot tenterà di connettersi a %s tramite un socket SSL." #: src/conf.py:392 msgid "" "Space-separated list\n" " of fingerprints of trusted certificates for this network.\n" " Supported hash algorithms are: %L.\n" " If non-empty, Certification Authority signatures will not be used " "to\n" " verify certificates." msgstr "" #: src/conf.py:398 msgid "" "A certificate that is trusted to verify\n" " certificates of this network (aka. Certificate Authority)." msgstr "" #: src/conf.py:401 msgid "Deprecated config value, keep it to False." msgstr "" #: src/conf.py:404 #, fuzzy msgid "" "Determines what certificate file (if any) the bot will use to\n" " connect with SSL sockets to %s." msgstr "Determina se il bot tenterà di connettersi a %s tramite un socket SSL." #: src/conf.py:407 msgid "" "Determines what key (if any) will be used to join the\n" " channel." msgstr "" "Determina quale password (eventuale) verrà usata per entrare in canale." #: src/conf.py:409 msgid "" "Determines\n" " what nick the bot will use on this network. If empty, defaults to\n" " supybot.nick." msgstr "" "Determina che nick userà il bot su questa rete. Se lasciato vuoto assegna il " "valore di supybot.nick." #: src/conf.py:412 #, fuzzy msgid "" "Determines\n" " the bot's ident string, if the server doesn't provide one by " "default.\n" " If empty, defaults to supybot.ident." msgstr "" "Determina l'ident (ciò che precede @ nella hostmask) nel caso in cui il " "server non ne fornisca una." #: src/conf.py:415 #, fuzzy msgid "" "Determines\n" " the real name which the bot sends to the server. If empty, defaults " "to\n" " supybot.user" msgstr "" "Determina che nick userà il bot su questa rete. Se lasciato vuoto assegna il " "valore di supybot.nick." #: src/conf.py:419 #, fuzzy msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. If empty, defaults to\n" " supybot.protocols.irc.umodes" msgstr "" "Determina che nick userà il bot su questa rete. Se lasciato vuoto assegna il " "valore di supybot.nick." #: src/conf.py:424 #, fuzzy msgid "" "Determines what SASL username will be used on %s. This should\n" " be the bot's account name." msgstr "" "Determina quale nome utente SASL utilizzare su %s; dovrebbe essere il nome " "usato per\n" " l'account del bot. A causa del funzionamento di SASL, non è " "possibile usare\n" " un nick raggruppato." #: src/conf.py:427 msgid "Determines what SASL password will be used on %s." msgstr "Determina quale password per SASL sarà usata su %s." #: src/conf.py:430 msgid "" "Determines what SASL ECDSA key (if any) will be used on %s.\n" " The public key must be registered with NickServ for SASL\n" " ECDSA-NIST256P-CHALLENGE to work." msgstr "" #: src/conf.py:435 #, fuzzy msgid "" "Determines what SASL mechanisms will be tried and in which order.\n" " " msgstr "" "Determina quale password (eventuale) verrà usata per entrare in canale." #: src/conf.py:438 #, fuzzy msgid "" "Determines whether the bot will abort the connection if the\n" " none of the enabled SASL mechanism succeeded." msgstr "Determina se il bot tenterà di connettersi a %s tramite un socket SSL." #: src/conf.py:441 msgid "" "If not empty, determines the hostname:port of the socks proxy that\n" " will be used to connect to this network." msgstr "" #: src/conf.py:461 #, fuzzy msgid "Determines how urls should be formatted." msgstr "Determina quali plugin saranno caricati." #: src/conf.py:469 msgid "" "Determines how timestamps\n" " printed for human reading should be formatted. Refer to the Python\n" " documentation for the time module to see valid formatting characters " "for\n" " time formats." msgstr "" "Determina come formattare i timestamp affinché possano essere comprensibli.\n" " Per sapere quali sono i formati validi fai riferimento alla " "documentazione\n" " di Python per il modulo time." #: src/conf.py:484 msgid "" "Determines whether elapsed times will be given\n" " as \"1 day, 2 hours, 3 minutes, and 15 seconds\" or as \"1d 2h 3m 15s\"." msgstr "" "Determina se il tempo trascorso sarà espresso nella forma \"1 giorno, 2 " "ore,\n" " 3 minuti e 15 secondi\" o \"1d 2h 3m 15s\"." #: src/conf.py:495 msgid "" "Maximum number of items in a list\n" " before the end is replaced with 'and others'. Set to 0 to always\n" " show the entire list." msgstr "" #: src/conf.py:512 msgid "other" msgstr "" #: src/conf.py:517 msgid "" "Determines the absolute maximum length of\n" " the bot's reply -- no reply will be passed through the bot with a " "length\n" " greater than this." msgstr "" "Determina la lunghezza massima delle risposte; il bot non invierà alcuna " "risposta che supera questo valore." #: src/conf.py:522 msgid "" "Determines whether the bot will break up long\n" " messages into chunks and allow users to use the 'more' command to get " "the\n" " remaining chunks." msgstr "" "Determina se il bot separerà i messaggi lunghi in più parti permettendo\n" " agli utenti di utilizzare il comando \"more\" per ottenere il resto." #: src/conf.py:527 msgid "" "Determines what the maximum number of\n" " chunks (for use with the 'more' command) will be." msgstr "" "Determina il numero massimo di parti (da utilizzare con il comando \"more\")." #: src/conf.py:531 msgid "" "Determines how long individual chunks\n" " will be. If set to 0, uses our super-tweaked,\n" " get-the-most-out-of-an-individual-message default." msgstr "" "Determina la lunghezza di ogni singolo pezzo. Se impostata a 0 permette\n" " di usare il nostro super algoritmo per ottenere il più possibile da ogni " "messaggio." #: src/conf.py:536 msgid "" "Determines how many mores will be sent\n" " instantly (i.e., without the use of the more command, immediately when\n" " they are formed). Defaults to 1, which means that a more command will " "be\n" " required for all but the first chunk." msgstr "" "Determina quanti pezzi saranno inviati immediatamente (ovvero senza " "utilizzare il comando\n" " \"more\"). Impostato a 1 in modo predefinito, per cui viene ne viene " "visualizzato uno solo." #: src/conf.py:542 #, fuzzy msgid "" "Determines whether the bot will send\n" " multi-message replies in a single message. This defaults to True \n" " in order to prevent the bot from flooding. If this is set to False\n" " the bot will send multi-message replies on multiple lines." msgstr "" "Determina se il bot invierà le risposte multiple in uno o più messaggi. Per\n" " ragioni di sicurezza (il bot è meno suscettibile a flood) mostrerà " "tutto\n" " in un singolo messaggio e utilizzando \"more\" se necessario." #: src/conf.py:548 msgid "" "Determines whether the bot will reply with an\n" " error message when it is addressed but not given a valid command. If " "this\n" " value is False, the bot will remain silent, as long as no other plugins\n" " override the normal behavior." msgstr "" "Determines se il bot risponderà con un messaggio di errore quando è " "chiamato\n" " ma non si fornisce un comando valido. Se impostato a False rimarrà " "silenzioso,\n" " a meno che altri plugin modifichino questo comportamento." #: src/conf.py:555 msgid "" "Determines whether error messages that result\n" " from bugs in the bot will show a detailed error message (the uncaught\n" " exception) or a generic error message." msgstr "" "Determina se i messaggi di errore causati da bug del bot saranno mostrati\n" " in modo dettagliato (eccezione non gestita) o con un messaggio di errore " "generico." #: src/conf.py:559 msgid "" "Determines whether the bot will send error\n" " messages to users in private. You might want to do this in order to " "keep\n" " channel traffic to minimum. This can be used in combination with\n" " supybot.reply.error.withNotice." msgstr "" "Determina se il bot invierà i messaggi di errore in privato; utile per " "ridurre il\n" " traffico del canale. Può essere usato in combinazione con supybot.reply." "error.withNotice." #: src/conf.py:564 #, fuzzy msgid "" "Determines whether the bot will send error\n" " messages to users via NOTICE instead of PRIVMSG. You might want to do " "this\n" " so users can ignore NOTICEs from the bot and not have to see error\n" " messages; or you might want to use it in combination with\n" " supybot.reply.error.inPrivate so private errors don't open a query " "window\n" " in most IRC clients." msgstr "" "Determina se il bot invierà i messaggi di errore tramite NOTICE anziché " "PRIVMSG.\n" " Si può volerlo impostare in modo che gli utenti possano ignorare i " "NOTICE\n" " provenienti dal bot oppure usarlo in combinazione con supybot.reply." "errorInPrivate\n" " affinché gli errori non causino l'apertura di una finestra privata nel " "client IRC." #: src/conf.py:571 #, fuzzy msgid "" "Determines whether the bot will *not* provide\n" " details in the error\n" " message to users who attempt to call a command for which they do not " "have\n" " the necessary capability. You may wish to make this True if you don't " "want\n" " users to understand the underlying security system preventing them from\n" " running certain commands." msgstr "" "Determina se il bot invierà un messaggio di errore agli utenti che cercano " "di usare\n" " un comando per cui non hanno le capacità necessarie. Si può volerlo " "impostare a\n" " True per evitare che vengano a conoscenza del sistema di sicurezza " "sottostante\n" " che impedisce loro di eseguire certi comandi." #: src/conf.py:579 msgid "" "Determines whether the bot will reply\n" " privately when replying in a channel, rather than replying to the " "whole\n" " channel." msgstr "Determina se il bot risponderà in privato piuttosto che in canale." #: src/conf.py:584 msgid "" "Determines whether the bot will reply with a\n" " notice when replying in a channel, rather than replying with a privmsg " "as\n" " normal." msgstr "Determina se il bot risponderà con un notice piuttosto che in privato." #: src/conf.py:590 #, fuzzy msgid "" "Determines whether the bot will reply with a\n" " notice when it is sending a private message, in order not to open a /" "query\n" " window in clients." msgstr "" "Determina se il bot risponderà con un notice quando gli si invia un " "messaggio privato\n" " per evitare di aprire una finestra nel client (una /query). Può essere " "modificato\n" " per singolo utente tramite la variabile di configurazione reply." "withNoticeWhenPrivate." #: src/conf.py:595 #, fuzzy msgid "" "Determines whether the bot will always prefix\n" " the user's nick to its reply to that user's command." msgstr "" "Determina se il bot userà sempre il nick dell'utente come prefisso della " "risposta al comando richiesto." #: src/conf.py:599 msgid "" "Determines whether the bot should attempt to\n" " reply to all messages even if they don't address it (either via its " "nick\n" " or a prefix character). If you set this to True, you almost certainly " "want\n" " to set supybot.reply.whenNotCommand to False." msgstr "" "Determina se il bot tenterà di rispondere a tutti i messaggi anche se non " "richiamato\n" " (tramite il nick o il prefisso). Se impostato a True, si vorrà " "certamente\n" " impostare supybot.reply.whenNotCommand a False." #: src/conf.py:605 msgid "" "Determines whether the bot will allow you to\n" " send channel-related commands outside of that channel. Sometimes " "people\n" " find it confusing if a channel-related command (like Filter.outfilter)\n" " changes the behavior of the channel but was sent outside the channel\n" " itself." msgstr "" "Determina se il bot permetterà di inviare comandi relativi al canale al di\n" " fuori dello stesso. Talvolta gli utenti trovano disorientante che un\n" " comando (come Filter.outfilter) modifichi il comportamento del canale\n" " ma è stato inviato fuori da questo." #: src/conf.py:612 msgid "" "Determines whether the bot will unidentify\n" " someone when that person changes their nick. Setting this to True\n" " will cause the bot to track such changes. It defaults to False for a\n" " little greater security." msgstr "" "Determina se il bot de-identificherà un utente quando questo cambia nick.\n" " Impostandolo a True, il bot terrà traccia di questi cambiamenti; è\n" " su False in modo predefinito per migliorare leggermente la sicurezza." #: src/conf.py:618 msgid "" "Determines whether the bot will always join a\n" " channel when it's invited. If this value is False, the bot will only " "join\n" " a channel if the user inviting it has the 'admin' capability (or if " "it's\n" " explicitly told to join the channel using the Admin.join command)." msgstr "" "Determina se il bot entrerà sempre in un canale in cui è stato invitato. Se\n" " impostato a False entrerà solo se l'utente che lo invita ha la capacità " "\"admin\"\n" " (o se gli è stato esplicitamente detto di farlo tramite il comando Admin." "join)." #: src/conf.py:624 msgid "" "Supybot normally replies with the full help\n" " whenever a user misuses a command. If this value is set to True, the " "bot\n" " will only reply with the syntax of the command (the first line of the\n" " help) rather than the full help." msgstr "" "Quando un utente commette un errore inviando un comando, Supybot risponde\n" " solitamente con l'help completo. Se questo valore è impostato a True,\n" " il bot risponderà solo con la sintassi del comando (la prima riga " "dell'help)." #: src/conf.py:639 msgid "" "Determines what prefix characters the bot will\n" " reply to. A prefix character is a single character that the bot will " "use\n" " to determine what messages are addressed to it; when there are no " "prefix\n" " characters set, it just uses its nick. Each character in this string " "is\n" " interpreted individually; you can have multiple prefix chars\n" " simultaneously, and if any one of them is used as a prefix the bot will\n" " assume it is being addressed." msgstr "" "Definisce il prefisso con il quale risponderà il bot. Un prefisso è un " "carattere\n" " che serve a determinare quali messaggi siano indirizzati ad esso; quando " "non\n" " c'è un prefisso viene utilizzato il suo nick. Ogni carattere è " "interpretato\n" " individualmente ed è possibile impostarne più di uno." #: src/conf.py:648 msgid "" "Determines what strings the\n" " bot will reply to when they are at the beginning of the message. " "Whereas\n" " prefix.chars can only be one character (although there can be many of\n" " them), this variable is a space-separated list of strings, so you can\n" " set something like '@@ ??' and the bot will reply when a message is\n" " prefixed by either @@ or ??." msgstr "" "Determina a quali stringhe risponderà il bot quando queste sono all'inizio " "del\n" " messaggio. Mentre con \"prefix.chars\" può essere solo un carattere " "(sebbene\n" " possano essercene diversi) questa variabile è una lista separata da " "spazi,\n" " per cui è possibile ad esempio impostare \"@@ ??\" e far sì che il bot\n" " risponderà sia ad un messaggio prefissato da \"@@\" sia da \"??\"." #: src/conf.py:655 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick, rather than with a prefix character." msgstr "" "Determina se il bot risponderà quando gli utenti lo richiamano\n" " tramite il nick piuttosto che con un prefisso." #: src/conf.py:658 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick at the end of the message, rather than at\n" " the beginning." msgstr "" "Determina se il bot risponderà quando gli utenti lo richiamano\n" " tramite il nick alla fine del messaggio piuttosto che all'inizio." #: src/conf.py:662 msgid "" "Determines what extra nicks\n" " the bot will always respond to when addressed by, even if its current " "nick\n" " is something else." msgstr "" "Determina a quali ulteriori nick risponderà il bot, anche se quello attuale " "è diverso." #: src/conf.py:672 msgid "The operation succeeded." msgstr "L'operazione è riuscita." #: src/conf.py:673 msgid "" "Determines what message the bot replies with when a command succeeded.\n" " If this configuration variable is empty, no success message will be\n" " sent." msgstr "" "Determina con quale messagggio risponderà il bot quando un comando è stato\n" " effettuato con successo. Se questa variabile è vuota non verrà inviato " "alcun messaggio." #: src/conf.py:678 msgid "" "An error has occurred and has been logged.\n" " Please contact this bot's administrator for more information." msgstr "" "Si è verificato un errore ed è stato registrato. Per ulteriori informazioni\n" " contattare l'amministratore del bot." #: src/conf.py:679 msgid "" "\n" " Determines what error message the bot gives when it wants to be\n" " ambiguous." msgstr "" "\n" " Determina quale messagggio di errore riporterà il bot quando non vuole " "essere preciso." #: src/conf.py:684 #, fuzzy msgid "" "An error has occurred and has been logged.\n" " Check the logs for more information." msgstr "" "Si è verificato un errore ed è stato registrato. Per ulteriori informazioni " "controllare i log." #: src/conf.py:685 msgid "" "Determines what error\n" " message the bot gives to the owner when it wants to be ambiguous." msgstr "" "Determina quale messagggio di errore riporterà il bot all'owner quando non " "vuole essere preciso." #: src/conf.py:689 msgid "" "Your hostmask doesn't match or your password\n" " is wrong." msgstr "La tua hostmask non corrisponde o la password è errata." #: src/conf.py:690 msgid "" "Determines what message the bot replies with when\n" " someone tries to use a command that requires being identified or having " "a\n" " password and neither credential is correct." msgstr "" "Determina con quale messagggio risponderà il bot quando si prova a " "utilizzare un comando\n" " per cui bisogna essere identificati, o ha una password, e le " "credenziali non sono corrette." #: src/conf.py:696 msgid "" "I can't find %s in my user\n" " database. If you didn't give a user name, then I might not know what " "your\n" " user is, and you'll need to identify before this command might work." msgstr "" "Non trovo %s nel database degli utenti. Se non fornisci un nome utente non " "posso\n" " sapere quale sia e dovrai identificarti prima di utilizzare questo " "comando." #: src/conf.py:699 msgid "" "Determines what error message the bot replies with when someone tries\n" " to accessing some information on a user the bot doesn't know about." msgstr "" "Determina con quale messagggio di errore risponderà il bot quando qualcuno\n" " prova ad accedere a informazioni riguardanti un utente di cui il bot non " "sa nulla." #: src/conf.py:703 msgid "" "You must be registered to use this command.\n" " If you are already registered, you must either identify (using the " "identify\n" " command) or add a hostmask matching your current hostmask (using the\n" " \"hostmask add\" command)." msgstr "" "Per utilizzare questo comando devi essere registrato. Se lo sei già devi " "identificarti\n" " (tramite il comando \"identify\") o aggiungere una hostmask che " "corrisponda a quella\n" " attuale (tramite il comando \"hostmask add\")." #: src/conf.py:706 msgid "" "Determines what error message the bot\n" " replies with when someone tries to do something that requires them to " "be\n" " registered but they're not currently recognized." msgstr "" "Determina con quale messagggio risponderà il bot quando si prova a fare " "qualcosa\n" " per cui bisogna essere registrati ma non si è attualmente riconosciuti." #: src/conf.py:711 msgid "" "You don't have the %s capability. If you\n" " think that you should have this capability, be sure that you are " "identified\n" " before trying again. The 'whoami' command can tell you if you're\n" " identified." msgstr "" "Non hai la capacità %s; se pensi di averla assicurati di essere identificato " "prima\n" " di riprovare. Il comando \"whoami\" ti dirà se lo sei." #: src/conf.py:714 msgid "" "Determines what error message is given when the bot\n" " is telling someone they aren't cool enough to use the command they tried " "to\n" " use." msgstr "" "Determina quale messaggio di errore fornire quando il bot dice a qualcuno\n" " di non avere accesso ad un certo comando." #: src/conf.py:719 msgid "" "You're missing some capability you need.\n" " This could be because you actually possess the anti-capability for the\n" " capability that's required of you, or because the channel provides that\n" " anti-capability by default, or because the global capabilities include\n" " that anti-capability. Or, it could be because the channel or\n" " supybot.capabilities.default is set to False, meaning that no commands " "are\n" " allowed unless explicitly in your capabilities. Either way, you can't " "do\n" " what you want to do." msgstr "" "Ti manca qualche capacità necessaria. Questo potrebbe essere dovuto al fatto " "che\n" " possiedi una anti-capacità per quella richiesta o perché il canale " "fornisce\n" " quella anti-capacità in modo predefinito o, ancora, perché le capacità " "globali\n" " includono tale anti-capacità. Oppure supybot.capabilities.default è " "impostata a\n" " False, per cui non è permesso l'uso di alcun comando a meno che " "espressamente\n" " specificato nelle tue capacità (da un amministratore). In ogni caso non " "ti è\n" " permesso fare ciò che intendevi." #: src/conf.py:727 msgid "" "Determines what generic error message is given when the bot is telling\n" " someone that they aren't cool enough to use the command they tried to " "use,\n" " and the author of the code calling errorNoCapability didn't provide an\n" " explicit capability for whatever reason." msgstr "" "Determina quale messaggio di errore generico fornire quando il bot dice a " "qualcuno\n" " di non avere accesso ad un certo comando e l'autore del codice che " "richiama\n" " errorNoCapability non ha fornito un'informazione esplicita sul motivo." #: src/conf.py:733 msgid "" "That operation cannot be done in a\n" " channel." msgstr "L'operazione non può essere eseguita in un canale." #: src/conf.py:734 msgid "" "Determines what error messages the bot sends to people\n" " who try to do things in a channel that really should be done in\n" " private." msgstr "" "Determina quale messaggio di errore invierà il bot a chi tenta di fare " "qualcosa\n" " in canale e che dovrebbe invece fare in privato." #: src/conf.py:739 msgid "" "This may be a bug. If you think it is,\n" " please file a bug report at\n" " ." msgstr "" "Questo sembra essere un bug. Se pensi lo sia, invia una segnalazione " "all'indirizzo\n" " ." #: src/conf.py:742 msgid "" "Determines what message the bot sends when it thinks you've\n" " encountered a bug that the developers don't know about." msgstr "" "Determina quale messaggio invierà il bot quando pensa tu abbia trovato\n" " un bug di cui gli sviluppatori non sono a conoscenza." #: src/conf.py:749 msgid "$Type #$id: $text (added by $username at $at)" msgstr "" #: src/conf.py:750 msgid "" "Format used by generic database plugins (Lart, Dunno, Prase, Success,\n" " Quote, ...) to show an entry. You can use the following variables:\n" " $type/$types/$Type/$Types (plugin name and variants), $id, $text,\n" " $at (creation time), $userid/$username/$nick (author)." msgstr "" #: src/conf.py:760 msgid "" "A floating point number of seconds to throttle\n" " snarfed URLs, in order to prevent loops between two bots snarfing the " "same\n" " URLs and having the snarfed URL in the output of the snarf message." msgstr "" "Un numero in virgola mobile corrispondente ai secondi di attesa prima di\n" " intercettare lo stesso URL, in modo da evitare che due bot entrino in\n" " un loop infinito intercettandosi a vicenda." #: src/conf.py:765 msgid "" "Determines the number of seconds\n" " between running the upkeep function that flushes (commits) open " "databases,\n" " collects garbage, and records some useful statistics at the debugging\n" " level." msgstr "" "Determina il numero di secondi tra ogni operazione di mantenimento (flush) " "che\n" " aggiorna i database, svuota la cache e registra le statistiche utili per " "il debug." #: src/conf.py:771 msgid "" "Determines whether the bot will periodically\n" " flush data and configuration files to disk. Generally, the only time\n" " you'll want to set this to False is when you want to modify those\n" " configuration files by hand and don't want the bot to flush its current\n" " version over your modifications. Do note that if you change this to " "False\n" " inside the bot, your changes won't be flushed. To make this change\n" " permanent, you must edit the registry yourself." msgstr "" "Determina se il bot salverà periodicamente su disco i dati e le " "configurazioni.\n" " In genere l'unica occasione per impostarlo a False è quando si vuole " "modificare\n" " i file manualmente ed evitare che il bot salvi invece i dati che ha in " "memoria;\n" " se imposti il valore a False tramite il bot i cambiamenti non verranno " "salvati.\n" " Se vuoi che questa modifica sia permanente devi modificare il registro a " "mano." #: src/conf.py:797 msgid "" "Determines what characters are valid for quoting\n" " arguments to commands in order to prevent them from being tokenized.\n" " " msgstr "" "Determina quali caratteri siano validi per citare gli argomenti dei comandi\n" ", ed evitare che vengano tokenizzati.\n" " " #: src/conf.py:804 msgid "" "Determines whether the bot will allow nested\n" " commands, which rule. You definitely should keep this on." msgstr "" "Determina se il bot permetterà comandi nidificati. È un'ottima\n" " funzionalità, devi assolutamente tenerla attiva." #: src/conf.py:807 msgid "" "Determines what the maximum number of\n" " nested commands will be; users will receive an error if they attempt\n" " commands more nested than this." msgstr "" "Determina il numero massimo di comandi nidificati. Se gli utenti tentano\n" " di utilizzarne di più, riceveranno un errore." #: src/conf.py:816 #, fuzzy msgid "" "Supybot allows you to specify what brackets\n" " are used for your nested commands. Valid sets of brackets include\n" " [], <>, {}, and (). [] has strong historical motivation, but <> or\n" " () might be slightly superior because they cannot occur in a nick.\n" " If this string is empty, nested commands will not be allowed in this\n" " channel." msgstr "" "Supybot permette di specificare quale tipo di parentesi utilizzare per i " "comandi\n" " nidificati; sono disponibili: [], <>, {} e (). [] hanno forti " "motivazioni storiche\n" " in quanto non necessitano l'uso di shift. <> o () sono leggermente " "migliori perché\n" " non utilizzabili in un nick. Se questa stringa è vuota i comandi " "nidificati non\n" " saranno permessi." #: src/conf.py:823 msgid "" "Supybot allows nested commands. Enabling this\n" " option will allow nested commands with a syntax similar to UNIX pipes, " "for\n" " example: 'bot: foo | bar'." msgstr "" "Supybot permette i comandi nidificati. Abilitare questa opzione ne " "permetterà l'uso\n" " con una sintassi simile alle pipe UNIX, ad esempio: 'bot: foo | bar'." #: src/conf.py:828 msgid "" "Determines what commands have default\n" " plugins set, and which plugins are set to be the default for each of " "those\n" " commands." msgstr "" "Determina, quando un comando è presente in più di un plugin, quale sarà " "utilizzato\n" " in modo predefinito per ciascuno di questi comandi." #: src/conf.py:834 msgid "" "Determines what plugins automatically get precedence over all\n" " other plugins when selecting a default plugin for a command. By\n" " default, this includes the standard loaded plugins. You probably\n" " shouldn't change this if you don't know what you're doing; if you " "do\n" " know what you're doing, then also know that this set is\n" " case-sensitive." msgstr "" "Determina quali plugin abbiano la precedenza su tutti gli altri quando se " "ne\n" " seleziona uno predefinito per un certo comando; di default questo " "include\n" " i plugin standard caricati. A meno che tu non sappia cosa fare, è " "probabile\n" " tu non voglia modificare questo comportamento; in caso contrario " "sappi anche\n" " che i nomi sono case sensitive." #: src/conf.py:844 msgid "" "Allows this bot's owner user to use commands\n" " that grants them shell access. This config variable exists in case you " "want\n" " to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)\n" " from gaining shell access to the bot's server by impersonating the " "owner.\n" " Setting this to False also disables plugins and commands that can be\n" " used to indirectly gain shell access." msgstr "" #: src/conf.py:859 msgid "" "Determines the interval used for\n" " the history storage." msgstr "" #: src/conf.py:862 msgid "" "Determines whether the bot will defend itself\n" " against command-flooding." msgstr "" "Determina se il bot si difenderà dall'uso di troppi comandi alla volta " "(flood)." #: src/conf.py:865 msgid "" "Determines how many commands users are\n" " allowed per minute. If a user sends more than this many commands in " "any\n" " 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.punishment seconds." msgstr "" "Determina quanti comandi al minuto permettere agli utenti. Se qualcuno " "supera\n" " questo valore verrà ignorato per il numero di secondi definito nella " "variabile\n" " supybot.abuse.flood.command.punishment." #: src/conf.py:870 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with commands." msgstr "" "Determina per quanti secondi il bot ignorerà un utente che abusa dei comandi " "(flood)." #: src/conf.py:873 #, fuzzy msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for command flooding." msgstr "" "Determina se il bot avvertirà l'utente che è stato ignorato per abuso di " "comandi non validi (flood)." #: src/conf.py:877 msgid "" "Determines whether the bot will defend itself\n" " against invalid command-flooding." msgstr "" "Determina se il bot si difenderà dall'uso di troppi comandi non validi alla " "volta (flood)." #: src/conf.py:880 msgid "" "Determines how many invalid commands users\n" " are allowed per minute. If a user sends more than this many invalid\n" " commands in any 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.invalid.punishment seconds. Typically, " "this\n" " value is lower than supybot.abuse.flood.command.maximum, since it's far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" "Determina quanti comandi non validi al minuto permettere agli utenti. Se " "qualcuno supera\n" " questo valore verrà ignorato per il numero di secondi definito nella " "variabile\n" " supybot.abuse.flood.command.invalid.punishment. Solitamente questo " "valore è inferiore\n" " a quello di supybot.abuse.flood.command.maximum, in quanto è meno " "probabile che un utente\n" " abusi di comandi non validi piuttosto che di quelli validi." #: src/conf.py:888 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with invalid commands. Typically, this\n" " value is higher than supybot.abuse.flood.command.punishment, since it's " "far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" "Determina per quanti secondi il bot ignorerà un utente che abusa di comandi " "non validi (flood).\n" " Solitamente questo valore è superiore a quello di supybot.abuse.flood." "command.punishment,\n" " in quanto è meno probabile che un utente abusi di comandi non validi " "piuttosto che di quelli validi." #: src/conf.py:894 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for invalid command flooding." msgstr "" "Determina se il bot avvertirà l'utente che è stato ignorato per abuso di " "comandi non validi (flood)." #: src/conf.py:903 msgid "" "Determines the default length of time a\n" " driver should block waiting for input." msgstr "" "Determina la quantità di tempo predefinita durante il quale un driver\n" " di rete debba bloccarsi in attesa dei dati in entrata." #: src/conf.py:911 msgid "" "Determines what driver module the \n" " bot will use. Current, the only (and default) driver is Socket." msgstr "" #: src/conf.py:915 msgid "" "Determines the maximum time the bot will\n" " wait before attempting to reconnect to an IRC server. The bot may, of\n" " course, reconnect earlier if possible." msgstr "" "Determina il tempo massimo che il bot attenderà prima di riconnettersi al " "server\n" " IRC. Se ne avrà la possibilità potrà comunque connettersi prima." #: src/conf.py:967 msgid "" "Determines what directory configuration data is\n" " put into." msgstr "Determina in quale directory collocare i dati di configurazione." #: src/conf.py:970 msgid "Determines what directory data is put into." msgstr "Determina in quale directory collocare i dati." #: src/conf.py:972 msgid "" "Determines what directory backup data is put\n" " into. Set it to /dev/null to disable backup (it is a special value,\n" " so it also works on Windows and systems without /dev/null)." msgstr "" "Determina in quale directory collocare i dati di backup. Impostare il " "valore\n" " a /dev/null per disabilitarlo (è un valore speciale che funziona anche " "su\n" " Windows e sistemi privi di questo device)." #: src/conf.py:979 msgid "" "Determines what directory temporary files\n" " are put into." msgstr "Determina in quale directory collocare i file temporanei." #: src/conf.py:982 #, fuzzy msgid "" "Determines what directory files of the\n" " web server (templates, custom images, ...) are put into." msgstr "Determina in quale directory collocare i file temporanei." #: src/conf.py:995 msgid "" "Determines what directories\n" " the bot will look for plugins in. Accepts a comma-separated list of\n" " strings.\n" " This means that to add another directory, you can nest the former value " "and\n" " add a new one. E.g. you can say: bot: 'config supybot.directories." "plugins\n" " [config supybot.directories.plugins], newPluginDirectory'." msgstr "" "Determina in quali directory il bot cercherà i plugin. Accetta un elenco di " "valori\n" " separati da virgola; ciò significa che se si vuole aggiungere un'altra " "directory\n" " è possibile nidificare il valore precedente e inserirne uno nuovo. " "Esempio:\n" " \"config supybot.directories.plugins [config supybot.directories." "plugins], nuovaDirectory\"." #: src/conf.py:1003 msgid "" "List of all plugins that were\n" " ever loaded. Currently has no effect whatsoever. You probably want to " "use\n" " the 'load' or 'unload' commands, or edit supybot.plugins.\n" " instead of this." msgstr "" #: src/conf.py:1008 msgid "" "Determines whether the bot will always load\n" " important plugins (Admin, Channel, Config, Misc, Owner, and User)\n" " regardless of what their configured state is. Generally, if these " "plugins\n" " are configured not to load, you didn't do it on purpose, and you still\n" " want them to load. Users who don't want to load these plugins are " "smart\n" " enough to change the value of this variable appropriately :)" msgstr "" "Determina se il bot caricherà sempre i plugin importanti (Admin, Channel, " "Config,\n" " Misc, Owner e User) a prescindere da come siano configurati. In genere " "se questi\n" " sono impostati per non essere caricati non è stato fatto di proposito e " "vorrai\n" " comunque caricarli. Chi non vuole che questo avvenga sarà " "sufficientemente abile\n" " nel modificare il valore di questa variabile in modo appropriato :)" #: src/conf.py:1036 msgid "" "Determines what databases are available for use. If this\n" " value is not configured (that is, if its value is empty) then sane " "defaults\n" " will be provided." msgstr "" "Determina quali database rendere disponibili. Se non configurato (ovvero il\n" " valore è vuoto) verrà utilizzato uno predefinito." #: src/conf.py:1042 msgid "" "Determines what filename will be used\n" " for the users database. This file will go into the directory specified " "by\n" " the supybot.directories.conf variable." msgstr "" "Determina il nome del file utilizzato per il database degli utenti che " "finirà\n" " nella directory specificata dalla variabile supybot.directories.conf." #: src/conf.py:1046 msgid "" "Determines how long it takes identification to\n" " time out. If the value is less than or equal to zero, identification " "never\n" " times out." msgstr "" "Determina per quanto tempo rimarrà identificato un utente. Se il valore è\n" " inferiore o uguale a zero l'identificazione non scadrà mai." #: src/conf.py:1050 msgid "" "Determines whether the bot will allow users to\n" " unregister their users. This can wreak havoc with already-existing\n" " databases, so by default we don't allow it. Enable this at your own " "risk.\n" " (Do also note that this does not prevent the owner of the bot from " "using\n" " the unregister command.)\n" " " msgstr "" "Determina se il bot permetterà agli utenti di de-registrarsi. Ciò può " "causare\n" " danni ai database esistenti per cui non è permesso in modo predefinito;\n" " abilitalo a tuo rischio. Nota che questo non impedisce al proprietario " "del\n" " bot (owner) di utilizzare il comando \"unregister\".\n" " " #: src/conf.py:1059 msgid "" "Determines what filename will be used\n" " for the ignores database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Determina il nome del file utilizzato per il database degli ignore che " "finirà\n" " nella directory specificata dalla variabile supybot.directories.conf." #: src/conf.py:1065 msgid "" "Determines what filename will be used\n" " for the channels database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Determina il nome del file utilizzato per il database dei canali che finirà\n" " nella directory specificata dalla variabile supybot.directories.conf." #: src/conf.py:1071 #, fuzzy msgid "" "Determines what filename will be used\n" " for the networks database. This file will go into the directory " "specified\n" " by the supybot.directories.conf variable." msgstr "" "Determina il nome del file utilizzato per il database degli ignore che " "finirà\n" " nella directory specificata dalla variabile supybot.directories.conf." #: src/conf.py:1103 #, fuzzy msgid "" "Determines whether the bot will require user\n" " registration to use 'add' commands in database-based Supybot\n" " plugins." msgstr "" "Determina se il bot si difenderà dall'uso di troppi comandi non validi alla " "volta (flood)." #: src/conf.py:1107 msgid "" "Determines whether database-based plugins that\n" " can be channel-specific will be so. This can be overridden by " "individual\n" " channels. Do note that the bot needs to be restarted immediately after\n" " changing this variable or your db plugins may not work for your " "channel;\n" " also note that you may wish to set\n" " supybot.databases.plugins.channelSpecific.link appropriately if you " "wish\n" " to share a certain channel's databases globally." msgstr "" "Determina se i plugin che utilizzano un database possano essere specifici " "per\n" " un certo canale; ciò è sostituibile per ogni canale. Nota che il bot va\n" " riavviato subito dopo la modifica di questa variabile altrimenti tali " "plugin\n" " potrebbero non funzionare; se desideri condividere il database di un " "certo\n" " canale a livello globale è inoltre possibile impostare la variabile\n" " supybot.databases.plugins.channelSpecific.link in modo appropriato." #: src/conf.py:1115 msgid "" "Determines what channel global\n" " (non-channel-specific) databases will be considered a part of. This is\n" " helpful if you've been running channel-specific for awhile and want to " "turn\n" " the databases for your primary channel into global databases. If\n" " supybot.databases.plugins.channelSpecific.link.allow prevents linking, " "the\n" " current channel will be used. Do note that the bot needs to be " "restarted\n" " immediately after changing this variable or your db plugins may not " "work\n" " for your channel." msgstr "" "Determina quali database specifici per un certo canale saranno considerati " "parte\n" " di uno globale. Utile se ne sono stati utilizzati di specifici per un " "certo\n" " periodo e li si vuole convertire. Se la variabile supybot.databases." "plugins.channelSpecific.link.allow\n" " impedisce il collegamento verrà utilizzato il canale attuale. Nota che " "il bot va\n" " riavviato subito dopo la modifica di questa variabile altrimenti tali " "plugin\n" " potrebbero non funzionare." #: src/conf.py:1124 msgid "" "Determines whether another channel's global\n" " (non-channel-specific) databases will be allowed to link to this " "channel's\n" " databases. Do note that the bot needs to be restarted immediately " "after\n" " changing this variable or your db plugins may not work for your " "channel.\n" " " msgstr "" "Determina se i database globali di un altro canale possano essere collegati " "a\n" " quelli di questo canale. Nota che il bot va riavviato subito dopo la " "modifica\n" " di questa variabile altrimenti tali plugin potrebbero non funzionare.\n" " " #: src/conf.py:1142 msgid "" "Determines\n" " whether CDB databases will be allowed as a database implementation." msgstr "" "Determina se i database CDB sono permessi come implementazione di un " "database." #: src/conf.py:1145 msgid "" "Determines how often CDB databases will have\n" " their modifications flushed to disk. When the number of modified " "records\n" " is greater than this fraction of the total number of records, the " "database\n" " will be entirely flushed to disk." msgstr "" "Determina la frequenza di salvataggio su disco delle modifiche dei databases " "CDB.\n" " Quando il numero di dati modificati è più grande di quelli non " "modificati il\n" " database verrà interamente scritto sul disco." #: src/conf.py:1237 msgid "" "Determines what will be used as the\n" " default banmask style." msgstr "Determina il tipo di banmask utilizzata in modo predefinito." #: src/conf.py:1241 #, fuzzy msgid "" "Determines whether the bot will strictly\n" " follow the RFC; currently this only affects what strings are\n" " considered to be nicks. If you're using a server or a network that\n" " requires you to message a nick such as services@this.network.server\n" " then you you should set this to False." msgstr "" "Determina se il bot seguirà rigorosamente la RFC; attualmente riguarda solo " "quali\n" " stringhe siano considerate dei nick. Se utilizzi un server o una rete " "che richiede\n" " di inviare un messaggio a un nick nella forma \"services@this.network." "server\", è\n" " necessario impostare questo valore a False." #: src/conf.py:1248 msgid "" "Determines whether the bot will enable\n" " draft/experimental extensions of the IRC protocol. Setting this to True\n" " may break your bot at any time without warning and/or break your\n" " configuration irreversibly. So keep it False unless you know what you " "are\n" " doing." msgstr "" #: src/conf.py:1255 #, fuzzy msgid "" "Determines what certificate file (if any) the bot\n" " will use connect with SSL sockets by default." msgstr "Determina se il bot tenterà di connettersi a %s tramite un socket SSL." #: src/conf.py:1259 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. Many people might choose +i; " "some\n" " networks allow +x, which indicates to the auth services on those " "networks\n" " that you should be given a fake host." msgstr "" "Determina quali modalità utente (mode) il bot chiederà al server quando si " "connette.\n" " Molti utenti vorranno +i mentre per altre reti è +x, indica ai servizi " "che si\n" " desidera avere l'hostmask camuffata (vhost) o parzialmente crittata." #: src/conf.py:1265 #, fuzzy msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv4." msgstr "" "Determina quale vhost associare al bot prima di connettersi al server IRC." #: src/conf.py:1269 #, fuzzy msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv6." msgstr "" "Determina quale vhost associare al bot prima di connettersi al server IRC." #: src/conf.py:1273 #, fuzzy msgid "" "Determines how many old messages the bot will\n" " keep around in its history. Changing this variable will not take " "effect\n" " on a network until it is reconnected." msgstr "" "Determina quanti messaggi vecchi terrà il bot nella cronologia. La modifica\n" " di questa variabile non avrà effetto finché non si riavvia il bot." #: src/conf.py:1278 msgid "" "A floating point number of seconds to throttle\n" " queued messages -- that is, messages will not be sent faster than once " "per\n" " throttleTime seconds." msgstr "" "Un numero in virgola mobile corrispondente ai secondi di attesa prima di\n" " inviare i messaggi in coda; vale a dire che verranno inviati una volta\n" " ogni numero di secondi specificati." #: src/conf.py:1283 msgid "" "Determines whether the bot will send PINGs to\n" " the server it's connected to in order to keep the connection alive and\n" " discover earlier when it breaks. Really, this option only exists for\n" " debugging purposes: you always should make it True unless you're " "testing\n" " some strange server issues." msgstr "" "Determina se il bot invierà PING al server sul quale è connesso per " "mantenere\n" " attiva la connessione e diagnosticare in tempo se c'è un'interruzione.\n" " In realtà questa opzione esiste solo per scopi di debug, a meno che non\n" " si incontrino problemi strani con il server, andrebbe sempre tenuta a " "True." #: src/conf.py:1290 msgid "" "Determines the number of seconds between sending\n" " pings to the server, if pings are being sent to the server." msgstr "" "Determina il numero di secondi tra ogni invio di PING al server, se questo " "avviene." #: src/conf.py:1295 msgid "" "Determines whether the bot will refuse\n" " duplicated messages to be queued for delivery to the server. This is a\n" " safety mechanism put in place to prevent plugins from sending the same\n" " message multiple times; most of the time it doesn't matter, unless " "you're\n" " doing certain kinds of plugin hacking." msgstr "" "Determina se il bot rifiuterà di accodare messaggi doppi da spedire al " "server.\n" " Questo è un meccanismo di sicurezza per evitare che i plugin inviino più " "volte\n" " lo stesso messaggio; il più delle volte non ha importanza, tranne che " "non si\n" " facciano delle modifiche ai plugin." #: src/conf.py:1303 msgid "" "Determines how many seconds must elapse between\n" " JOINs sent to the server." msgstr "" "Determina quanti secondi debbano trascorrere tra un invio di JOIN e l'altro." #: src/conf.py:1311 msgid "" "Determines how many bytes the bot will\n" " 'peek' at when looking through a URL for a doctype or title or " "something\n" " similar. It'll give up after it reads this many bytes, even if it " "hasn't\n" " found what it was looking for." msgstr "" "Determina il numero di byte entro i quali il bot \"sbircierà\" cercando il " "doctype,\n" " il title o altri tag HTML simili in un URL. Abbandonerà la ricerca dopo " "aver\n" " letto questa quantità di byte, anche se non trova quel che stava " "cercando." #: src/conf.py:1335 #, fuzzy msgid "" "Determines what HTTP proxy all HTTP requests should go\n" " through. The value should be of the form 'host:port'." msgstr "" "Determina tramite quale proxy debbano passare le richieste HTTP.\n" " Il valore deve essere nella forma \"host:porta\"." #: src/conf.py:1375 msgid "" "If set, the Accept-Language HTTP header will be set to this\n" " value for requests. Useful for overriding the auto-detected language " "based on\n" " the server's location." msgstr "" #: src/conf.py:1381 msgid "" "If set, the User-Agent HTTP header will be set to a randomly\n" " selected value from this comma-separated list of strings for requests." msgstr "" #: src/conf.py:1389 msgid "" "Determines whether server certificates\n" " will be verified, which checks whether the server certificate is signed\n" " by a known certificate authority, and aborts the connection if it is " "not.\n" " This is assumed to be True of serverFingerprints or " "authorityCertificate\n" " is set." msgstr "" #: src/conf.py:1416 msgid "" "If true, uses IPV6_V6ONLY to disable\n" " forwaring of IPv4 traffic to IPv6 sockets. On *nix, has the same\n" " effect as setting kernel variable net.ipv6.bindv6only to 1." msgstr "" #: src/conf.py:1420 #, fuzzy msgid "" "Space-separated list of IPv4 hosts the HTTP server\n" " will bind." msgstr "Determina a quale host si collegherà il server HTTP." #: src/conf.py:1423 #, fuzzy msgid "" "Space-separated list of IPv6 hosts the HTTP server will\n" " bind." msgstr "Determina a quale porta si collegherà il server HTTP." #: src/conf.py:1426 msgid "" "Determines what port the HTTP server will\n" " bind." msgstr "Determina a quale porta si collegherà il server HTTP." #: src/conf.py:1429 msgid "" "Determines whether the server will stay\n" " alive if no plugin is using it. This also means that the server will\n" " start even if it is not used." msgstr "" "Definisce se il server rimarrà in esecuzione se nessun plugin lo utilizza.\n" " Ciò significa che verrà avviato anche se non utilizzato." #: src/conf.py:1433 #, fuzzy msgid "" "Determines the path of the file served as\n" " favicon to browsers." msgstr "Determina a quale porta si collegherà il server HTTP." #: src/conf.py:1436 msgid "" "Determines the public URL of the server.\n" " By default it is http://:/, but you will want to change\n" " this if there is a reverse proxy (nginx, apache, ...) in front of\n" " the bot." msgstr "" #: src/conf.py:1446 #, fuzzy msgid "" "Determines whether the bot will ignore\n" " unidentified users by default. Of course, that'll make it\n" " particularly hard for those users to register or identify with the bot\n" " without adding their hostmasks, but that's your problem to solve." msgstr "" "Determina se il bot ignorerà gli utenti non registrati in modo predefinito.\n" " Naturalmente renderà difficoltoso registrarsi o identificarsi, ma questo " "è un tuo problema." #: src/conf.py:1453 msgid "" "A string that is the external IP of the bot. If this is the\n" " empty string, the bot will attempt to find out its IP dynamically " "(though\n" " sometimes that doesn't work, hence this variable). This variable is not " "used\n" " by Limnoria and its built-in plugins: see supybot.protocols.irc.vhost /\n" " supybot.protocols.irc.vhost6 to set the IRC bind host, and\n" " supybot.servers.http.hosts4 / supybot.servers.http.hosts6 to set the " "HTTP\n" " server bind host." msgstr "" #: src/conf.py:1472 msgid "" "Determines what the default timeout for socket\n" " objects will be. This means that *all* sockets will timeout when this " "many\n" " seconds has gone by (unless otherwise modified by the author of the " "code\n" " that uses the sockets)." msgstr "" "Determina il timeout predefinito per i socket. Ciò significa che *tutti* i " "socket\n" " scadranno dopo questo periodo (a meno che l'utore del codice ne abbia " "modificato il valore)." #: src/conf.py:1478 msgid "" "Determines what file the bot should write its PID\n" " (Process ID) to, so you can kill it more easily. If it's left unset (as " "is\n" " the default) then no PID file will be written. A restart is required " "for\n" " changes to this variable to take effect." msgstr "" "Determina in quale file il bot salverà il suo PID (Process ID) in modo che " "si possa\n" " terminare più facilmente. Se non impostata, il PID non verrà scritto in " "nessun\n" " file. Perché le modifiche a questa variabile abbiano effetto è " "necessario un riavvio." #: src/conf.py:1488 msgid "" "Determines whether the bot will automatically\n" " thread all commands." msgstr "Determina se il bot userà i thread per tutti i comandi." #: src/conf.py:1491 msgid "" "Determines whether the bot will automatically\n" " flush all flushers *very* often. Useful for debugging when you don't " "know\n" " what's breaking or when, but think that it might be logged." msgstr "" "Determina se il bot eseguirà tutte le operazioni di mantenimento (flush) " "*molto*\n" " spesso. Utile per fare il debug quando non si sa cosa non funziona o " "quando\n" " si ritiene necessario tenerlo sotto controllo." #: src/httpserver.py:62 msgid "Supybot Web server index" msgstr "Indice del server web di Supybot" #: src/httpserver.py:67 msgid "Here is a list of the plugins that have a Web interface:" msgstr "Ecco un elenco dei plugin che hanno un'interfaccia web:" #: src/httpserver.py:245 msgid "" "\n" " This is a default response of the Supybot HTTP server. If you see this\n" " message, it probably means you are developing a plugin, and you have\n" " neither overriden this message or defined an handler for this query." msgstr "" "\n" " Questa è la risposta predefinita del server HTTP di Supybot. Se stai " "leggendo\n" " questo messaggio significa probabilmente che stai sviluppando un plugin " "e non\n" " hai sovrascritto questo messaggio o definito un gestore per questa query." #: src/httpserver.py:291 msgid "" "\n" " I am a pretty clever IRC bot, but I suck at serving Web pages, " "particulary\n" " if I don't know what to serve.\n" " What I'm saying is you just triggered a 404 Not Found, and I am not\n" " trained to help you in such a case." msgstr "" "\n" " Sono un bot IRC piuttosto intelligente ma terribile quando è ora di " "servire pagine\n" " web, in particolare se non cosa servire. Quel che sto cercando di dirti " "è che hai\n" " appena causato un \"404 Not Found\" e non sono addestrato per aiutarti " "in questa circostanza." #: src/httpserver.py:310 msgid "Request not handled." msgstr "Richiesta non gestita." #: src/httpserver.py:317 msgid "No plugins available." msgstr "Nessun plugin disponibile." #: src/httpserver.py:335 src/httpserver.py:353 src/httpserver.py:391 msgid "Request not handled" msgstr "Richiesta non gestita" #: src/httpserver.py:378 msgid "No favicon set." msgstr "" #: src/ircutils.py:573 msgid "is an op on %L" msgstr "" #: src/ircutils.py:575 msgid "is a halfop on %L" msgstr "" #: src/ircutils.py:577 msgid "is voiced on %L" msgstr "" #: src/ircutils.py:580 msgid "is also on %L" msgstr "" #: src/ircutils.py:582 msgid "is on %L" msgstr "" #: src/ircutils.py:585 msgid "isn't on any publicly visible channels" msgstr "" #: src/ircutils.py:593 src/ircutils.py:594 src/ircutils.py:600 msgid "" msgstr "" #: src/ircutils.py:602 #, fuzzy msgid " %s is away: %s." msgstr "%s non è un %s valido." #: src/ircutils.py:607 msgid " identified" msgstr "" #: src/ircutils.py:613 msgid "%s (%s) has been%s on server %s since %s (idle for %s). %s %s.%s" msgstr "" #: src/ircutils.py:617 msgid "%s (%s) has been%s on server %s and disconnected on %s." msgstr "" #: src/questions.py:61 msgid "Sorry, that response was not an option." msgstr "Spiacente, questa risposta non è un'opzione." #: src/questions.py:110 msgid "Sorry, you must enter a value." msgstr "Spiacente, devi inserire un valore." #: src/questions.py:130 msgid "Enter password: " msgstr "Inserire la password: " #: src/questions.py:132 msgid "Re-enter password: " msgstr "Inserire nuovamente la password: " #: src/questions.py:145 msgid "Passwords don't match." msgstr "Le password non corrispondono." #: src/registry.py:219 #, fuzzy msgid "%r is not a valid entry in %r" msgstr "%s non è un %s valido." #: src/registry.py:568 msgid "Value must be either True or False (or On or Off), not %r." msgstr "" #: src/registry.py:585 msgid "Value must be an integer, not %r." msgstr "" #: src/registry.py:595 #, fuzzy msgid "Value must be a non-negative integer, not %r." msgstr "intero non negativo" #: src/registry.py:604 msgid "Value must be positive (non-zero) integer, not %r." msgstr "" #: src/registry.py:613 msgid "Value must be a floating-point number, not %r." msgstr "" #: src/registry.py:629 msgid "Value must be a floating-point number greater than zero, not %r." msgstr "" #: src/registry.py:640 msgid "Value must be a floating point number in the range [0, 1], not %r." msgstr "" #: src/registry.py:655 msgid "Value should be a valid Python string, not %r." msgstr "" #: src/registry.py:693 msgid "Valid values include %L." msgstr "" #: src/registry.py:695 msgid "Valid values include %L, not %%r." msgstr "" #: src/registry.py:769 msgid "Value must be a valid regular expression, not %r." msgstr "" #: src/utils/gen.py:113 msgid "year" msgstr "" #: src/utils/gen.py:116 msgid "week" msgstr "" #: src/utils/gen.py:119 msgid "day" msgstr "" #: src/utils/gen.py:122 msgid "hour" msgstr "" #: src/utils/gen.py:126 msgid "minute" msgstr "" #: src/utils/gen.py:129 msgid "second" msgstr "" #: src/utils/gen.py:138 msgid "%s ago" msgstr "" #: src/utils/str.py:349 msgid "and" msgstr "" #~ msgid "" #~ "Determines what driver module the bot\n" #~ " will use. Socket, a simple driver based on timeout sockets, is used " #~ "by\n" #~ " default because it's simple and stable. Twisted is very stable and " #~ "simple,\n" #~ " and if you've got Twisted installed, is probably your best bet." #~ msgstr "" #~ "Determina quale modulo per il driver di rete utilizzerà il bot. Socket, " #~ "un driver\n" #~ " semplice basato su timeout del socket e usato in modo predefinito per " #~ "la sua\n" #~ " stabilità. Twisted è molto stabile e semplice, se lo hai installato " #~ "è\n" #~ " probabilmente la scelta migliore." #~ msgid "" #~ "Determines the content of the robots.txt file,\n" #~ " served on the server to search engine." #~ msgstr "" #~ "Determina il contenuto del file robots.txt, fornito dal server al motore " #~ "di ricerca." #~ msgid "" #~ "A string that is the external IP of the bot. If this is the\n" #~ " empty string, the bot will attempt to find out its IP dynamically " #~ "(though\n" #~ " sometimes that doesn't work, hence this variable)." #~ msgstr "" #~ "Una stringa equivalente all'IP esterno del bot. Se lasciata vuota, il bot " #~ "tenterà\n" #~ " di risolvere il suo IP dinamicamente (sebbene a volte non funzioni, " #~ "ecco quindi\n" #~ " lo scopo di questa variabile)." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/locales/messages.pot0000644000175000017500000012263714535072470016110 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: 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" #: src/callbacks.py:217 msgid "Error: " msgstr "" #: src/callbacks.py:233 msgid "Error: I tried to send you an empty message." msgstr "" #: src/callbacks.py:361 msgid "Missing \"%s\". You may want to quote your arguments with double quotes in order to prevent extra brackets from being evaluated as nested commands." msgstr "" #: src/callbacks.py:390 msgid "\"|\" with nothing preceding. I obviously can't do a pipe with nothing before the |." msgstr "" #: src/callbacks.py:398 msgid "Spurious \"%s\". You may want to quote your arguments with double quotes in order to prevent extra brackets from being evaluated as nested commands." msgstr "" #: src/callbacks.py:407 msgid "\"|\" with nothing following. I obviously can't do a pipe with nothing after the |." msgstr "" #: src/callbacks.py:623 msgid "%s is not a valid %s." msgstr "" #: src/callbacks.py:625 msgid "That's not a valid %s." msgstr "" #: src/callbacks.py:794 msgid "(XX more messages)" msgstr "" #: src/callbacks.py:849 msgid "more message" msgstr "" #: src/callbacks.py:851 msgid "more messages" msgstr "" #: src/callbacks.py:986 msgid "You've attempted more nesting than is currently allowed on this bot." msgstr "" #: src/callbacks.py:1171 msgid "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." msgstr "" #: src/callbacks.py:1356 msgid "" "Determines what commands are currently disabled. Such\n" " commands will not appear in command lists, etc. They will appear not even\n" " to exist." msgstr "" #: src/callbacks.py:1595 msgid "Invalid arguments for %s." msgstr "" #: src/callbacks.py:1627 msgid "The %q command has no help." msgstr "" #: src/commands.py:284 msgid "integer" msgstr "" #: src/commands.py:295 msgid "non-integer value" msgstr "" #: src/commands.py:306 msgid "floating point number" msgstr "" #: src/commands.py:315 msgid "positive integer" msgstr "" #: src/commands.py:319 msgid "non-negative integer" msgstr "" #: src/commands.py:322 msgid "index" msgstr "" #: src/commands.py:347 msgid "number of seconds" msgstr "" #: src/commands.py:354 msgid "boolean" msgstr "" #: src/commands.py:368 src/commands.py:375 src/commands.py:384 #: src/commands.py:391 src/commands.py:400 msgid "do that" msgstr "" #: src/commands.py:371 src/commands.py:378 src/commands.py:387 #: src/commands.py:394 src/commands.py:403 msgid "I'm not even in %s." msgstr "" #: src/commands.py:373 msgid "I need to be voiced to %s." msgstr "" #: src/commands.py:381 msgid "I need to be at least voiced to %s." msgstr "" #: src/commands.py:389 msgid "I need to be halfopped to %s." msgstr "" #: src/commands.py:397 msgid "I need to be at least halfopped to %s." msgstr "" #: src/commands.py:405 msgid "I need to be opped to %s." msgstr "" #: src/commands.py:411 src/commands.py:569 msgid "channel" msgstr "" #: src/commands.py:424 msgid "nick or hostmask" msgstr "" #: src/commands.py:478 msgid "regular expression" msgstr "" #: src/commands.py:489 src/commands.py:493 msgid "nick" msgstr "" #: src/commands.py:490 msgid "That nick is too long for this server." msgstr "" #: src/commands.py:501 msgid "I haven't seen %s." msgstr "" #: src/commands.py:548 src/commands.py:567 msgid "I'm not in %s." msgstr "" #: src/commands.py:552 msgid "This command may only be given in a channel that I am in." msgstr "" #: src/commands.py:565 msgid "You must be in %s." msgstr "" #: src/commands.py:576 msgid "%s is not in %s." msgstr "" #: src/commands.py:624 msgid "You must not give the empty string as an argument." msgstr "" #: src/commands.py:632 msgid "You must not give a string containing spaces as an argument." msgstr "" #: src/commands.py:642 msgid "This message must be sent in a channel." msgstr "" #: src/commands.py:674 src/commands.py:681 msgid "url" msgstr "" #: src/commands.py:687 msgid "IRI" msgstr "" #: src/commands.py:693 msgid "email" msgstr "" #: src/commands.py:703 src/commands.py:711 msgid "http url" msgstr "" #: src/commands.py:718 msgid "command name" msgstr "" #: src/commands.py:726 msgid "ip" msgstr "" #: src/commands.py:732 msgid "letter" msgstr "" #: src/commands.py:764 msgid "plugin" msgstr "" #: src/commands.py:772 msgid "irc color" msgstr "" #: src/conf.py:130 msgid "" "Determines whether this plugin is loaded\n" " by default." msgstr "" #: src/conf.py:134 msgid "" "Determines whether this plugin is\n" " publicly visible." msgstr "" #: src/conf.py:231 msgid "Determines the bot's default nick." msgstr "" #: src/conf.py:234 msgid "" "Determines what alternative\n" " nicks will be used if the primary nick (supybot.nick) isn't available. A\n" " %s in this nick is replaced by the value of supybot.nick when used. If no\n" " alternates are given, or if all are used, the supybot.nick will be perturbed\n" " appropriately until an unused nick is found." msgstr "" #: src/conf.py:241 msgid "" "Determines the bot's ident string, if the server\n" " doesn't provide one by default." msgstr "" #: src/conf.py:258 msgid "" "Determines the real name which the bot sends to\n" " the server. A standard real name using the current version of the bot\n" " will be generated if this is left empty." msgstr "" #: src/conf.py:267 msgid "Determines what networks the bot will connect to." msgstr "" #: src/conf.py:377 msgid "" "Determines what password will be used on %s. Yes, we know that\n" " technically passwords are server-specific and not network-specific,\n" " but this is the best we can do right now." msgstr "" #: src/conf.py:381 msgid "" "Space-separated list of servers the bot will connect to for %s.\n" " Each will be tried in order, wrapping back to the first when the cycle\n" " is completed." msgstr "" #: src/conf.py:385 msgid "Space-separated list of channels the bot will join only on %s." msgstr "" #: src/conf.py:389 msgid "" "Determines whether the bot will attempt to connect with SSL\n" " sockets to %s." msgstr "" #: src/conf.py:392 msgid "" "Space-separated list\n" " of fingerprints of trusted certificates for this network.\n" " Supported hash algorithms are: %L.\n" " If non-empty, Certification Authority signatures will not be used to\n" " verify certificates." msgstr "" #: src/conf.py:398 msgid "" "A certificate that is trusted to verify\n" " certificates of this network (aka. Certificate Authority)." msgstr "" #: src/conf.py:401 msgid "Deprecated config value, keep it to False." msgstr "" #: src/conf.py:404 msgid "" "Determines what certificate file (if any) the bot will use to\n" " connect with SSL sockets to %s." msgstr "" #: src/conf.py:407 msgid "" "Determines what key (if any) will be used to join the\n" " channel." msgstr "" #: src/conf.py:409 msgid "" "Determines\n" " what nick the bot will use on this network. If empty, defaults to\n" " supybot.nick." msgstr "" #: src/conf.py:412 msgid "" "Determines\n" " the bot's ident string, if the server doesn't provide one by default.\n" " If empty, defaults to supybot.ident." msgstr "" #: src/conf.py:415 msgid "" "Determines\n" " the real name which the bot sends to the server. If empty, defaults to\n" " supybot.user" msgstr "" #: src/conf.py:419 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. If empty, defaults to\n" " supybot.protocols.irc.umodes" msgstr "" #: src/conf.py:424 msgid "" "Determines what SASL username will be used on %s. This should\n" " be the bot's account name." msgstr "" #: src/conf.py:427 msgid "Determines what SASL password will be used on %s." msgstr "" #: src/conf.py:430 msgid "" "Determines what SASL ECDSA key (if any) will be used on %s.\n" " The public key must be registered with NickServ for SASL\n" " ECDSA-NIST256P-CHALLENGE to work." msgstr "" #: src/conf.py:435 msgid "" "Determines what SASL mechanisms will be tried and in which order.\n" " " msgstr "" #: src/conf.py:438 msgid "" "Determines whether the bot will abort the connection if the\n" " none of the enabled SASL mechanism succeeded." msgstr "" #: src/conf.py:441 msgid "" "If not empty, determines the hostname:port of the socks proxy that\n" " will be used to connect to this network." msgstr "" #: src/conf.py:461 msgid "Determines how urls should be formatted." msgstr "" #: src/conf.py:469 msgid "" "Determines how timestamps\n" " printed for human reading should be formatted. Refer to the Python\n" " documentation for the time module to see valid formatting characters for\n" " time formats." msgstr "" #: src/conf.py:484 msgid "" "Determines whether elapsed times will be given\n" " as \"1 day, 2 hours, 3 minutes, and 15 seconds\" or as \"1d 2h 3m 15s\"." msgstr "" #: src/conf.py:495 msgid "" "Maximum number of items in a list\n" " before the end is replaced with 'and others'. Set to 0 to always\n" " show the entire list." msgstr "" #: src/conf.py:512 msgid "other" msgstr "" #: src/conf.py:517 msgid "" "Determines the absolute maximum length of\n" " the bot's reply -- no reply will be passed through the bot with a length\n" " greater than this." msgstr "" #: src/conf.py:522 msgid "" "Determines whether the bot will break up long\n" " messages into chunks and allow users to use the 'more' command to get the\n" " remaining chunks." msgstr "" #: src/conf.py:527 msgid "" "Determines what the maximum number of\n" " chunks (for use with the 'more' command) will be." msgstr "" #: src/conf.py:531 msgid "" "Determines how long individual chunks\n" " will be. If set to 0, uses our super-tweaked,\n" " get-the-most-out-of-an-individual-message default." msgstr "" #: src/conf.py:536 msgid "" "Determines how many mores will be sent\n" " instantly (i.e., without the use of the more command, immediately when\n" " they are formed). Defaults to 1, which means that a more command will be\n" " required for all but the first chunk." msgstr "" #: src/conf.py:542 msgid "" "Determines whether the bot will send\n" " multi-message replies in a single message. This defaults to True \n" " in order to prevent the bot from flooding. If this is set to False\n" " the bot will send multi-message replies on multiple lines." msgstr "" #: src/conf.py:548 msgid "" "Determines whether the bot will reply with an\n" " error message when it is addressed but not given a valid command. If this\n" " value is False, the bot will remain silent, as long as no other plugins\n" " override the normal behavior." msgstr "" #: src/conf.py:555 msgid "" "Determines whether error messages that result\n" " from bugs in the bot will show a detailed error message (the uncaught\n" " exception) or a generic error message." msgstr "" #: src/conf.py:559 msgid "" "Determines whether the bot will send error\n" " messages to users in private. You might want to do this in order to keep\n" " channel traffic to minimum. This can be used in combination with\n" " supybot.reply.error.withNotice." msgstr "" #: src/conf.py:564 msgid "" "Determines whether the bot will send error\n" " messages to users via NOTICE instead of PRIVMSG. You might want to do this\n" " so users can ignore NOTICEs from the bot and not have to see error\n" " messages; or you might want to use it in combination with\n" " supybot.reply.error.inPrivate so private errors don't open a query window\n" " in most IRC clients." msgstr "" #: src/conf.py:571 msgid "" "Determines whether the bot will *not* provide\n" " details in the error\n" " message to users who attempt to call a command for which they do not have\n" " the necessary capability. You may wish to make this True if you don't want\n" " users to understand the underlying security system preventing them from\n" " running certain commands." msgstr "" #: src/conf.py:579 msgid "" "Determines whether the bot will reply\n" " privately when replying in a channel, rather than replying to the whole\n" " channel." msgstr "" #: src/conf.py:584 msgid "" "Determines whether the bot will reply with a\n" " notice when replying in a channel, rather than replying with a privmsg as\n" " normal." msgstr "" #: src/conf.py:590 msgid "" "Determines whether the bot will reply with a\n" " notice when it is sending a private message, in order not to open a /query\n" " window in clients." msgstr "" #: src/conf.py:595 msgid "" "Determines whether the bot will always prefix\n" " the user's nick to its reply to that user's command." msgstr "" #: src/conf.py:599 msgid "" "Determines whether the bot should attempt to\n" " reply to all messages even if they don't address it (either via its nick\n" " or a prefix character). If you set this to True, you almost certainly want\n" " to set supybot.reply.whenNotCommand to False." msgstr "" #: src/conf.py:605 msgid "" "Determines whether the bot will allow you to\n" " send channel-related commands outside of that channel. Sometimes people\n" " find it confusing if a channel-related command (like Filter.outfilter)\n" " changes the behavior of the channel but was sent outside the channel\n" " itself." msgstr "" #: src/conf.py:612 msgid "" "Determines whether the bot will unidentify\n" " someone when that person changes their nick. Setting this to True\n" " will cause the bot to track such changes. It defaults to False for a\n" " little greater security." msgstr "" #: src/conf.py:618 msgid "" "Determines whether the bot will always join a\n" " channel when it's invited. If this value is False, the bot will only join\n" " a channel if the user inviting it has the 'admin' capability (or if it's\n" " explicitly told to join the channel using the Admin.join command)." msgstr "" #: src/conf.py:624 msgid "" "Supybot normally replies with the full help\n" " whenever a user misuses a command. If this value is set to True, the bot\n" " will only reply with the syntax of the command (the first line of the\n" " help) rather than the full help." msgstr "" #: src/conf.py:639 msgid "" "Determines what prefix characters the bot will\n" " reply to. A prefix character is a single character that the bot will use\n" " to determine what messages are addressed to it; when there are no prefix\n" " characters set, it just uses its nick. Each character in this string is\n" " interpreted individually; you can have multiple prefix chars\n" " simultaneously, and if any one of them is used as a prefix the bot will\n" " assume it is being addressed." msgstr "" #: src/conf.py:648 msgid "" "Determines what strings the\n" " bot will reply to when they are at the beginning of the message. Whereas\n" " prefix.chars can only be one character (although there can be many of\n" " them), this variable is a space-separated list of strings, so you can\n" " set something like '@@ ??' and the bot will reply when a message is\n" " prefixed by either @@ or ??." msgstr "" #: src/conf.py:655 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick, rather than with a prefix character." msgstr "" #: src/conf.py:658 msgid "" "Determines whether the bot will reply when\n" " people address it by its nick at the end of the message, rather than at\n" " the beginning." msgstr "" #: src/conf.py:662 msgid "" "Determines what extra nicks\n" " the bot will always respond to when addressed by, even if its current nick\n" " is something else." msgstr "" #: src/conf.py:672 msgid "The operation succeeded." msgstr "" #: src/conf.py:673 msgid "" "Determines what message the bot replies with when a command succeeded.\n" " If this configuration variable is empty, no success message will be\n" " sent." msgstr "" #: src/conf.py:678 msgid "" "An error has occurred and has been logged.\n" " Please contact this bot's administrator for more information." msgstr "" #: src/conf.py:679 msgid "" "\n" " Determines what error message the bot gives when it wants to be\n" " ambiguous." msgstr "" #: src/conf.py:684 msgid "" "An error has occurred and has been logged.\n" " Check the logs for more information." msgstr "" #: src/conf.py:685 msgid "" "Determines what error\n" " message the bot gives to the owner when it wants to be ambiguous." msgstr "" #: src/conf.py:689 msgid "" "Your hostmask doesn't match or your password\n" " is wrong." msgstr "" #: src/conf.py:690 msgid "" "Determines what message the bot replies with when\n" " someone tries to use a command that requires being identified or having a\n" " password and neither credential is correct." msgstr "" #: src/conf.py:696 msgid "" "I can't find %s in my user\n" " database. If you didn't give a user name, then I might not know what your\n" " user is, and you'll need to identify before this command might work." msgstr "" #: src/conf.py:699 msgid "" "Determines what error message the bot replies with when someone tries\n" " to accessing some information on a user the bot doesn't know about." msgstr "" #: src/conf.py:703 msgid "" "You must be registered to use this command.\n" " If you are already registered, you must either identify (using the identify\n" " command) or add a hostmask matching your current hostmask (using the\n" " \"hostmask add\" command)." msgstr "" #: src/conf.py:706 msgid "" "Determines what error message the bot\n" " replies with when someone tries to do something that requires them to be\n" " registered but they're not currently recognized." msgstr "" #: src/conf.py:711 msgid "" "You don't have the %s capability. If you\n" " think that you should have this capability, be sure that you are identified\n" " before trying again. The 'whoami' command can tell you if you're\n" " identified." msgstr "" #: src/conf.py:714 msgid "" "Determines what error message is given when the bot\n" " is telling someone they aren't cool enough to use the command they tried to\n" " use." msgstr "" #: src/conf.py:719 msgid "" "You're missing some capability you need.\n" " This could be because you actually possess the anti-capability for the\n" " capability that's required of you, or because the channel provides that\n" " anti-capability by default, or because the global capabilities include\n" " that anti-capability. Or, it could be because the channel or\n" " supybot.capabilities.default is set to False, meaning that no commands are\n" " allowed unless explicitly in your capabilities. Either way, you can't do\n" " what you want to do." msgstr "" #: src/conf.py:727 msgid "" "Determines what generic error message is given when the bot is telling\n" " someone that they aren't cool enough to use the command they tried to use,\n" " and the author of the code calling errorNoCapability didn't provide an\n" " explicit capability for whatever reason." msgstr "" #: src/conf.py:733 msgid "" "That operation cannot be done in a\n" " channel." msgstr "" #: src/conf.py:734 msgid "" "Determines what error messages the bot sends to people\n" " who try to do things in a channel that really should be done in\n" " private." msgstr "" #: src/conf.py:739 msgid "" "This may be a bug. If you think it is,\n" " please file a bug report at\n" " ." msgstr "" #: src/conf.py:742 msgid "" "Determines what message the bot sends when it thinks you've\n" " encountered a bug that the developers don't know about." msgstr "" #: src/conf.py:749 msgid "$Type #$id: $text (added by $username at $at)" msgstr "" #: src/conf.py:750 msgid "" "Format used by generic database plugins (Lart, Dunno, Prase, Success,\n" " Quote, ...) to show an entry. You can use the following variables:\n" " $type/$types/$Type/$Types (plugin name and variants), $id, $text,\n" " $at (creation time), $userid/$username/$nick (author)." msgstr "" #: src/conf.py:760 msgid "" "A floating point number of seconds to throttle\n" " snarfed URLs, in order to prevent loops between two bots snarfing the same\n" " URLs and having the snarfed URL in the output of the snarf message." msgstr "" #: src/conf.py:765 msgid "" "Determines the number of seconds\n" " between running the upkeep function that flushes (commits) open databases,\n" " collects garbage, and records some useful statistics at the debugging\n" " level." msgstr "" #: src/conf.py:771 msgid "" "Determines whether the bot will periodically\n" " flush data and configuration files to disk. Generally, the only time\n" " you'll want to set this to False is when you want to modify those\n" " configuration files by hand and don't want the bot to flush its current\n" " version over your modifications. Do note that if you change this to False\n" " inside the bot, your changes won't be flushed. To make this change\n" " permanent, you must edit the registry yourself." msgstr "" #: src/conf.py:797 msgid "" "Determines what characters are valid for quoting\n" " arguments to commands in order to prevent them from being tokenized.\n" " " msgstr "" #: src/conf.py:804 msgid "" "Determines whether the bot will allow nested\n" " commands, which rule. You definitely should keep this on." msgstr "" #: src/conf.py:807 msgid "" "Determines what the maximum number of\n" " nested commands will be; users will receive an error if they attempt\n" " commands more nested than this." msgstr "" #: src/conf.py:816 msgid "" "Supybot allows you to specify what brackets\n" " are used for your nested commands. Valid sets of brackets include\n" " [], <>, {}, and (). [] has strong historical motivation, but <> or\n" " () might be slightly superior because they cannot occur in a nick.\n" " If this string is empty, nested commands will not be allowed in this\n" " channel." msgstr "" #: src/conf.py:823 msgid "" "Supybot allows nested commands. Enabling this\n" " option will allow nested commands with a syntax similar to UNIX pipes, for\n" " example: 'bot: foo | bar'." msgstr "" #: src/conf.py:828 msgid "" "Determines what commands have default\n" " plugins set, and which plugins are set to be the default for each of those\n" " commands." msgstr "" #: src/conf.py:834 msgid "" "Determines what plugins automatically get precedence over all\n" " other plugins when selecting a default plugin for a command. By\n" " default, this includes the standard loaded plugins. You probably\n" " shouldn't change this if you don't know what you're doing; if you do\n" " know what you're doing, then also know that this set is\n" " case-sensitive." msgstr "" #: src/conf.py:844 msgid "" "Allows this bot's owner user to use commands\n" " that grants them shell access. This config variable exists in case you want\n" " to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)\n" " from gaining shell access to the bot's server by impersonating the owner.\n" " Setting this to False also disables plugins and commands that can be\n" " used to indirectly gain shell access." msgstr "" #: src/conf.py:859 msgid "" "Determines the interval used for\n" " the history storage." msgstr "" #: src/conf.py:862 msgid "" "Determines whether the bot will defend itself\n" " against command-flooding." msgstr "" #: src/conf.py:865 msgid "" "Determines how many commands users are\n" " allowed per minute. If a user sends more than this many commands in any\n" " 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.punishment seconds." msgstr "" #: src/conf.py:870 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with commands." msgstr "" #: src/conf.py:873 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for command flooding." msgstr "" #: src/conf.py:877 msgid "" "Determines whether the bot will defend itself\n" " against invalid command-flooding." msgstr "" #: src/conf.py:880 msgid "" "Determines how many invalid commands users\n" " are allowed per minute. If a user sends more than this many invalid\n" " commands in any 60 second period, they will be ignored for\n" " supybot.abuse.flood.command.invalid.punishment seconds. Typically, this\n" " value is lower than supybot.abuse.flood.command.maximum, since it's far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" #: src/conf.py:888 msgid "" "Determines how many seconds the bot\n" " will ignore users who flood it with invalid commands. Typically, this\n" " value is higher than supybot.abuse.flood.command.punishment, since it's far\n" " less likely (and far more annoying) for users to flood with invalid\n" " commands than for them to flood with valid commands." msgstr "" #: src/conf.py:894 msgid "" "Determines whether the bot will notify people\n" " that they're being ignored for invalid command flooding." msgstr "" #: src/conf.py:903 msgid "" "Determines the default length of time a\n" " driver should block waiting for input." msgstr "" #: src/conf.py:911 msgid "" "Determines what driver module the \n" " bot will use. Current, the only (and default) driver is Socket." msgstr "" #: src/conf.py:915 msgid "" "Determines the maximum time the bot will\n" " wait before attempting to reconnect to an IRC server. The bot may, of\n" " course, reconnect earlier if possible." msgstr "" #: src/conf.py:967 msgid "" "Determines what directory configuration data is\n" " put into." msgstr "" #: src/conf.py:970 msgid "Determines what directory data is put into." msgstr "" #: src/conf.py:972 msgid "" "Determines what directory backup data is put\n" " into. Set it to /dev/null to disable backup (it is a special value,\n" " so it also works on Windows and systems without /dev/null)." msgstr "" #: src/conf.py:979 msgid "" "Determines what directory temporary files\n" " are put into." msgstr "" #: src/conf.py:982 msgid "" "Determines what directory files of the\n" " web server (templates, custom images, ...) are put into." msgstr "" #: src/conf.py:995 msgid "" "Determines what directories\n" " the bot will look for plugins in. Accepts a comma-separated list of\n" " strings.\n" " This means that to add another directory, you can nest the former value and\n" " add a new one. E.g. you can say: bot: 'config supybot.directories.plugins\n" " [config supybot.directories.plugins], newPluginDirectory'." msgstr "" #: src/conf.py:1003 msgid "" "List of all plugins that were\n" " ever loaded. Currently has no effect whatsoever. You probably want to use\n" " the 'load' or 'unload' commands, or edit supybot.plugins.\n" " instead of this." msgstr "" #: src/conf.py:1008 msgid "" "Determines whether the bot will always load\n" " important plugins (Admin, Channel, Config, Misc, Owner, and User)\n" " regardless of what their configured state is. Generally, if these plugins\n" " are configured not to load, you didn't do it on purpose, and you still\n" " want them to load. Users who don't want to load these plugins are smart\n" " enough to change the value of this variable appropriately :)" msgstr "" #: src/conf.py:1036 msgid "" "Determines what databases are available for use. If this\n" " value is not configured (that is, if its value is empty) then sane defaults\n" " will be provided." msgstr "" #: src/conf.py:1042 msgid "" "Determines what filename will be used\n" " for the users database. This file will go into the directory specified by\n" " the supybot.directories.conf variable." msgstr "" #: src/conf.py:1046 msgid "" "Determines how long it takes identification to\n" " time out. If the value is less than or equal to zero, identification never\n" " times out." msgstr "" #: src/conf.py:1050 msgid "" "Determines whether the bot will allow users to\n" " unregister their users. This can wreak havoc with already-existing\n" " databases, so by default we don't allow it. Enable this at your own risk.\n" " (Do also note that this does not prevent the owner of the bot from using\n" " the unregister command.)\n" " " msgstr "" #: src/conf.py:1059 msgid "" "Determines what filename will be used\n" " for the ignores database. This file will go into the directory specified\n" " by the supybot.directories.conf variable." msgstr "" #: src/conf.py:1065 msgid "" "Determines what filename will be used\n" " for the channels database. This file will go into the directory specified\n" " by the supybot.directories.conf variable." msgstr "" #: src/conf.py:1071 msgid "" "Determines what filename will be used\n" " for the networks database. This file will go into the directory specified\n" " by the supybot.directories.conf variable." msgstr "" #: src/conf.py:1103 msgid "" "Determines whether the bot will require user\n" " registration to use 'add' commands in database-based Supybot\n" " plugins." msgstr "" #: src/conf.py:1107 msgid "" "Determines whether database-based plugins that\n" " can be channel-specific will be so. This can be overridden by individual\n" " channels. Do note that the bot needs to be restarted immediately after\n" " changing this variable or your db plugins may not work for your channel;\n" " also note that you may wish to set\n" " supybot.databases.plugins.channelSpecific.link appropriately if you wish\n" " to share a certain channel's databases globally." msgstr "" #: src/conf.py:1115 msgid "" "Determines what channel global\n" " (non-channel-specific) databases will be considered a part of. This is\n" " helpful if you've been running channel-specific for awhile and want to turn\n" " the databases for your primary channel into global databases. If\n" " supybot.databases.plugins.channelSpecific.link.allow prevents linking, the\n" " current channel will be used. Do note that the bot needs to be restarted\n" " immediately after changing this variable or your db plugins may not work\n" " for your channel." msgstr "" #: src/conf.py:1124 msgid "" "Determines whether another channel's global\n" " (non-channel-specific) databases will be allowed to link to this channel's\n" " databases. Do note that the bot needs to be restarted immediately after\n" " changing this variable or your db plugins may not work for your channel.\n" " " msgstr "" #: src/conf.py:1142 msgid "" "Determines\n" " whether CDB databases will be allowed as a database implementation." msgstr "" #: src/conf.py:1145 msgid "" "Determines how often CDB databases will have\n" " their modifications flushed to disk. When the number of modified records\n" " is greater than this fraction of the total number of records, the database\n" " will be entirely flushed to disk." msgstr "" #: src/conf.py:1237 msgid "" "Determines what will be used as the\n" " default banmask style." msgstr "" #: src/conf.py:1241 msgid "" "Determines whether the bot will strictly\n" " follow the RFC; currently this only affects what strings are\n" " considered to be nicks. If you're using a server or a network that\n" " requires you to message a nick such as services@this.network.server\n" " then you you should set this to False." msgstr "" #: src/conf.py:1248 msgid "" "Determines whether the bot will enable\n" " draft/experimental extensions of the IRC protocol. Setting this to True\n" " may break your bot at any time without warning and/or break your\n" " configuration irreversibly. So keep it False unless you know what you are\n" " doing." msgstr "" #: src/conf.py:1255 msgid "" "Determines what certificate file (if any) the bot\n" " will use connect with SSL sockets by default." msgstr "" #: src/conf.py:1259 msgid "" "Determines what user modes the bot will request\n" " from the server when it first connects. Many people might choose +i; some\n" " networks allow +x, which indicates to the auth services on those networks\n" " that you should be given a fake host." msgstr "" #: src/conf.py:1265 msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv4." msgstr "" #: src/conf.py:1269 msgid "" "Determines what vhost the bot will bind to before\n" " connecting a server (IRC, HTTP, ...) via IPv6." msgstr "" #: src/conf.py:1273 msgid "" "Determines how many old messages the bot will\n" " keep around in its history. Changing this variable will not take effect\n" " on a network until it is reconnected." msgstr "" #: src/conf.py:1278 msgid "" "A floating point number of seconds to throttle\n" " queued messages -- that is, messages will not be sent faster than once per\n" " throttleTime seconds." msgstr "" #: src/conf.py:1283 msgid "" "Determines whether the bot will send PINGs to\n" " the server it's connected to in order to keep the connection alive and\n" " discover earlier when it breaks. Really, this option only exists for\n" " debugging purposes: you always should make it True unless you're testing\n" " some strange server issues." msgstr "" #: src/conf.py:1290 msgid "" "Determines the number of seconds between sending\n" " pings to the server, if pings are being sent to the server." msgstr "" #: src/conf.py:1295 msgid "" "Determines whether the bot will refuse\n" " duplicated messages to be queued for delivery to the server. This is a\n" " safety mechanism put in place to prevent plugins from sending the same\n" " message multiple times; most of the time it doesn't matter, unless you're\n" " doing certain kinds of plugin hacking." msgstr "" #: src/conf.py:1303 msgid "" "Determines how many seconds must elapse between\n" " JOINs sent to the server." msgstr "" #: src/conf.py:1311 msgid "" "Determines how many bytes the bot will\n" " 'peek' at when looking through a URL for a doctype or title or something\n" " similar. It'll give up after it reads this many bytes, even if it hasn't\n" " found what it was looking for." msgstr "" #: src/conf.py:1335 msgid "" "Determines what HTTP proxy all HTTP requests should go\n" " through. The value should be of the form 'host:port'." msgstr "" #: src/conf.py:1375 msgid "" "If set, the Accept-Language HTTP header will be set to this\n" " value for requests. Useful for overriding the auto-detected language based on\n" " the server's location." msgstr "" #: src/conf.py:1381 msgid "" "If set, the User-Agent HTTP header will be set to a randomly\n" " selected value from this comma-separated list of strings for requests." msgstr "" #: src/conf.py:1389 msgid "" "Determines whether server certificates\n" " will be verified, which checks whether the server certificate is signed\n" " by a known certificate authority, and aborts the connection if it is not.\n" " This is assumed to be True of serverFingerprints or authorityCertificate\n" " is set." msgstr "" #: src/conf.py:1416 msgid "" "If true, uses IPV6_V6ONLY to disable\n" " forwaring of IPv4 traffic to IPv6 sockets. On *nix, has the same\n" " effect as setting kernel variable net.ipv6.bindv6only to 1." msgstr "" #: src/conf.py:1420 msgid "" "Space-separated list of IPv4 hosts the HTTP server\n" " will bind." msgstr "" #: src/conf.py:1423 msgid "" "Space-separated list of IPv6 hosts the HTTP server will\n" " bind." msgstr "" #: src/conf.py:1426 msgid "" "Determines what port the HTTP server will\n" " bind." msgstr "" #: src/conf.py:1429 msgid "" "Determines whether the server will stay\n" " alive if no plugin is using it. This also means that the server will\n" " start even if it is not used." msgstr "" #: src/conf.py:1433 msgid "" "Determines the path of the file served as\n" " favicon to browsers." msgstr "" #: src/conf.py:1436 msgid "" "Determines the public URL of the server.\n" " By default it is http://:/, but you will want to change\n" " this if there is a reverse proxy (nginx, apache, ...) in front of\n" " the bot." msgstr "" #: src/conf.py:1446 msgid "" "Determines whether the bot will ignore\n" " unidentified users by default. Of course, that'll make it\n" " particularly hard for those users to register or identify with the bot\n" " without adding their hostmasks, but that's your problem to solve." msgstr "" #: src/conf.py:1453 msgid "" "A string that is the external IP of the bot. If this is the\n" " empty string, the bot will attempt to find out its IP dynamically (though\n" " sometimes that doesn't work, hence this variable). This variable is not used\n" " by Limnoria and its built-in plugins: see supybot.protocols.irc.vhost /\n" " supybot.protocols.irc.vhost6 to set the IRC bind host, and\n" " supybot.servers.http.hosts4 / supybot.servers.http.hosts6 to set the HTTP\n" " server bind host." msgstr "" #: src/conf.py:1472 msgid "" "Determines what the default timeout for socket\n" " objects will be. This means that *all* sockets will timeout when this many\n" " seconds has gone by (unless otherwise modified by the author of the code\n" " that uses the sockets)." msgstr "" #: src/conf.py:1478 msgid "" "Determines what file the bot should write its PID\n" " (Process ID) to, so you can kill it more easily. If it's left unset (as is\n" " the default) then no PID file will be written. A restart is required for\n" " changes to this variable to take effect." msgstr "" #: src/conf.py:1488 msgid "" "Determines whether the bot will automatically\n" " thread all commands." msgstr "" #: src/conf.py:1491 msgid "" "Determines whether the bot will automatically\n" " flush all flushers *very* often. Useful for debugging when you don't know\n" " what's breaking or when, but think that it might be logged." msgstr "" #: src/httpserver.py:62 msgid "Supybot Web server index" msgstr "" #: src/httpserver.py:67 msgid "Here is a list of the plugins that have a Web interface:" msgstr "" #: src/httpserver.py:245 msgid "" "\n" " This is a default response of the Supybot HTTP server. If you see this\n" " message, it probably means you are developing a plugin, and you have\n" " neither overriden this message or defined an handler for this query." msgstr "" #: src/httpserver.py:291 msgid "" "\n" " I am a pretty clever IRC bot, but I suck at serving Web pages, particulary\n" " if I don't know what to serve.\n" " What I'm saying is you just triggered a 404 Not Found, and I am not\n" " trained to help you in such a case." msgstr "" #: src/httpserver.py:310 msgid "Request not handled." msgstr "" #: src/httpserver.py:317 msgid "No plugins available." msgstr "" #: src/httpserver.py:335 src/httpserver.py:353 src/httpserver.py:391 msgid "Request not handled" msgstr "" #: src/httpserver.py:378 msgid "No favicon set." msgstr "" #: src/ircutils.py:573 msgid "is an op on %L" msgstr "" #: src/ircutils.py:575 msgid "is a halfop on %L" msgstr "" #: src/ircutils.py:577 msgid "is voiced on %L" msgstr "" #: src/ircutils.py:580 msgid "is also on %L" msgstr "" #: src/ircutils.py:582 msgid "is on %L" msgstr "" #: src/ircutils.py:585 msgid "isn't on any publicly visible channels" msgstr "" #: src/ircutils.py:593 src/ircutils.py:594 src/ircutils.py:600 msgid "" msgstr "" #: src/ircutils.py:602 msgid " %s is away: %s." msgstr "" #: src/ircutils.py:607 msgid " identified" msgstr "" #: src/ircutils.py:613 msgid "%s (%s) has been%s on server %s since %s (idle for %s). %s %s.%s" msgstr "" #: src/ircutils.py:617 msgid "%s (%s) has been%s on server %s and disconnected on %s." msgstr "" #: src/questions.py:61 msgid "Sorry, that response was not an option." msgstr "" #: src/questions.py:110 msgid "Sorry, you must enter a value." msgstr "" #: src/questions.py:130 msgid "Enter password: " msgstr "" #: src/questions.py:132 msgid "Re-enter password: " msgstr "" #: src/questions.py:145 msgid "Passwords don't match." msgstr "" #: src/registry.py:219 msgid "%r is not a valid entry in %r" msgstr "" #: src/registry.py:568 msgid "Value must be either True or False (or On or Off), not %r." msgstr "" #: src/registry.py:585 msgid "Value must be an integer, not %r." msgstr "" #: src/registry.py:595 msgid "Value must be a non-negative integer, not %r." msgstr "" #: src/registry.py:604 msgid "Value must be positive (non-zero) integer, not %r." msgstr "" #: src/registry.py:613 msgid "Value must be a floating-point number, not %r." msgstr "" #: src/registry.py:629 msgid "Value must be a floating-point number greater than zero, not %r." msgstr "" #: src/registry.py:640 msgid "Value must be a floating point number in the range [0, 1], not %r." msgstr "" #: src/registry.py:655 msgid "Value should be a valid Python string, not %r." msgstr "" #: src/registry.py:693 msgid "Valid values include %L." msgstr "" #: src/registry.py:695 msgid "Valid values include %L, not %%r." msgstr "" #: src/registry.py:769 msgid "Value must be a valid regular expression, not %r." msgstr "" #: src/utils/gen.py:113 msgid "year" msgstr "" #: src/utils/gen.py:116 msgid "week" msgstr "" #: src/utils/gen.py:119 msgid "day" msgstr "" #: src/utils/gen.py:122 msgid "hour" msgstr "" #: src/utils/gen.py:126 msgid "minute" msgstr "" #: src/utils/gen.py:129 msgid "second" msgstr "" #: src/utils/gen.py:138 msgid "%s ago" msgstr "" #: src/utils/str.py:349 msgid "and" msgstr "" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3257546 limnoria-2023.11.18/man/0000755000175000017500000000000014535072535012675 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-adduser.10000644000175000017500000000225414535072470016217 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-adduser.1 .\" .TH LIMNORIA-ADDUSER 1 "APRIL 2005" .SH NAME limnoria-adduser \- Adds a user to a Limnoria users.conf file .SH SYNOPSIS .B limnoria-adduser .RI [ options ] " users.conf .SH DESCRIPTION .B limnoria-adduser adds a user to the specified users.conf file. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-u " NAME" "\fR,\fP \-\^\-username=" NAME Specifies the username to use for the new user. .TP .BR \-p " PASSWORD" "\fR,\fP \-\^\-password=" PASSWORD Specifies the password to use for the new user. .TP .BR \-c " CAPABILITY" "\fR,\fP \-\^\-capability=" CAPABILITY Capability the user should have; this option may be given multiple times. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-botchk.10000644000175000017500000000271714535072470016046 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-botchk.1 .\" .TH LIMNORIA-BOTCHK 1 "APRIL 2005" .SH NAME limnoria-botchk \- A script to start Limnoria if it's not already running. .SH SYNOPSIS .B limnoria-botchk .RI [ options ] .SH DESCRIPTION .B limnoria-botchk is a script that will start Limnoria if it detects that one is not currently running. This can be useful for scheduling .IR limnoria (1) to run via .IR cron (8). .SH OPTIONS .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-v ", " \-\^\-verbose Use verbose output when running the script. .TP .BI \-\^\-botdir= BOTDIR Determines which directory the bot be started in. .TP .BI \-\^\-pidfile= PIDFILE Specifies the name of the pidfile to look for. This should be relative to the given botdir. .TP .BI \-\^\-limnoria= LIMNORIA Specifies the location of .IR limnoria (1). If this is not given, it is assumed that .IR limnoria (1) is in the user's $PATH. .TP .BI \-\^\-conffile= CONFFILE Specifies the path to the bot's configuration file. This will be used when (re)starting the bot. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-plugin-create.10000644000175000017500000000214514535072470017326 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-plugin-create.1 .\" .TH LIMNORIA-PLUGIN-CREATE 1 "APRIL 2005" .SH NAME limnoria-plugin-create \- A wizard for creating Limnoria plugins .SH SYNOPSIS .B limnoria-plugin-create .RI [ options ] .SH DESCRIPTION .B limnoria-plugin-create is a wizard that creates a template python source file for a new .IR limnoria (1) plugin. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BI \-n " NAME" "\fR,\fP \-\^\-name=" NAME Sets the name for the plugin. .TP .BR \-t ", " \-\^\-thread Makes the plugin threaded. .TP .BI \-\^\-real\-name= REALNAME Specify what real name the copyright is assigned to. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-plugin-doc.10000644000175000017500000000254214535072470016631 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-plugin-doc.1 .\" .TH LIMNORIA-PLUGIN-DOC 1 "May 2009" .SH NAME limnoria-plugin-doc \- Generates the documentation for a Limnoria plugin .SH SYNOPSIS .B limnoria-plugin-doc .RI [ options ] .SH DESCRIPTION .B limnoria-plugin-doc is used to generate documentation (StructuredText or reStructuredText format) for a .IR limnoria (1) plugin. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-c ", " \-\^\-clean Clean the various data/conf/log directories after generating the docs. .TP .BR \-o ", " \-\^\-output\-dir= \fIOUTPUTDIR Specifies the directory in which to write the documentation for the plugin. .TP .BR \-f ", " \-\^\-format= \fIFORMAT Specifies which output format to use. Choices are 'rst' or 'stx'. .TP .BI \-\^\-plugins\-dir= PLUGINSDIRS Looks in the given directory for plugins and generates documentation for all of them. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-reset-password.10000644000175000017500000000200214535072470017541 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-reset-password.1 .\" .TH LIMNORIA-RESET-PASSWORD 1 "JUNE 2022" .SH NAME limnoria-reset-password \- Changes a user's password in a Limnoria users.conf file .SH SYNOPSIS .B limnoria-reset-password .RI [ options ] " users.conf .SH DESCRIPTION .B limnoria-reset-password changes a user's password in a Limnoria users.conf file .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-u " NAME" "\fR,\fP \-\^\-username=" NAME Specifies the username to use for the new user. .TP .BR \-p " PASSWORD" "\fR,\fP \-\^\-password=" PASSWORD Specifies the new password to use for the new user. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-adduser (1) .SH AUTHOR This manual page was originally written by Valentin Lorentz . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-test.10000644000175000017500000000273314535072470015551 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-test.1 .\" .TH LIMNORIA-TEST 1 "OCTOBER 2005" .SH NAME limnoria-test \- Runs the test suite for a Limnoria plugin .SH SYNOPSIS .B limnoria-test .RI [ options ] " plugins .SH DESCRIPTION .B limnoria-test Runs the test suite for a Limnoria plugin .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-c ", " \-\^\-clean Cleans the various data/conf/logs directories before running tests. .TP .BR \-t " TIMEOUT" "\fR,\fP \-\^\-timeout=" TIMEOUT Specifies the timeout for tests to return responses. .TP .BR \-v ", " \-\^\-verbose Sets the verbose flag, logging extra information about each test that runs. .TP .BR \-\^\-no\-network Prevents the network-based tests from being run. .TP .BR \-\^\-trace Traces all calls made. Unless you're really in a pinch, you probably shouldn't do this; it results in copious amounts of output. .TP .BR "\fR,\fP \-\^\-plugins\-dir=" PLUGINSDIR Looks in the given directory for plugins and loads the tests for all of them. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria-wizard.10000644000175000017500000000224714535072470016072 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-wizard.1 .\" .TH LIMNORIA-WIZARD 1 "SEPTEMBER 2004" .SH NAME limnoria-wizard \- A wizard for creating Limnoria configuration files .SH SYNOPSIS .B limnoria-wizard .RI [ options ] .SH DESCRIPTION .B limnoria-wizard is an in-depth wizard that provides a nice user interface for creating configuration files for .IR limnoria (1). .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .B \-\^\-allow\-root Determines whether the wizard will be allowed to run as root. You do not want this. Do not do it. Even if you think you want it, you do not. .TP .B \-\^\-no\-network Determines whether the wizard will be allowed to run without a network connection. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/limnoria.10000644000175000017500000000366214535072470014576 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria.1 .\" .TH LIMNORIA 1 "JULY 2009" .SH NAME limnoria - A robust and user friendly Python IRC bot .SH SYNOPSIS .B limnoria .RI [ options ] " configFile .SH DESCRIPTION .B Limnoria is a robust, user-friendly, and programmer-friendly Python IRC bot. It aims to be an adequate replacement for most existing IRC bots. It includes a very flexible and powerful ACL system for controlling access to commands, as well as more than 50 builtin plugins providing around 400 actual commands. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-P ", " \-\^\-profile Enable profiling. .TP .BI \-n " NICK" "\fR,\fP \-\^\-nick=" NICK Nick the bot should use. .TP .BI \-u " USER" "\fR,\fP \-\^\-user=" USER Full username the bot should use. .TP .BI \-i " IDENT" "\fR,\fP \-\^\-ident=" IDENT Ident the bot should use. .TP .BR \-d ", " \-\^\-daemon Determines whether the bot will daemonize. This is a no-op on non-POSIX systems. .TP .B \-\^\-allow\-default\-owner Determines whether the bot will allow its defaultCapabilities not to include "\-owner", thus giving all users the owner capability by default. This is dumb, hence we require a command-line option to enable it. .TP .B \-\^\-allow\-root Determines whether the bot will be allowed to run as root. You do not want this. Do not do it. Even if you think you want it, you do not. .TP .B \-\^\-debug Determines whether some extra debugging stuff will be logged by this script. .SH "SEE ALSO" .IR python (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-adduser.10000644000175000017500000000225414535072470016112 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-adduser.1 .\" .TH LIMNORIA-ADDUSER 1 "APRIL 2005" .SH NAME limnoria-adduser \- Adds a user to a Limnoria users.conf file .SH SYNOPSIS .B limnoria-adduser .RI [ options ] " users.conf .SH DESCRIPTION .B limnoria-adduser adds a user to the specified users.conf file. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-u " NAME" "\fR,\fP \-\^\-username=" NAME Specifies the username to use for the new user. .TP .BR \-p " PASSWORD" "\fR,\fP \-\^\-password=" PASSWORD Specifies the password to use for the new user. .TP .BR \-c " CAPABILITY" "\fR,\fP \-\^\-capability=" CAPABILITY Capability the user should have; this option may be given multiple times. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-botchk.10000644000175000017500000000271714535072470015741 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-botchk.1 .\" .TH LIMNORIA-BOTCHK 1 "APRIL 2005" .SH NAME limnoria-botchk \- A script to start Limnoria if it's not already running. .SH SYNOPSIS .B limnoria-botchk .RI [ options ] .SH DESCRIPTION .B limnoria-botchk is a script that will start Limnoria if it detects that one is not currently running. This can be useful for scheduling .IR limnoria (1) to run via .IR cron (8). .SH OPTIONS .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-v ", " \-\^\-verbose Use verbose output when running the script. .TP .BI \-\^\-botdir= BOTDIR Determines which directory the bot be started in. .TP .BI \-\^\-pidfile= PIDFILE Specifies the name of the pidfile to look for. This should be relative to the given botdir. .TP .BI \-\^\-limnoria= LIMNORIA Specifies the location of .IR limnoria (1). If this is not given, it is assumed that .IR limnoria (1) is in the user's $PATH. .TP .BI \-\^\-conffile= CONFFILE Specifies the path to the bot's configuration file. This will be used when (re)starting the bot. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-plugin-create.10000644000175000017500000000214514535072470017221 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-plugin-create.1 .\" .TH LIMNORIA-PLUGIN-CREATE 1 "APRIL 2005" .SH NAME limnoria-plugin-create \- A wizard for creating Limnoria plugins .SH SYNOPSIS .B limnoria-plugin-create .RI [ options ] .SH DESCRIPTION .B limnoria-plugin-create is a wizard that creates a template python source file for a new .IR limnoria (1) plugin. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BI \-n " NAME" "\fR,\fP \-\^\-name=" NAME Sets the name for the plugin. .TP .BR \-t ", " \-\^\-thread Makes the plugin threaded. .TP .BI \-\^\-real\-name= REALNAME Specify what real name the copyright is assigned to. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-plugin-doc.10000644000175000017500000000254214535072470016524 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-plugin-doc.1 .\" .TH LIMNORIA-PLUGIN-DOC 1 "May 2009" .SH NAME limnoria-plugin-doc \- Generates the documentation for a Limnoria plugin .SH SYNOPSIS .B limnoria-plugin-doc .RI [ options ] .SH DESCRIPTION .B limnoria-plugin-doc is used to generate documentation (StructuredText or reStructuredText format) for a .IR limnoria (1) plugin. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-c ", " \-\^\-clean Clean the various data/conf/log directories after generating the docs. .TP .BR \-o ", " \-\^\-output\-dir= \fIOUTPUTDIR Specifies the directory in which to write the documentation for the plugin. .TP .BR \-f ", " \-\^\-format= \fIFORMAT Specifies which output format to use. Choices are 'rst' or 'stx'. .TP .BI \-\^\-plugins\-dir= PLUGINSDIRS Looks in the given directory for plugins and generates documentation for all of them. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-reset-password.10000644000175000017500000000200214535072470017434 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-reset-password.1 .\" .TH LIMNORIA-RESET-PASSWORD 1 "JUNE 2022" .SH NAME limnoria-reset-password \- Changes a user's password in a Limnoria users.conf file .SH SYNOPSIS .B limnoria-reset-password .RI [ options ] " users.conf .SH DESCRIPTION .B limnoria-reset-password changes a user's password in a Limnoria users.conf file .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-u " NAME" "\fR,\fP \-\^\-username=" NAME Specifies the username to use for the new user. .TP .BR \-p " PASSWORD" "\fR,\fP \-\^\-password=" PASSWORD Specifies the new password to use for the new user. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-adduser (1) .SH AUTHOR This manual page was originally written by Valentin Lorentz . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-test.10000644000175000017500000000273314535072470015444 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-test.1 .\" .TH LIMNORIA-TEST 1 "OCTOBER 2005" .SH NAME limnoria-test \- Runs the test suite for a Limnoria plugin .SH SYNOPSIS .B limnoria-test .RI [ options ] " plugins .SH DESCRIPTION .B limnoria-test Runs the test suite for a Limnoria plugin .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-c ", " \-\^\-clean Cleans the various data/conf/logs directories before running tests. .TP .BR \-t " TIMEOUT" "\fR,\fP \-\^\-timeout=" TIMEOUT Specifies the timeout for tests to return responses. .TP .BR \-v ", " \-\^\-verbose Sets the verbose flag, logging extra information about each test that runs. .TP .BR \-\^\-no\-network Prevents the network-based tests from being run. .TP .BR \-\^\-trace Traces all calls made. Unless you're really in a pinch, you probably shouldn't do this; it results in copious amounts of output. .TP .BR "\fR,\fP \-\^\-plugins\-dir=" PLUGINSDIR Looks in the given directory for plugins and loads the tests for all of them. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot-wizard.10000644000175000017500000000224714535072470015765 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria-wizard.1 .\" .TH LIMNORIA-WIZARD 1 "SEPTEMBER 2004" .SH NAME limnoria-wizard \- A wizard for creating Limnoria configuration files .SH SYNOPSIS .B limnoria-wizard .RI [ options ] .SH DESCRIPTION .B limnoria-wizard is an in-depth wizard that provides a nice user interface for creating configuration files for .IR limnoria (1). .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .B \-\^\-allow\-root Determines whether the wizard will be allowed to run as root. You do not want this. Do not do it. Even if you think you want it, you do not. .TP .B \-\^\-no\-network Determines whether the wizard will be allowed to run without a network connection. .SH "SEE ALSO" .IR python (1), .IR limnoria (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/man/supybot.10000644000175000017500000000366214535072470014471 0ustar00valval.\" Process this file with .\" groff -man -Tascii limnoria.1 .\" .TH LIMNORIA 1 "JULY 2009" .SH NAME limnoria - A robust and user friendly Python IRC bot .SH SYNOPSIS .B limnoria .RI [ options ] " configFile .SH DESCRIPTION .B Limnoria is a robust, user-friendly, and programmer-friendly Python IRC bot. It aims to be an adequate replacement for most existing IRC bots. It includes a very flexible and powerful ACL system for controlling access to commands, as well as more than 50 builtin plugins providing around 400 actual commands. .SH OPTIONS .TP .B \-\^\-version Show version of program. .TP .BR \-h ", " \-\^\-help Show summary of options. .TP .BR \-P ", " \-\^\-profile Enable profiling. .TP .BI \-n " NICK" "\fR,\fP \-\^\-nick=" NICK Nick the bot should use. .TP .BI \-u " USER" "\fR,\fP \-\^\-user=" USER Full username the bot should use. .TP .BI \-i " IDENT" "\fR,\fP \-\^\-ident=" IDENT Ident the bot should use. .TP .BR \-d ", " \-\^\-daemon Determines whether the bot will daemonize. This is a no-op on non-POSIX systems. .TP .B \-\^\-allow\-default\-owner Determines whether the bot will allow its defaultCapabilities not to include "\-owner", thus giving all users the owner capability by default. This is dumb, hence we require a command-line option to enable it. .TP .B \-\^\-allow\-root Determines whether the bot will be allowed to run as root. You do not want this. Do not do it. Even if you think you want it, you do not. .TP .B \-\^\-debug Determines whether some extra debugging stuff will be logged by this script. .SH "SEE ALSO" .IR python (1), .IR limnoria-test (1), .IR limnoria-botchk (1), .IR limnoria-wizard (1), .IR limnoria-adduser (1), .IR limnoria-plugin-doc (1), .IR limnoria-plugin-create (1) .SH AUTHOR This manual page was originally written by James McCoy . Permission is granted to copy, distribute and/or modify this document under the terms of the Limnoria license, a BSD-style license. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3257546 limnoria-2023.11.18/plugins/0000755000175000017500000000000014535072535013603 5ustar00valval././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3257546 limnoria-2023.11.18/plugins/Admin/0000755000175000017500000000000014535072535014633 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/__init__.py0000644000175000017500000000456614535072470016755 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ These are commands useful for administrating the bot; they all require their caller to have the 'admin' capability. This plugin is loaded by default. """ import supybot import supybot.world as world __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you\'re keeping the plugin in CVS or some similar system. __version__ = "%%VERSION%%" # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/config.py0000644000175000017500000000441014535072470016447 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Admin') def configure(advanced): # This will be called by supybot to configure this module. advanced is # a bool that specifies whether the user identified themself as an advanced # user or not. You should effect your configuration by manipulating the # registry as appropriate. from supybot.questions import expect, anything, something, yn conf.registerPlugin('Admin', True) Admin = conf.registerPlugin('Admin') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Admin/locales/0000755000175000017500000000000014535072535016255 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/locales/de.po0000644000175000017500000001760014535072470017207 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-31 13:37+0100\n" "Last-Translator: Florian Besser \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: German\n" "X-Poedit-Country: Germany\n" #: plugin.py:46 msgid "" "This plugin provides access to administrative commands, such as\n" " adding capabilities, managing ignore lists, and joining channels.\n" " This is a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:57 msgid "Nick/channel temporarily unavailable." msgstr "Nick/Kanal temporär nicht verfügbar." #: plugin.py:85 msgid "Cannot join %s, it's full." msgstr "Kann %s nicht beitreten, der Kanal ist voll." #: plugin.py:93 msgid "Cannot join %s, I was not invited." msgstr "Kann %s nicht beitreten, ich wurde nicht eingeladen." #: plugin.py:101 msgid "Cannot join %s, I am banned." msgstr "Ich kann %s nicht betreten, ich bin gebannt." #: plugin.py:109 msgid "Cannot join %s, my keyword was wrong." msgstr "Ich kann %s nicht beitreten, mein Schlüsselwort ist falsch." #: plugin.py:117 plugin.py:126 msgid "Cannot join %s, I'm not identified with NickServ." msgstr "Ich kann %s nicht betreten, ich bin nicht mit NickServ identifiziert." #: plugin.py:156 msgid "" " []\n" "\n" " Tell the bot to join the given channel. If is given, it is " "used\n" " when attempting to join the channel.\n" " " msgstr "" " []\n" "\n" "Sagt dem Bot dem angegeben Kanal beizutreten. Falls angegeben " "wird, wird dieser benutzt um zu versuchen den Kanal zu betreten." #: plugin.py:162 msgid "channel" msgstr "" #: plugin.py:169 msgid "I'm already too close to maximum number of channels for this network." msgstr "Ich bin schon zu nah an den maximalen Kanälen für dieses Netzwerk." #: plugin.py:178 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the channels the bot is on.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Listet die Hostmasken auf, die der Bot ignoriert." #: plugin.py:187 msgid "I'm not currently in any channels." msgstr "Momentan bin ich in keinen Kanälen." #: plugin.py:193 msgid "My connection is restricted, I can't change nicks." msgstr "Meine Verbindung ist begrenzt, I kann meinen Nick nicht wechseln." #: plugin.py:200 msgid "Someone else is already using that nick." msgstr "Jemand anderes benutzt diesen Nick schon." #: plugin.py:207 #, fuzzy msgid "I can't change nick, I'm currently banned in %s." msgstr "Ich kann meinen Nick nicht ändern, der Server sagte %q." #: plugin.py:215 msgid "I can't change nicks, the server said %q." msgstr "Ich kann meinen Nick nicht ändern, der Server sagte %q." #: plugin.py:229 #, fuzzy msgid "" "[] []\n" "\n" " Changes the bot's nick to . If no nick is given, returns the\n" " bot's current nick.\n" " " msgstr "" "[]\n" "\n" "Ändert den Nick des Bots zu . Falls nicht angegeben wird, wird " "der momentane Botnick zurückgegeben." #: plugin.py:248 msgid "" " \n" "\n" " Gives the user specified by (or the user to whom " "\n" " currently maps) the specified capability \n" " " msgstr "" " \n" "\n" "Gibt dem angebenen Benutzer (oder dem auf den die " "zutrifft) die angegebene Fähigkeit." #: plugin.py:268 msgid "" "The \"owner\" capability can't be added in the bot. Use the supybot-adduser " "program (or edit the users.conf file yourself) to add an owner capability." msgstr "" "Die \"owner\" Fähigkeit kann nicht über den Bot hinzugefügt werden. Benutze " "das supybot-adduser Programm (oder verändere users.conf per Hand) um die " "Besitzer Fähigkeit hinzuzufügen." #: plugin.py:279 msgid "You can't add capabilities you don't have." msgstr "Du kannst keine Fähigkeiten hinzufügen, die du nicht hast." #: plugin.py:284 msgid "" " \n" "\n" " Takes from the user specified by (or the user to whom\n" " currently maps) the specified capability " "\n" " " msgstr "" " \n" "\n" "Nimmt dem Benutzer der durch (oder dem Benutzer auf den momentan " " zeigt) angeben wird die angegeben Fähigkeit ." #: plugin.py:296 msgid "That user doesn't have that capability." msgstr "Der Benutzer hat diese Fähigkeit nicht." #: plugin.py:298 msgid "You can't remove capabilities you don't have." msgstr "Du kannst keine Fähigkeiten entfernen, die du nicht hast." #: plugin.py:306 msgid "" " []\n" "\n" " This will set a persistent ignore on or the hostmask\n" " currently associated with . is an optional " "argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " " msgstr "" " []\n" "\n" "Es wird eine beständige Ignorierung auf oder auf die Hostmaske " "die momentan mit verbunden wird gesetzt. ist " "optional, das legt fest wann die Ignorierung abläuft;falls dies nicht " "angegeben wird, wird die Ignorierung niemals ablaufen." #: plugin.py:319 msgid "" "\n" "\n" " This will remove the persistent ignore on or the\n" " hostmask currently associated with .\n" " " msgstr "" "\n" "\n" "Wird die beständige Ignorierung, von oder der Hostmaske die " "momentan mit dem verbunden wird, aufheben." #: plugin.py:328 msgid "%s wasn't in the ignores database." msgstr "%s war nicht in der Datenbank für Ignorierungen." #: plugin.py:333 msgid "" "takes no arguments\n" "\n" " Lists the hostmasks that the bot is ignoring.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Listet die Hostmasken auf, die der Bot ignoriert." #: plugin.py:341 msgid "I'm not currently globally ignoring anyone." msgstr "Momentan ignoriere ich niemanden global." #: plugin.py:345 msgid "" "takes no arguments\n" "\n" " Clears the current send queue for this network.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Leert die momentane Sendenwarteschlange für dieses Netzwerk." #: plugin.py:354 msgid "" " [ ...]\n" "\n" " Perform (with associated s on all channels on current " "network." msgstr "" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the channels the bot is on. Must be given in private, in " #~ "order\n" #~ " to protect the secrecy of secret channels.\n" #~ " " #~ msgstr "" #~ "hat keine Argumenten\n" #~ "\n" #~ "Gibt die Kanäle aus in denen der Bot sich befindet. Dieser Befehl muss " #~ "privat gegeben werden, um das Geheimnis der geheimen Kanale zu wahren." #~ msgid "" #~ "[] []\n" #~ "\n" #~ " Tells the bot to part the list of channels you give it. " #~ " is\n" #~ " only necessary if you want the bot to part a channel other than " #~ "the\n" #~ " current channel. If is specified, use it as the part\n" #~ " message.\n" #~ " " #~ msgstr "" #~ "[] []\n" #~ "\n" #~ "Sagt dem Bot die Liste von angebenen Kanälen zu verlassen. ist " #~ "nur notwendig, falls der Bot einen anderen Kanal als den Momentanen " #~ "verlassen soll. Falls angegeben wird, wird dies als " #~ "Verlassensnachricht verwendet." #~ msgid "I'm not in %s." #~ msgstr "Ich bin nicht in %s." #~ msgid "That nick is currently banned." #~ msgstr "Dieser Nick ist momentan gebannt." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/locales/fi.po0000644000175000017500000002123114535072470017210 0ustar00valval# Admin plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Finnish translation of Admin plugin in Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:19+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "X-Generator: Poedit 1.6.10\n" #: plugin.py:46 msgid "" "This plugin provides access to administrative commands, such as\n" " adding capabilities, managing ignore lists, and joining channels.\n" " This is a core Supybot plugin that should not be removed!" msgstr "" "Tämä plugin antaa pääsyn ylläpitäviin komentoihin, kuten valtuuksien " "lisäämiseen,\n" " ignore-listojen hallintaan ja kanaville liittymiseen. Tämä on ydin Supybot-" "plugin, jota ei pitäisi poistaa!s" #: plugin.py:57 msgid "Nick/channel temporarily unavailable." msgstr "Nimimerkki/kanava on väliaikaisesti saavuttamattomissa." #: plugin.py:85 msgid "Cannot join %s, it's full." msgstr "Ei voida liittyä kanavalle %s, se on täynnä." #: plugin.py:93 msgid "Cannot join %s, I was not invited." msgstr "Ei voi liittyä kanavalle %s, minua ei ole kutsuttu." #: plugin.py:101 msgid "Cannot join %s, I am banned." msgstr "Ei voi liittyä kanavalle %s, se on antanut minulle porttikiellon." #: plugin.py:109 msgid "Cannot join %s, my keyword was wrong." msgstr "En voi liittyä kanavalle %s, minun avainsana oli väärä." #: plugin.py:117 plugin.py:126 msgid "Cannot join %s, I'm not identified with NickServ." msgstr "En voi liittyä kanavalle %s, koska en ole tunnistautunut NickServille." #: plugin.py:156 msgid "" " []\n" "\n" " Tell the bot to join the given channel. If is given, it is " "used\n" " when attempting to join the channel.\n" " " msgstr "" " []\n" "\n" " Käskee botin liittyä annetulle kanavalle. Jos on annettu, " "sitä käytetään\n" " yrittäessä liittyä kanavalle.\n" " " #: plugin.py:162 msgid "channel" msgstr "" #: plugin.py:169 msgid "I'm already too close to maximum number of channels for this network." msgstr "Minä olen jo liian lähellä kanavien maksimimäärää tässä verkossa." #: plugin.py:178 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the channels the bot is on.\n" " " msgstr "" "Ei ota parametrejä\n" "\n" " Luetteloi hostmaskit jotka ovat botin huomiotta jättämis listalla.\n" " " #: plugin.py:187 msgid "I'm not currently in any channels." msgstr "En juuri nyt ole millään kanavalla." #: plugin.py:193 msgid "My connection is restricted, I can't change nicks." msgstr "Minun yhteyteni on rajoitettu. En voi vaihtaa nimimerkkiä." #: plugin.py:200 msgid "Someone else is already using that nick." msgstr "Joku muu käyttää jo tuota nimimerkkiä." #: plugin.py:207 #, fuzzy msgid "I can't change nick, I'm currently banned in %s." msgstr "Minä en voi vaihtaa nimimerkkiä, koska palvelin sanoi %q" #: plugin.py:215 msgid "I can't change nicks, the server said %q." msgstr "Minä en voi vaihtaa nimimerkkiä, koska palvelin sanoi %q" #: plugin.py:229 msgid "" "[] []\n" "\n" " Changes the bot's nick to . If no nick is given, returns the\n" " bot's current nick.\n" " " msgstr "" "[] []\n" "\n" " Vaihtaa botin nimimerkin . Mikäli nimimerkkiä ei anneta, " "botin nykyinen\n" " nimimerkki palautetaan.\n" " " #: plugin.py:248 msgid "" " \n" "\n" " Gives the user specified by (or the user to whom " "\n" " currently maps) the specified capability \n" " " msgstr "" " \n" "\n" " Antaa määrittämälle käyttäjälle (tai käyttäjälle jonka " "\n" " ilmoittaa) määritetyn valtuuden \n" " " #: plugin.py:268 msgid "" "The \"owner\" capability can't be added in the bot. Use the supybot-adduser " "program (or edit the users.conf file yourself) to add an owner capability." msgstr "" "\"Owner\" valtuutta ei voi lisätä botissa. Käytä supybot-adduser ohjelmaa " "(tai muokkaa users.conf tiedostoa itse) lisätäksesi owner valtuuden." #: plugin.py:279 msgid "You can't add capabilities you don't have." msgstr "Et voi lisätä valtuuksia, joita sinulla ei ole." #: plugin.py:284 msgid "" " \n" "\n" " Takes from the user specified by (or the user to whom\n" " currently maps) the specified capability " "\n" " " msgstr "" " \n" "\n" " Ottaa määrittämältä käyttäjältä (tai käyttäjältä " "johon\n" " sopii) määritetyn valtuuden \n" " " #: plugin.py:296 msgid "That user doesn't have that capability." msgstr "Tuolla käyttäjällä ei tuota valtuutta." #: plugin.py:298 msgid "You can't remove capabilities you don't have." msgstr "Sinä et voi poistaa valtuuksia, joita sinulla ei ole." #: plugin.py:306 msgid "" " []\n" "\n" " This will set a persistent ignore on or the hostmask\n" " currently associated with . is an optional " "argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " " msgstr "" " []\n" "\n" " Tämä asettaa pysyvän huomiotta jättämisen tai " "hostmaskiin,\n" " joka on tällä hetkellä yhdistetty . on " "vaihtoehtoinen paremetri,\n" " joka määrittää (\"sekuntit\") joiden jälkeen huomiotta jättäminen " "poistetaan; jos\n" " sitä ei ole annettu, huomiotta jättäminen ei vanhene ikinä " "automaattisesti.\n" " " #: plugin.py:319 msgid "" "\n" "\n" " This will remove the persistent ignore on or the\n" " hostmask currently associated with .\n" " " msgstr "" "\n" "\n" " Tämä poistaa pysyvän huomiotta jättämisen tai\n" " hostmaskista joka on tällä hetkellä yhdistetty .\n" " " #: plugin.py:328 msgid "%s wasn't in the ignores database." msgstr "%s ei ollut huomiotta jätettävien tietokannassa." #: plugin.py:333 msgid "" "takes no arguments\n" "\n" " Lists the hostmasks that the bot is ignoring.\n" " " msgstr "" "Ei ota parametrejä\n" "\n" " Luetteloi hostmaskit jotka ovat botin huomiotta jättämis listalla.\n" " " #: plugin.py:341 msgid "I'm not currently globally ignoring anyone." msgstr "En tällä hetkellä jätä ketään huomioitta globaalisti." #: plugin.py:345 msgid "" "takes no arguments\n" "\n" " Clears the current send queue for this network.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Tyhjentää nykyisen lähetysjonon tälle verkolle.\n" " " #: plugin.py:354 msgid "" " [ ...]\n" "\n" " Perform (with associated s on all channels on current " "network." msgstr "" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the channels the bot is on. Must be given in private, in " #~ "order\n" #~ " to protect the secrecy of secret channels.\n" #~ " " #~ msgstr "" #~ "Ei ota parametrejä\n" #~ "\n" #~ " Palauttaa listan kanavista, joilla botti on. Täytyy antaa " #~ "yksityisviestillä salaistenkanavien\n" #~ " salaisuuden suojelemiseksi.\n" #~ " " #~ msgid "" #~ "[] []\n" #~ "\n" #~ " Tells the bot to part the list of channels you give it. " #~ " is\n" #~ " only necessary if you want the bot to part a channel other than " #~ "the\n" #~ " current channel. If is specified, use it as the part\n" #~ " message.\n" #~ " " #~ msgstr "" #~ "[] []\n" #~ "\n" #~ " Käskee botin poistua kanavilta, jotka annat sille. on\n" #~ " vaadittu jos haluat botin poistuvat muulta, kuin \n" #~ " nykyiseltä kanavalta. Jos on määritetty, sitä käytetään\n" #~ " poistumisviestissä.\n" #~ " " #~ msgid "I'm not in %s." #~ msgstr "Minä en ole kanavalla %s." #~ msgid "That nick is currently banned." #~ msgstr "Tuolla nimimerkillä on tällähetkellä porttikielto." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/locales/fr.po0000644000175000017500000001720114535072470017223 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: ProgVal \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: Ascii\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:46 msgid "" "This plugin provides access to administrative commands, such as\n" " adding capabilities, managing ignore lists, and joining channels.\n" " This is a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:57 msgid "Nick/channel temporarily unavailable." msgstr "Nick/canal temporairement indisponible" #: plugin.py:85 msgid "Cannot join %s, it's full." msgstr "Ne peut joindre %s, il est plein." #: plugin.py:93 msgid "Cannot join %s, I was not invited." msgstr "Ne peut joindre %s, pas invité." #: plugin.py:101 msgid "Cannot join %s, I am banned." msgstr "Ne peut joindre %s, j'y suis banni." #: plugin.py:109 msgid "Cannot join %s, my keyword was wrong." msgstr "Ne peut joindre %s, mon mot de passe est mauvais." #: plugin.py:117 plugin.py:126 msgid "Cannot join %s, I'm not identified with NickServ." msgstr "Ne peut joindre %s, je ne suis pas identifié auprès de NickServ." #: plugin.py:156 msgid "" " []\n" "\n" " Tell the bot to join the given channel. If is given, it is " "used\n" " when attempting to join the channel.\n" " " msgstr "" " []\n" "\n" "Dit au bot de rejoindre le canal donné. Si la est donnée, elle est " "utilisée pour rejoindre le canal." #: plugin.py:162 msgid "channel" msgstr "" #: plugin.py:169 msgid "I'm already too close to maximum number of channels for this network." msgstr "Je suis déjà sur un nombre de canaux trop grand pour ce réseau." #: plugin.py:178 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the channels the bot is on.\n" " " msgstr "" "Ne prend pas d'argument\n" "\n" "Liste les masques d'hôte que le bot ignore." #: plugin.py:187 msgid "I'm not currently in any channels." msgstr "Je ne suis actuellement sur aucun canal." #: plugin.py:193 msgid "My connection is restricted, I can't change nicks." msgstr "Ma connexion est restreinte, je ne peux changer de nick." #: plugin.py:200 msgid "Someone else is already using that nick." msgstr "Quelqu'un d'autre utilise déjà ce nick." #: plugin.py:207 #, fuzzy msgid "I can't change nick, I'm currently banned in %s." msgstr "Je ne peux changer de nick, le serveur a dit %q." #: plugin.py:215 msgid "I can't change nicks, the server said %q." msgstr "Je ne peux changer de nick, le serveur a dit %q." #: plugin.py:229 msgid "" "[] []\n" "\n" " Changes the bot's nick to . If no nick is given, returns the\n" " bot's current nick.\n" " " msgstr "" "[] []\n" "\n" "Change le nick du bot à . Si aucun nick n'est donné, retourne le nick " "actuel du bot." #: plugin.py:248 msgid "" " \n" "\n" " Gives the user specified by (or the user to whom " "\n" " currently maps) the specified capability \n" " " msgstr "" " \n" "\n" "Donne la à l'utilisateur spécifié par (ou l'utilisateur à " "qui correspond )." #: plugin.py:268 msgid "" "The \"owner\" capability can't be added in the bot. Use the supybot-adduser " "program (or edit the users.conf file yourself) to add an owner capability." msgstr "" "La capacité \"owner\" ne peut être ajoutée via le bot. Utilisez le programme " "supybot-adduser (ou éditez le fichier users.conf vous-même) pour ajouter la " "capacité owner." #: plugin.py:279 msgid "You can't add capabilities you don't have." msgstr "Vous ne pouvez ajouter des capacités que vous n'avez pas." #: plugin.py:284 msgid "" " \n" "\n" " Takes from the user specified by (or the user to whom\n" " currently maps) the specified capability " "\n" " " msgstr "" " \n" "\n" "Retire la à l'utilisateur spécifié par le (ou celui à qui " "correspond le )." #: plugin.py:296 msgid "That user doesn't have that capability." msgstr "Cet utilisateur n'a pas cette capacité." #: plugin.py:298 msgid "You can't remove capabilities you don't have." msgstr "Vous ne pouvez retirer des capacités que vous n'avez pas." #: plugin.py:306 msgid "" " []\n" "\n" " This will set a persistent ignore on or the hostmask\n" " currently associated with . is an optional " "argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " " msgstr "" " []\n" "\n" "Ajoute un masque d'ignorance persistant sur le , ou sur le " "masque d'hôte de . est un argument optionnel spécifiant " "quand (en \"secondes à partir de maintenant\") l'ignorance expirera ; si " "elle n'est pas donnée, l'ignorance n'expirera jamais." #: plugin.py:319 msgid "" "\n" "\n" " This will remove the persistent ignore on or the\n" " hostmask currently associated with .\n" " " msgstr "" "\n" "\n" "Ceci retirera le masque d'ignorance persistant sur le , ou " "sur le masque d'hôte associé au ." #: plugin.py:328 msgid "%s wasn't in the ignores database." msgstr "%s n'étais pas dans ma base de données d'ignorance." #: plugin.py:333 msgid "" "takes no arguments\n" "\n" " Lists the hostmasks that the bot is ignoring.\n" " " msgstr "" "Ne prend pas d'argument\n" "\n" "Liste les masques d'hôte que le bot ignore." #: plugin.py:341 msgid "I'm not currently globally ignoring anyone." msgstr "Je n'ignore actuellement personne globalement." #: plugin.py:345 msgid "" "takes no arguments\n" "\n" " Clears the current send queue for this network.\n" " " msgstr "" "Ne prend pas d'argument\n" "\n" "Vide la queue en attente pour ce réseau." #: plugin.py:354 msgid "" " [ ...]\n" "\n" " Perform (with associated s on all channels on current " "network." msgstr "" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the channels the bot is on. Must be given in private, in " #~ "order\n" #~ " to protect the secrecy of secret channels.\n" #~ " " #~ msgstr "" #~ "Ne prend pas d'argument \n" #~ "\n" #~ "Retourne les canaux sur lesquels le bot est. Doit être en privé, dans le " #~ "but d'éviter que les canaux secrets ne soient divulgués." #~ msgid "" #~ "[] []\n" #~ "\n" #~ " Tells the bot to part the list of channels you give it. " #~ " is\n" #~ " only necessary if you want the bot to part a channel other than " #~ "the\n" #~ " current channel. If is specified, use it as the part\n" #~ " message.\n" #~ " " #~ msgstr "" #~ "[] []\n" #~ "\n" #~ "Dit au bot de partir de la liste de canaux que vous avez donnée. " #~ "n'est nécessaire que si vous voulez que le bot parte d'un autre canal que " #~ "l'actuel. Si la est spécifiée, elle est utilisée comme message " #~ "de départ." #~ msgid "I'm not in %s." #~ msgstr "Je ne suis pas sur %s." #~ msgid "That nick is currently banned." #~ msgstr "Ce nick est banni." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/locales/it.po0000644000175000017500000001766014535072470017241 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-15 20:25+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" #: plugin.py:46 msgid "" "This plugin provides access to administrative commands, such as\n" " adding capabilities, managing ignore lists, and joining channels.\n" " This is a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:57 msgid "Nick/channel temporarily unavailable." msgstr "Nick/canale temporaneamente non disponibile." #: plugin.py:85 msgid "Cannot join %s, it's full." msgstr "Non posso entrare in %s, è pieno." #: plugin.py:93 msgid "Cannot join %s, I was not invited." msgstr "Non posso entrare in %s, non sono stato invitato." #: plugin.py:101 msgid "Cannot join %s, I am banned." msgstr "Non posso entrare in %s, sono stato bannato." #: plugin.py:109 msgid "Cannot join %s, my keyword was wrong." msgstr "Non posso entrare in %s, la password era sbagliata." #: plugin.py:117 plugin.py:126 msgid "Cannot join %s, I'm not identified with NickServ." msgstr "Non posso entrare in %s, non sono identificato con NickServ." #: plugin.py:156 msgid "" " []\n" "\n" " Tell the bot to join the given channel. If is given, it is " "used\n" " when attempting to join the channel.\n" " " msgstr "" " []\n" "\n" " Dice al bot di entrare nel canale specificato. Se è " "fornita,\n" " viene usata quando si tenta di entrare nel canale.\n" " " #: plugin.py:162 msgid "channel" msgstr "" #: plugin.py:169 msgid "I'm already too close to maximum number of channels for this network." msgstr "Sono già troppo vicino al numero massimo di canali per questa rete." #: plugin.py:178 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the channels the bot is on.\n" " " msgstr "" "Non necessita argomenti\n" "\n" " Elenca le hostmask che il bot sta ignorando.\n" " " #: plugin.py:187 msgid "I'm not currently in any channels." msgstr "Al momento non sono in nessun canale." #: plugin.py:193 msgid "My connection is restricted, I can't change nicks." msgstr "La mia connessione è limitata, non posso cambiare nick." #: plugin.py:200 msgid "Someone else is already using that nick." msgstr "Qualcun altro sta utilizzando questo nick." #: plugin.py:207 #, fuzzy msgid "I can't change nick, I'm currently banned in %s." msgstr "Non posso cambiare nick, il server ha detto %q." #: plugin.py:215 msgid "I can't change nicks, the server said %q." msgstr "Non posso cambiare nick, il server ha detto %q." #: plugin.py:229 #, fuzzy msgid "" "[] []\n" "\n" " Changes the bot's nick to . If no nick is given, returns the\n" " bot's current nick.\n" " " msgstr "" "[]\n" "\n" " Cambia il nick del bot in . Se non ne viene fornito uno, " "restituisce\n" " quello attuale.\n" " " #: plugin.py:248 msgid "" " \n" "\n" " Gives the user specified by (or the user to whom " "\n" " currently maps) the specified capability \n" " " msgstr "" " \n" "\n" " Dà all'utente specificato da (o quello a cui corrisponde\n" " attualmente) la specificata.\n" " " #: plugin.py:268 msgid "" "The \"owner\" capability can't be added in the bot. Use the supybot-adduser " "program (or edit the users.conf file yourself) to add an owner capability." msgstr "" "La capacità \"owner\" non può essere aggiunta al bot. Utilizzare il " "programma supybot-adduser (o modificare il file users.conf) per aggiungerla." #: plugin.py:279 msgid "You can't add capabilities you don't have." msgstr "Non puoi aggiungere capacità che non hai." #: plugin.py:284 msgid "" " \n" "\n" " Takes from the user specified by (or the user to whom\n" " currently maps) the specified capability " "\n" " " msgstr "" " \n" "\n" " Rimuove l'utente specificato da (o quello a cui " "corrisponde\n" " attualmente) la specificata\n" " " #: plugin.py:296 msgid "That user doesn't have that capability." msgstr "Questo utente non ha tale capacità." #: plugin.py:298 msgid "You can't remove capabilities you don't have." msgstr "Non puoi rimuovere capacità che non hai." #: plugin.py:306 msgid "" " []\n" "\n" " This will set a persistent ignore on or the hostmask\n" " currently associated with . is an optional " "argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " " msgstr "" " []\n" "\n" " Imposta un ignore permanente su o l'hostmask " "attualmente\n" " associata a . è un argomento opzionale per " "specificare\n" " quando (in \"secondi a partire da subito\") scadrà l'ignore; se " "non fornito,\n" " questo non scadrà mai.\n" " " #: plugin.py:319 msgid "" "\n" "\n" " This will remove the persistent ignore on or the\n" " hostmask currently associated with .\n" " " msgstr "" "\n" "\n" " Rimuove l'ignore persistente su o l'attuale hostmask " "associata a .\n" " " #: plugin.py:328 msgid "%s wasn't in the ignores database." msgstr "%s non è nel mio database degli ignorati." #: plugin.py:333 msgid "" "takes no arguments\n" "\n" " Lists the hostmasks that the bot is ignoring.\n" " " msgstr "" "Non necessita argomenti\n" "\n" " Elenca le hostmask che il bot sta ignorando.\n" " " #: plugin.py:341 msgid "I'm not currently globally ignoring anyone." msgstr "Al momento non sto ignorando nessuno." #: plugin.py:345 msgid "" "takes no arguments\n" "\n" " Clears the current send queue for this network.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Pulisce l'attuale coda dei messaggi da inviare (interrompe il flood) " "per questa rete.\n" " " #: plugin.py:354 msgid "" " [ ...]\n" "\n" " Perform (with associated s on all channels on current " "network." msgstr "" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the channels the bot is on. Must be given in private, in " #~ "order\n" #~ " to protect the secrecy of secret channels.\n" #~ " " #~ msgstr "" #~ "non necessita argomenti\n" #~ "\n" #~ " Restituisce i canali dove è presente il bot. Deve essere " #~ "richiesto in\n" #~ " privato per preservare la segretezza dei canali privati.\n" #~ " " #~ msgid "" #~ "[] []\n" #~ "\n" #~ " Tells the bot to part the list of channels you give it. " #~ " is\n" #~ " only necessary if you want the bot to part a channel other than " #~ "the\n" #~ " current channel. If is specified, use it as the part\n" #~ " message.\n" #~ " " #~ msgstr "" #~ "[] []\n" #~ "\n" #~ " Fornisce al bot l'elenco dei canali da cui uscire. è " #~ "necessario\n" #~ " solo se si vuole far uscire il bot da un canale diverso da quello " #~ "attuale.\n" #~ " Se viene specificato, verrà usato come messaggio di " #~ "uscita.\n" #~ " " #~ msgid "I'm not in %s." #~ msgstr "Non sono in %s." #~ msgid "That nick is currently banned." #~ msgstr "Il nick è attualmente bannato." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/plugin.py0000644000175000017500000003546014535072470016511 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import time import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.schedule as schedule import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Admin') class Admin(callbacks.Plugin): """This plugin provides access to administrative commands, such as adding capabilities, managing ignore lists, and joining channels. This is a core Supybot plugin that should not be removed!""" def __init__(self, irc): self.__parent = super(Admin, self) self.__parent.__init__(irc) self.joins = {} self.pendingNickChanges = {} @internationalizeDocstring def do437(self, irc, msg): """Nick/channel temporarily unavailable.""" target = msg.args[0] t = time.time() + 30 if irc.isChannel(target): # Let's schedule a rejoin. networkGroup = conf.supybot.networks.get(irc.network) def rejoin(): irc.queueMsg(networkGroup.channels.join(target)) # We don't need to schedule something because we'll get another # 437 when we try to join later. schedule.addEvent(rejoin, t) self.log.info('Scheduling a rejoin to %s at %s; ' 'Channel temporarily unavailable.', target, t) else: irc = self.pendingNickChanges.get(irc, None) if irc is not None: def nick(): irc.queueMsg(ircmsgs.nick(target)) schedule.addEvent(nick, t) self.log.info('Scheduling a nick change to %s at %s; ' 'Nick temporarily unavailable.', target, t) else: self.log.debug('Got 437 without Admin.nick being called.') def do471(self, irc, msg): try: channel = msg.args[1] (irc, msg) = self.joins.pop(channel) irc.error(_('Cannot join %s, it\'s full.') % channel) except KeyError: self.log.debug('Got 471 without Admin.join being called.') def do473(self, irc, msg): try: channel = msg.args[1] (irc, msg) = self.joins.pop(channel) irc.error(_('Cannot join %s, I was not invited.') % channel) except KeyError: self.log.debug('Got 473 without Admin.join being called.') def do474(self, irc, msg): try: channel = msg.args[1] (irc, msg) = self.joins.pop(channel) irc.error(_('Cannot join %s, I am banned.') % channel) except KeyError: self.log.debug('Got 474 without Admin.join being called.') def do475(self, irc, msg): try: channel = msg.args[1] (irc, msg) = self.joins.pop(channel) irc.error(_('Cannot join %s, my keyword was wrong.') % channel) except KeyError: self.log.debug('Got 475 without Admin.join being called.') def do477(self, irc, msg): try: channel = msg.args[1] (irc,msg) = self.joins.pop(channel) irc.error(_('Cannot join %s, I\'m not identified with ' 'NickServ.') % channel) except KeyError: self.log.debug('Got 477 without Admin.join being called.') def do515(self, irc, msg): try: channel = msg.args[1] (irc, msg) = self.joins.pop(channel) irc.error(_('Cannot join %s, I\'m not identified with ' 'NickServ.') % channel) except KeyError: self.log.debug('Got 515 without Admin.join being called.') def doJoin(self, irc, msg): if msg.prefix == irc.prefix: try: del self.joins[msg.args[0]] except KeyError: s = 'Joined a channel without Admin.join being called.' self.log.debug(s) def doInvite(self, irc, msg): channel = msg.args[1] if channel not in irc.state.channels: if conf.supybot.alwaysJoinOnInvite.get(channel)() or \ ircdb.checkCapability(msg.prefix, 'admin'): self.log.info('Invited to %s by %s.', channel, msg.prefix) networkGroup = conf.supybot.networks.get(irc.network) irc.queueMsg(networkGroup.channels.join(channel)) conf.supybot.networks.get(irc.network).channels().add(channel) else: self.log.warning('Invited to %s by %s, but ' 'supybot.alwaysJoinOnInvite was False and ' 'the user lacked the "admin" capability.', channel, msg.prefix) @internationalizeDocstring def join(self, irc, msg, args, channel, key): """ [] Tell the bot to join the given channel. If is given, it is used when attempting to join the channel. """ if not irc.isChannel(channel): irc.errorInvalid(_('channel'), channel, Raise=True) networkGroup = conf.supybot.networks.get(irc.network) networkGroup.channels().add(channel) if key: networkGroup.channels.key.get(channel).setValue(key) maxchannels = irc.state.supported.get('maxchannels', sys.maxsize) if len(irc.state.channels) + 1 > maxchannels: irc.error(_('I\'m already too close to maximum number of ' 'channels for this network.'), Raise=True) irc.queueMsg(networkGroup.channels.join(channel)) irc.noReply() self.joins[channel] = (irc, msg) join = wrap(join, ['validChannel', additional('something')]) @internationalizeDocstring def channels(self, irc, msg, args): """takes no arguments Returns the channels the bot is on. """ L = irc.state.channels.keys() if L: utils.sortBy(ircutils.toLower, L) irc.reply(format('%L', L), private=True) else: irc.reply(_('I\'m not currently in any channels.')) channels = wrap(channels) def do484(self, irc, msg): irc = self.pendingNickChanges.get(irc, None) if irc is not None: irc.error(_('My connection is restricted, I can\'t change nicks.')) else: self.log.debug('Got 484 without Admin.nick being called.') def do433(self, irc, msg): irc = self.pendingNickChanges.get(irc, None) if irc is not None: irc.error(_('Someone else is already using that nick.')) else: self.log.debug('Got 433 without Admin.nick being called.') def do435(self, irc, msg): irc = self.pendingNickChanges.get(irc, None) if irc is not None: irc.error(_('I can\'t change nick, I\'m currently banned in %s.') % msg.args[2]) else: self.log.debug('Got 435 without Admin.nick being called.') def do438(self, irc, msg): irc = self.pendingNickChanges.get(irc, None) if irc is not None: irc.error(format(_('I can\'t change nicks, the server said %q.'), msg.args[2]), private=True) else: self.log.debug('Got 438 without Admin.nick being called.') def doNick(self, irc, msg): if msg.nick == irc.nick or msg.args[0] == irc.nick: try: del self.pendingNickChanges[irc] except KeyError: self.log.debug('Got NICK without Admin.nick being called.') @internationalizeDocstring def nick(self, irc, msg, args, nick, network): """[] [] Changes the bot's nick to . If no nick is given, returns the bot's current nick. """ network = network or irc.network if nick: group = getattr(conf.supybot.networks, network) group.nick.setValue(nick) irc.queueMsg(ircmsgs.nick(nick)) self.pendingNickChanges[irc.getRealIrc()] = irc else: irc.reply(irc.nick) nick = wrap(nick, [additional('nick'), additional('something')]) class capability(callbacks.Commands): @internationalizeDocstring def add(self, irc, msg, args, user, capability): """ Gives the user specified by (or the user to whom currently maps) the specified capability """ # Ok, the concepts that are important with capabilities: # ### 1) No user should be able to elevate their privilege to owner. ### 2) Admin users are *not* superior to #channel.ops, and don't ### have God-like powers over channels. ### 3) We assume that Admin users are two things: non-malicious and ### and greedy for power. So they'll try to elevate their ### privilege to owner, but they won't try to crash the bot for ### no reason. # Thus, the owner capability can't be given in the bot. Admin # users can only give out capabilities they have themselves (which # will depend on supybot.capabilities and its child default) but # generally means they can't mess with channel capabilities. if ircutils.strEqual(capability, 'owner'): irc.error(_('The "owner" capability can\'t be added in the ' 'bot. Use the supybot-adduser program (or edit the ' 'users.conf file yourself) to add an owner ' 'capability.')) return if ircdb.isAntiCapability(capability) or \ ircdb.checkCapability(msg.prefix, capability): user.addCapability(capability) ircdb.users.setUser(user) irc.replySuccess() else: irc.error(_('You can\'t add capabilities you don\'t have.')) add = wrap(add, ['otherUser', 'lowered']) @internationalizeDocstring def remove(self, irc, msg, args, user, capability): """ Takes from the user specified by (or the user to whom currently maps) the specified capability """ if ircdb.checkCapability(msg.prefix, capability) or \ ircdb.isAntiCapability(capability): try: user.removeCapability(capability) ircdb.users.setUser(user) irc.replySuccess() except KeyError: irc.error(_('That user doesn\'t have that capability.')) else: s = _('You can\'t remove capabilities you don\'t have.') irc.error(s) remove = wrap(remove, ['otherUser','lowered']) class ignore(callbacks.Commands): @internationalizeDocstring def add(self, irc, msg, args, hostmask, expires): """ [] This will set a persistent ignore on or the hostmask currently associated with . is an optional argument specifying when (in "seconds from now") the ignore will expire; if it isn't given, the ignore will never automatically expire. """ ircdb.ignores.add(hostmask, expires) irc.replySuccess() add = wrap(add, ['hostmask', additional('expiry', 0)]) @internationalizeDocstring def remove(self, irc, msg, args, hostmask): """ This will remove the persistent ignore on or the hostmask currently associated with . """ try: ircdb.ignores.remove(hostmask) irc.replySuccess() except KeyError: irc.error(_('%s wasn\'t in the ignores database.') % hostmask) remove = wrap(remove, ['hostmask']) @internationalizeDocstring def list(self, irc, msg, args): """takes no arguments Lists the hostmasks that the bot is ignoring. """ # XXX Add the expirations. if ircdb.ignores.hostmasks: irc.reply(format('%L', (list(map(repr,ircdb.ignores.hostmasks))))) else: irc.reply(_('I\'m not currently globally ignoring anyone.')) list = wrap(list) def clearq(self, irc, msg, args): """takes no arguments Clears the current send queue for this network. """ irc.queue.reset() irc.replySuccess() clearq = wrap(clearq) def acmd(self, irc, msg, args, commandAndArgs): """ [ ...] Perform (with associated s on all channels on current network.""" for channel in irc.state.channels: msg = ircmsgs.IrcMsg(msg=msg, args=(channel,) + msg.args[1:]) self.Proxy(irc.getRealIrc(), msg, commandAndArgs) acmd = wrap(acmd, ['admin', many('something')]) Class = Admin # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Admin/test.py0000644000175000017500000001341114535072470016162 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class AdminTestCase(PluginTestCase): plugins = ('Admin', 'Utilities') def testChannels(self): def getAfterJoinMessages(): m = self.irc.takeMsg() self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.assertEqual(m.command, 'WHO') self.assertRegexp('channels', 'not.*in any') self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix)) getAfterJoinMessages() self.assertRegexp('channels', '#foo') self.irc.feedMsg(ircmsgs.join('#bar', prefix=self.prefix)) getAfterJoinMessages() self.assertRegexp('channels', '#bar and #foo') self.irc.feedMsg(ircmsgs.join('#Baz', prefix=self.prefix)) getAfterJoinMessages() self.assertRegexp('channels', '#bar, #Baz, and #foo') def testIgnoreAddRemove(self): self.assertNotError('admin ignore add foo!bar@baz') self.assertError('admin ignore add alsdkfjlasd') self.assertNotError('admin ignore remove foo!bar@baz') self.assertError('admin ignore remove foo!bar@baz') def testIgnoreList(self): self.assertNotError('admin ignore list') self.assertNotError('admin ignore add foo!bar@baz') self.assertNotError('admin ignore list') self.assertNotError('admin ignore add foo!bar@baz') self.assertRegexp('admin ignore list', 'foo') def testCapabilityAdd(self): self.assertError('capability add foo bar') u = ircdb.users.newUser() u.name = 'foo' ircdb.users.setUser(u) self.assertNotError('capability add foo bar') self.assertError('addcapability foo baz') self.assertIn('bar', u.capabilities) ircdb.users.delUser(u.id) def testCapabilityRemove(self): self.assertError('capability remove foo bar') u = ircdb.users.newUser() u.name = 'foo' ircdb.users.setUser(u) self.assertNotError('capability add foo bar') self.assertIn('bar', u.capabilities) self.assertError('removecapability foo bar') self.assertNotError('capability remove foo bar') self.assertNotIn('bar', u.capabilities) ircdb.users.delUser(u.id) def testJoin(self): m = self.getMsg('join #foo') self.assertEqual(m.command, 'JOIN') self.assertEqual(m.args[0], '#foo') m = self.getMsg('join #foo key') self.assertEqual(m.command, 'JOIN') self.assertEqual(m.args[0], '#foo') self.assertEqual(m.args[1], 'key') def testNick(self): try: m = self.getMsg('nick foobar') self.assertEqual(m.command, 'NICK') self.assertEqual(m.args[0], 'foobar') finally: conf.supybot.networks.test.nick.setValue('') def testAddCapabilityOwner(self): self.assertError('admin capability add %s owner' % self.nick) def testJoinOnOwnerInvite(self): self.irc.feedMsg(ircmsgs.invite(conf.supybot.nick(), '#foo', prefix=self.prefix)) m = self.getMsg(' ') self.assertEqual(m.command, 'JOIN') self.assertEqual(m.args[0], '#foo') def testNoJoinOnUnprivilegedInvite(self): try: world.testing = False for channel in '#foo', '#foo\u0009': self.irc.feedMsg(ircmsgs.invite(conf.supybot.nick(), channel, prefix='foo!bar@baz')) self.assertResponse('somecommand', 'Error: "somecommand" is not a valid command.') finally: world.testing = True def testAcmd(self): self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix)) self.irc.feedMsg(ircmsgs.join('#bar', prefix=self.prefix)) while self.irc.takeMsg(): pass msgs = [] msg = self.getMsg('acmd echo hi $channel') while msg: msgs.append(msg) msg = self.irc.takeMsg() self.assertCountEqual( [(msg.command,) + msg.args for msg in msgs], [("PRIVMSG", "#foo", "hi #foo"), ("PRIVMSG", "#bar", "hi #bar")]) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Aka/0000755000175000017500000000000014535072535014277 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Aka/__init__.py0000644000175000017500000000534414535072470016414 0ustar00valval### # Copyright (c) 2013-2021, 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. ### """ This plugin allows the user to create various aliases (known as "Akas", since Alias is the name of another plugin Aka is based on) to other commands or combinations of other commands (via nested commands). """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.progval __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} # This is a url where the most recent plugin package can be downloaded. __url__ = '' # 'http://supybot.com/Members/yourname/Aka/download' from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Aka/config.py0000644000175000017500000000564314535072470016124 0ustar00valval### # Copyright (c) 2013-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('Aka') except: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x:x def configure(advanced): # This will be called by supybot to configure this module. advanced is # a bool that specifies whether the user identified themself as an advanced # user or not. You should effect your configuration by manipulating the # registry as appropriate. from supybot.questions import expect, anything, something, yn conf.registerPlugin('Aka', True) Aka = conf.registerPlugin('Aka') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Aka, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerGlobalValue(Aka, 'maximumWordsInName', registry.Integer(5, _("""The maximum number of words allowed in a command name. Setting this to an high value may slow down your bot on long commands."""))) conf.registerGroup(Aka, 'web') conf.registerGlobalValue(Aka.web, 'enable', registry.Boolean(False, _("""Determines whether the Akas will be browsable through the HTTP server."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Aka/locales/0000755000175000017500000000000014535072535015721 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Aka/locales/fi.po0000644000175000017500000002541614535072470016665 0ustar00valval# Aka plugin for Limnoria # Copyright (C) 2014 Limnoria # Mikaela Suomalainen , 2014. # msgid "" msgstr "" "Project-Id-Version: Aka plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:57+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" #: config.py:55 msgid "" "The maximum number of words allowed in a\n" " command name. Setting this to an high value may slow down your bot\n" " on long commands." msgstr "" "Komennon nimessä sallittujen merkkien enimmäismäärä.\n" " Korkean arvon asettaminen tähän voi hidastaa bottiasi pitkien komentojen " "kanssa." #: config.py:61 msgid "" "Determines whether the Akas will be\n" " browsable through the HTTP server." msgstr "" #: plugin.py:141 plugin.py:274 plugin.py:732 msgid "This Aka already exists." msgstr "Tämä Aka on jo olemassa." #: plugin.py:170 plugin.py:182 plugin.py:196 plugin.py:301 plugin.py:318 #: plugin.py:335 plugin.py:912 msgid "This Aka does not exist." msgstr "Tätä Akaa ei ole olemassa." #: plugin.py:303 msgid "This Aka is already locked." msgstr "Tämä Aka on jo lukittu." #: plugin.py:320 msgid "This Aka is already unlocked." msgstr "Tämä Aka on jo avattu." #: plugin.py:465 #, fuzzy msgid "By %s at %s" msgstr "Lukinnut %s aikaan %s" #: plugin.py:501 msgid "" "\n" " This plugin allows users to define aliases to commands and combinations\n" " of commands (via nesting).\n" "\n" " Importing from Alias\n" " ^^^^^^^^^^^^^^^^^^^^\n" "\n" " Add an aka, Alias, which eases the transitioning to Aka from Alias.\n" "\n" " First we will load Alias and Aka::\n" "\n" " @load Alias\n" " jamessan: The operation succeeded.\n" " @load Aka\n" " jamessan: The operation succeeded.\n" "\n" " Then we import the Alias database to Aka in case it exists and unload\n" " Alias::\n" "\n" " @importaliasdatabase\n" " jamessan: The operation succeeded.\n" " @unload Alias\n" " jamessan: The operation succeeded.\n" "\n" " And now we will finally add the Aka ``alias`` itself::\n" "\n" " @aka add \"alias\" \"aka $1 $*\"\n" " jamessan: The operation succeeded.\n" "\n" " Now you can use Aka as you used Alias before.\n" "\n" " Trout\n" " ^^^^^\n" "\n" " Add an aka, trout, which expects a word as an argument::\n" "\n" " @aka add trout \"reply action slaps $1 with a large trout" "\"\n" " jamessan: The operation succeeded.\n" " @trout me\n" " * bot slaps me with a large trout\n" "\n" " This ``trout`` aka requires the plugin ``Reply`` to be loaded since it\n" " provides the ``action`` command.\n" "\n" " LastFM\n" " ^^^^^^\n" "\n" " Add an aka, ``lastfm``, which expects a last.fm username and replies " "with\n" " their most recently played item::\n" "\n" " @aka add lastfm \"rss [format concat http://ws.audioscrobbler." "com/1.0/user/ [format concat [web urlquote $1] /recenttracks.rss]]\"\n" "\n" " This ``lastfm`` aka requires the following plugins to be loaded: " "``RSS``,\n" " ``Format`` and ``Web``.\n" "\n" " ``RSS`` provides ``rss``, ``Format`` provides ``concat`` and ``Web`` " "provides\n" " ``urlquote``.\n" "\n" " Note that if the nested commands being aliased hadn't been quoted, then\n" " those commands would have been run immediately, and ``@lastfm`` would " "always\n" " reply with the same information, the result of those commands.\n" " " msgstr "" #: plugin.py:699 msgid "You've attempted more nesting than is currently allowed on this bot." msgstr "" "Olet yrittänyt sisällyttää enemmän komentoja, kuin tässä botti sallii juuri " "nyt." #: plugin.py:703 msgid " at least" msgstr "ainakin" #: plugin.py:712 msgid "Locked by %s at %s" msgstr "Lukinnut %s aikaan %s" #: plugin.py:717 #, fuzzy msgid "" "\n" "\n" "Alias for %q.%s" msgstr "" "\n" "\n" "Alias komennolle %q.%s" #: plugin.py:718 plugin.py:722 msgid "argument" msgstr "parametri" #: plugin.py:721 #, fuzzy msgid "" "\n" "\n" "Alias for %q.%s" msgstr "" "\n" "\n" "Alias komennolle %q.%s" #: plugin.py:729 msgid "You can't overwrite commands in this plugin." msgstr "Et voi ylikirjoittaa tämän lisä-osan komentoja." #: plugin.py:734 msgid "This Aka has too many spaces in its name." msgstr "Tämän Akan nimessä on liian monta välilyöntiä." #: plugin.py:739 msgid "Can't mix $* and optional args (@1, etc.)" msgstr "" "$*:ä ja vapaaehtoisia parametrejä (@1, jne.) ei voida sekoittaa keskenään" #: plugin.py:746 msgid "This Aka is locked." msgstr "Tämä Aka on lukittu." #: plugin.py:750 #, fuzzy msgid "" "[--channel <#channel>] \n" "\n" " Defines an alias that executes . The \n" " should be in the standard \"command argument [nestedcommand " "argument]\"\n" " arguments to the alias; they'll be filled with the first, second, " "etc.\n" " arguments. $1, $2, etc. can be used for required arguments. @1, " "@2,\n" " etc. can be used for optional arguments. $* simply means \"all\n" " arguments that have not replaced $1, $2, etc.\", ie. it will also\n" " include optional arguments.\n" " " msgstr "" "[--channel <#kanava>] \n" "\n" "Määrittää aliaksen , joka suorittaa . \n" " pitäisi olla tavallisessa muodossa \"komento parametri [sisällytettykomento " "parametri]\"\n" " parametreinä aliakselle; ne täytetään ensimmäisenä, toisena, jne.\n" " parametreinä. $1, $2, jne. voidaan käyttää vaadittuina parametreinä. @1, " "@2,\n" " jne. voidaan käyttää vapaaehtoisina parametreinä. $* tarkoittaa " "yksinkertaisesti \"kaikki\n" " jotka eivät ole korvanneet $1, $2, jne.\", esim. se sisältää vapaa-ehtoiset " "parametrit.\n" " " #: plugin.py:764 plugin.py:796 plugin.py:827 plugin.py:859 plugin.py:882 #: plugin.py:905 plugin.py:951 plugin.py:994 msgid "%r is not a valid channel." msgstr "%r ei ole kelvollinen kanava." #: plugin.py:782 #, fuzzy msgid "" "[--channel <#channel>] \n" "\n" " Overwrites an existing alias to execute instead. " "The\n" " should be in the standard \"command argument " "[nestedcommand\n" " argument]\" arguments to the alias; they'll be filled with the " "first,\n" " second, etc. arguments. $1, $2, etc. can be used for required\n" " arguments. @1, @2, etc. can be used for optional arguments. $* " "simply\n" " means \"all arguments that have not replaced $1, $2, etc.\", ie. it " "will\n" " also include optional arguments.\n" " " msgstr "" "[--kanava <#kanava>] \n" " Ylikirjoittaa olemassa olevan aliaksen suorittamaan " "sensijaan. \n" " pitäisi olla standardissa \"komento parametri [sisäkkäinen komento\" " "parametreinä aliakselle; ne täytetään\n" " ensimmäisillä, toisilla jne. parametreillä. $1, $2, jne. voidaan käyttää " "vaadittuihin parametreihin. $*\n" " yksinkertaisesti tarkoittaa \"kaikki parametrin, joita ei ole korvattu $1, " "$2 jne.\", esimerkiksi. se sisällyttää\n" " myös kaikki vapaaehtoiset parametrit." #: plugin.py:819 msgid "" "[--channel <#channel>] \n" "\n" " Removes the given alias, if unlocked.\n" " " msgstr "" "[--channel <#kanava>] \n" "\n" " Poistaa annetun aliaksen, ellei se ole lukittu.\n" " " #: plugin.py:841 msgid "" "Check if the user has any of the required capabilities to manage\n" " the regexp database." msgstr "" "Tarkistaa onko käyttäjällä vaadittu valtuus säännöllisten lausekkeiden\n" " tietokannan hallintaan." #: plugin.py:851 msgid "" "[--channel <#channel>] \n" "\n" " Locks an alias so that no one else can change it.\n" " " msgstr "" "[--channel <#kanava>] \n" "\n" " Lukitsee aliaksen estäen muita muokkaamasta sitä.\n" " " #: plugin.py:874 msgid "" "[--channel <#channel>] \n" "\n" " Unlocks an alias so that people can define new aliases over it.\n" " " msgstr "" "[--channel <#kanava>] \n" "\n" " Avaa aliaksen, jotta kaikki voivat määrittää uusia aliaksia sen päälle.\n" " " #: plugin.py:897 msgid "" "[--channel <#channel>] \n" "\n" " This command shows the content of an Aka.\n" " " msgstr "" " <#kanava> \n" "\n" " Tämä komento näyttää Akan sisällön." #: plugin.py:917 msgid "" "takes no arguments\n" "\n" " Imports the Alias database into Aka's, and clean the former." msgstr "" "ei ota parametrejä\n" "\n" " Tuo Aliaksen tietokannan Akaan ja tyhjentää aiemman." #: plugin.py:922 msgid "Alias plugin is not loaded." msgstr "Alias lisä-osa ei ole ladattu." #: plugin.py:933 msgid "Error occured when importing the %n: %L" msgstr "Virhe komennon %n tuomisessa: %L" #: plugin.py:941 msgid "" "[--channel <#channel>] [--keys] [--unlocked|--locked]\n" "\n" " Lists all Akas defined for . If is not " "specified,\n" " lists all global Akas. If --keys is given, lists only the Aka names\n" " and not their commands." msgstr "" #: plugin.py:960 msgid "--locked and --unlocked are incompatible options." msgstr "" #: plugin.py:980 msgid "No Akas found." msgstr "" #: plugin.py:985 msgid "" "[--channel <#channel>] \n" "\n" " Searches Akas defined for . If is not specified,\n" " searches all global Akas." msgstr "" #: plugin.py:1004 msgid "No matching Akas were found." msgstr "" #~ msgid "" #~ "Aka is the improved version of the Alias plugin. It stores akas outside\n" #~ " of the bot.conf, which doesn't have risk of corrupting the bot.conf " #~ "file\n" #~ " (this often happens when there are Unicode issues). Aka also\n" #~ " introduces multi-worded akas." #~ msgstr "" #~ "Aka on paranneltu versio Alias pluginista. Se tallentaa akat bot.conf-" #~ "tiedoston ulkopuolelle, jollla ei ole\n" #~ " riskiä korruptoida bot.conf tiedostoa (joka tapahtuu usein Unicode-" #~ "ongelmien kanssa). Aka\n" #~ " tukee myös useamman akan pituisia akoja." #~ msgid "There can be only one $* in an alias." #~ msgstr "Aliaksessa voi olla vain yksi $*." #~ msgid "" #~ "Add the help for 'plugin help Aka' here\n" #~ " This should describe *how* to use this plugin." #~ msgstr "" #~ "Lisää ohje komentoa 'plugin help Aka' varten tähän.\n" #~ " Tämän pitäisi kuvata *kuinka* tätä lisä-osaa käytetään." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Aka/plugin.py0000644000175000017500000011524214535072470016152 0ustar00valval### # Copyright (c) 2013-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import os import sys import datetime import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb from supybot.commands import * import supybot.plugins as plugins import supybot.utils.minisix as minisix import supybot.ircutils as ircutils import supybot.callbacks as callbacks import supybot.httpserver as httpserver from supybot.i18n import PluginInternationalization _ = PluginInternationalization('Aka') try: import sqlite3 except ImportError: sqlite3 = None try: import sqlalchemy import sqlalchemy.ext import sqlalchemy.ext.declarative except ImportError: sqlalchemy = None if not (sqlite3 or sqlalchemy): raise callbacks.Error('You have to install python-sqlite3 or ' 'python-sqlalchemy in order to load this plugin.') available_db = {} class Alias(object): __slots__ = ('name', 'alias', 'locked', 'locked_by', 'locked_at') def __init__(self, name, alias): self.name = name self.alias = alias self.locked = False self.locked_by = None self.locked_at = None def __repr__(self): return "" % (self.name, self.alias) if sqlite3: class SQLiteAkaDB(object): __slots__ = ('engines', 'filename', 'dbs',) def __init__(self, filename): self.engines = ircutils.IrcDict() self.filename = filename.replace('sqlite3', 'sqlalchemy') def close(self): self.dbs.clear() def get_db(self, channel): if channel in self.engines: engine = self.engines[channel] else: filename = plugins.makeChannelFilename(self.filename, channel) exists = os.path.exists(filename) engine = sqlite3.connect(filename, check_same_thread=False) if not exists: cursor = engine.cursor() cursor.execute("""CREATE TABLE aliases ( id INTEGER NOT NULL, name VARCHAR NOT NULL, alias VARCHAR NOT NULL, locked BOOLEAN NOT NULL, locked_by VARCHAR, locked_at DATETIME, PRIMARY KEY (id), UNIQUE (name))""") engine.commit() self.engines[channel] = engine assert engine.execute("select 1").fetchone() == (1,) return engine def has_aka(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) return self.get_db(channel).cursor() \ .execute("""SELECT COUNT() as count FROM aliases WHERE name = ?;""", (name,)) \ .fetchone()[0] def get_aka_list(self, channel): cursor = self.get_db(channel).cursor() cursor.execute("""SELECT name FROM aliases;""") list_ = cursor.fetchall() return list_ def get_alias(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') cursor = self.get_db(channel).cursor() cursor.execute("""SELECT alias FROM aliases WHERE name = ?;""", (name,)) r = cursor.fetchone() if r: return r[0] else: return None def add_aka(self, channel, name, alias): name = callbacks.canonicalName(name, preserve_spaces=True) if self.has_aka(channel, name): raise AkaError(_('This Aka already exists.')) if minisix.PY2: if isinstance(name, str): name = name.decode('utf8') if isinstance(alias, str): alias = alias.decode('utf8') db = self.get_db(channel) cursor = db.cursor() cursor.execute("""INSERT INTO aliases VALUES ( NULL, ?, ?, 0, NULL, NULL);""", (name, alias)) db.commit() def remove_aka(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) db.cursor().execute('DELETE FROM aliases WHERE name = ?', (name,)) db.commit() def lock_aka(self, channel, name, by): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) cursor = db.cursor().execute("""UPDATE aliases SET locked=1, locked_at=?, locked_by=? WHERE name = ?""", (datetime.datetime.now(), by, name)) if cursor.rowcount == 0: raise AkaError(_('This Aka does not exist.')) db.commit() def unlock_aka(self, channel, name, by): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) cursor = db.cursor() cursor.execute("""UPDATE aliases SET locked=0, locked_at=? WHERE name = ?""", (datetime.datetime.now(), name)) if cursor.rowcount == 0: raise AkaError(_('This Aka does not exist.')) db.commit() def get_aka_lock(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') cursor = self.get_db(channel).cursor() cursor.execute("""SELECT locked, locked_by, locked_at FROM aliases WHERE name = ?;""", (name,)) r = cursor.fetchone() if r: return (bool(r[0]), r[1], r[2]) else: raise AkaError(_('This Aka does not exist.')) def get_all(self, channel): cursor = self.get_db(channel).cursor() cursor.execute(""" SELECT id, name, alias, locked, locked_by, locked_at FROM aliases; """) return cursor.fetchall() available_db.update({'sqlite3': SQLiteAkaDB}) elif sqlalchemy: Base = sqlalchemy.ext.declarative.declarative_base() class SQLAlchemyAlias(Alias, Base): __slots__ = () __tablename__ = 'aliases' id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True) name = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False) alias = sqlalchemy.Column(sqlalchemy.String, nullable=False) locked = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False) locked_by = sqlalchemy.Column(sqlalchemy.String, nullable=True) locked_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True) # TODO: Add table for usage statistics class SqlAlchemyAkaDB(object): __slots__ = ('engines', 'filename', 'sqlalchemy', 'dbs') def __init__(self, filename): self.engines = ircutils.IrcDict() self.filename = filename self.sqlalchemy = sqlalchemy def close(self): self.dbs.clear() def get_db(self, channel): if channel in self.engines: engine = self.engines[channel] else: filename = plugins.makeChannelFilename(self.filename, channel) exists = os.path.exists(filename) engine = sqlalchemy.create_engine('sqlite:///' + filename) if not exists: Base.metadata.create_all(engine) self.engines[channel] = engine assert engine.execute("select 1").scalar() == 1 Session = sqlalchemy.orm.sessionmaker() Session.configure(bind=engine) return Session() def has_aka(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') count = self.get_db(channel).query(SQLAlchemyAlias) \ .filter(SQLAlchemyAlias.name == name) \ .count() return bool(count) def get_aka_list(self, channel): list_ = list(self.get_db(channel).query(SQLAlchemyAlias.name)) return list_ def get_alias(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') try: return self.get_db(channel).query(SQLAlchemyAlias.alias) \ .filter(SQLAlchemyAlias.name == name).one()[0] except sqlalchemy.orm.exc.NoResultFound: return None def add_aka(self, channel, name, alias): name = callbacks.canonicalName(name, preserve_spaces=True) if self.has_aka(channel, name): raise AkaError(_('This Aka already exists.')) if minisix.PY2: if isinstance(name, str): name = name.decode('utf8') if isinstance(alias, str): alias = alias.decode('utf8') db = self.get_db(channel) db.add(SQLAlchemyAlias(name, alias)) db.commit() def remove_aka(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) db.query(SQLAlchemyAlias).filter(SQLAlchemyAlias.name == name).delete() db.commit() def lock_aka(self, channel, name, by): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) try: aka = db.query(SQLAlchemyAlias) \ .filter(SQLAlchemyAlias.name == name).one() except sqlalchemy.orm.exc.NoResultFound: raise AkaError(_('This Aka does not exist.')) if aka.locked: raise AkaError(_('This Aka is already locked.')) aka.locked = True aka.locked_by = by aka.locked_at = datetime.datetime.now() db.commit() def unlock_aka(self, channel, name, by): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') db = self.get_db(channel) try: aka = db.query(SQLAlchemyAlias) \ .filter(SQLAlchemyAlias.name == name).one() except sqlalchemy.orm.exc.NoResultFound: raise AkaError(_('This Aka does not exist.')) if not aka.locked: raise AkaError(_('This Aka is already unlocked.')) aka.locked = False aka.locked_by = by aka.locked_at = datetime.datetime.now() db.commit() def get_aka_lock(self, channel, name): name = callbacks.canonicalName(name, preserve_spaces=True) if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') try: return self.get_db(channel) \ .query(SQLAlchemyAlias.locked, SQLAlchemyAlias.locked_by, SQLAlchemyAlias.locked_at)\ .filter(SQLAlchemyAlias.name == name).one() except sqlalchemy.orm.exc.NoResultFound: raise AkaError(_('This Aka does not exist.')) def get_all(self, channel): akas = self.get_db(channel).query( SQLAlchemyAlias.id, SQLAlchemyAlias.name, SQLAlchemyAlias.alias, SQLAlchemyAlias.locked, SQLAlchemyAlias.locked_by, SQLAlchemyAlias.locked_at ).all() return map( lambda aka: (aka.name, aka.alias, aka.locked, aka.locked_by, aka.locked_at), akas ) available_db.update({'sqlalchemy': SqlAlchemyAkaDB}) def getArgs(args, required=1, optional=0, wildcard=0): if len(args) < required: raise callbacks.ArgumentError if len(args) < required + optional: ret = list(args) + ([''] * (required + optional - len(args))) elif len(args) >= required + optional: if not wildcard: ret = list(args[:required + optional - 1]) ret.append(' '.join(args[required + optional - 1:])) else: ret = list(args) return ret class AkaError(Exception): pass class RecursiveAlias(AkaError): pass dollarRe = re.compile(r'\$(\d+)') def findBiggestDollar(alias): dollars = dollarRe.findall(alias) dollars = list(map(int, dollars)) dollars.sort() if dollars: return dollars[-1] else: return 0 atRe = re.compile(r'@(\d+)') def findBiggestAt(alias): ats = atRe.findall(alias) ats = list(map(int, ats)) ats.sort() if ats: return ats[-1] else: return 0 if 'sqlite3' in conf.supybot.databases() and 'sqlite3' in available_db: AkaDB = SQLiteAkaDB elif 'sqlalchemy' in conf.supybot.databases() and 'sqlalchemy' in available_db: log.warning('Aka\'s only enabled database engine is SQLAlchemy, which ' 'is deprecated. Please consider adding \'sqlite3\' to ' 'supybot.databases (and/or install sqlite3).') AkaDB = SqlAlchemyAkaDB else: raise plugins.NoSuitableDatabase(['sqlite3', 'sqlalchemy']) class AkaHTTPCallback(httpserver.SupyHTTPServerCallback): name = 'Aka web interface' base_template = '''\ Aka

Aka

%s ''' index_template = base_template % '''\

To view the global Akas either click here or enter 'global' in the form below.

''' list_template = base_template % '''\ %s
Name Alias Locked
''' def doGet(self, handler, path, *args, **kwargs): if path == '/': self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() self.write(self.index_template) elif path.startswith('/list/'): parts = path.split('/') channel = parts[2] if len(parts) == 3 else 'global' channel = utils.web.urlunquote(channel) self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() akas = {} for aka in self._plugin._db.get_all('global'): akas[aka[1]] = aka if channel != 'global': for aka in self._plugin._db.get_all(channel): akas[aka[1]] = aka aka_rows = [] for (name, aka) in sorted(akas.items()): (id, name, alias, locked, locked_by, locked_at) = aka locked_column = 'False' if locked: locked_column = format( _('By %s at %s'), locked_by, locked_at.split('.')[0], ) aka_rows.append( format( """ %s %s %s """, utils.web.html_escape(name), utils.web.html_escape(alias), utils.web.html_escape(locked_column), ) ) self.write(format(self.list_template, ''.join(aka_rows))) def doPost(self, handler, path, form, *args, **kwargs): if path == '/' and 'channel' in form: self.send_response(303) self.send_header( 'Location', format('list/%s', utils.web.urlquote(form['channel'].value)) ) self.end_headers() else: self.send_response(400) self.send_header('Content-type', 'text/plain; charset=utf-8') self.end_headers() self.write('Missing field \'channel\'.') class Aka(callbacks.Plugin): """ This plugin allows users to define aliases to commands and combinations of commands (via nesting). Importing from Alias ^^^^^^^^^^^^^^^^^^^^ Add an aka, Alias, which eases the transitioning to Aka from Alias. First we will load Alias and Aka:: @load Alias jamessan: The operation succeeded. @load Aka jamessan: The operation succeeded. Then we import the Alias database to Aka in case it exists and unload Alias:: @importaliasdatabase jamessan: The operation succeeded. @unload Alias jamessan: The operation succeeded. And now we will finally add the Aka ``alias`` itself:: @aka add "alias" "aka $1 $*" jamessan: The operation succeeded. Now you can use Aka as you used Alias before. Trout ^^^^^ Add an aka, ``trout``, which expects a word as an argument:: @aka add trout "reply action slaps $1 with a large trout" jamessan: The operation succeeded. @trout me * bot slaps me with a large trout This ``trout`` aka requires the plugin ``Reply`` to be loaded since it provides the ``action`` command. Random percentage ^^^^^^^^^^^^^^^^^ Add an aka, ``randpercent``, which returns a random percentage value:: @aka add randpercent "squish [dice 1d100]%" This requires the ``Filter`` and ``Games`` plugins to be loaded. Note that nested commands in an alias should be quoted, or they will only run once when you create the alias, and not each time the alias is called. (In this case, not quoting the nested command would mean that ``@randpercent`` always responds with the same value!) """ def __init__(self, irc): self.__parent = super(Aka, self) self.__parent.__init__(irc) # "sqlalchemy" is only for backward compatibility filename = conf.supybot.directories.data.dirize('Aka.sqlalchemy.db') self._db = AkaDB(filename) self._http_running = False conf.supybot.plugins.Aka.web.enable.addCallback(self._httpConfCallback) if self.registryValue('web.enable'): self._startHttp() def die(self): if self._http_running: self._stopHttp() def _httpConfCallback(self): if self.registryValue('web.enable'): if not self._http_running: self._startHttp() else: if self._http_running: self._stopHttp() def _startHttp(self): callback = AkaHTTPCallback() callback._plugin = self httpserver.hook('aka', callback) self._http_running = True def _stopHttp(self): httpserver.unhook('aka') self._http_running = False def isCommandMethod(self, name): args = name.split(' ') if '|' in args: return False if len(args) > 1 and \ callbacks.canonicalName(args[0]) != self.canonicalName(): for cb in dynamic.irc.callbacks: # including this plugin if cb.isCommandMethod(' '.join(args[0:-1])): return False if minisix.PY2 and isinstance(name, str): name = name.decode('utf8') channel = dynamic.channel or 'global' return self._db.has_aka(channel, name) or \ self._db.has_aka('global', name) or \ self.__parent.isCommandMethod(name) isCommand = isCommandMethod def listCommands(self): commands = ['add', 'remove', 'lock', 'unlock', 'importaliasdatabase', 'show', 'list', 'set', 'search'] return commands def getCommand(self, args, check_other_plugins=True): canonicalName = callbacks.canonicalName # All the code from here to the 'for' loop is copied from callbacks.py assert args == list(map(canonicalName, args)) first = args[0] for cb in self.cbs: if first == cb.canonicalName(): return cb.getCommand(args[1:]) if first == self.canonicalName() and len(args) > 1: ret = self.getCommand(args[1:], False) if ret: return [first] + ret max_length = self.registryValue('maximumWordsInName') for i in range(1, min(len(args)+1, max_length)): if self.isCommandMethod(callbacks.formatCommand(args[0:i])): return args[0:i] return [] def getCommandMethod(self, command): if len(command) == 1 or command[0] == self.canonicalName(): try: return self.__parent.getCommandMethod(command) except AttributeError: pass name = callbacks.formatCommand(command) channel = dynamic.channel or 'global' original = self._db.get_alias(channel, name) if not original: original = self._db.get_alias('global', name) biggestDollar = findBiggestDollar(original) biggestAt = findBiggestAt(original) wildcard = '$*' in original def f(irc, msg, args): tokens = callbacks.tokenize(original) if biggestDollar or biggestAt: args = getArgs(args, required=biggestDollar, optional=biggestAt, wildcard=wildcard) remaining_len = conf.supybot.reply.maximumLength() for (i, arg) in enumerate(args): if remaining_len < len(arg): arg = arg[0:remaining_len] args[i+1:] = [] break remaining_len -= len(arg) def regexpReplace(m): idx = int(m.group(1)) return args[idx-1] def replace(tokens, replacer): for (i, token) in enumerate(tokens): if isinstance(token, list): replace(token, replacer) else: tokens[i] = replacer(token) replace(tokens, lambda s: dollarRe.sub(regexpReplace, s)) if biggestAt: assert not wildcard args = args[biggestDollar:] replace(tokens, lambda s: atRe.sub(regexpReplace, s)) if wildcard: assert not biggestAt # Gotta remove the things that have already been subbed in. i = biggestDollar args[:] = args[i:] def everythingReplace(tokens): skip = 0 for (i, token) in enumerate(tokens): if skip: skip -= 1 continue if isinstance(token, list): everythingReplace(token) if token == '$*': tokens[i:i+1] = args skip = len(args)-1 # do not make replacements in # tokens we just added elif '$*' in token: tokens[i] = token.replace('$*', ' '.join(args)) everythingReplace(tokens) maxNesting = conf.supybot.commands.nested.maximum() if maxNesting and irc.nested+1 > maxNesting: irc.error(_('You\'ve attempted more nesting than is ' 'currently allowed on this bot.'), Raise=True) self.Proxy(irc, msg, tokens, nested=irc.nested+1) if biggestDollar and (wildcard or biggestAt): flexargs = _(' at least') else: flexargs = '' try: lock = self._db.get_aka_lock(channel, name) except AkaError: lock = self._db.get_aka_lock('global', name) (locked, locked_by, locked_at) = lock if locked: lock = ' ' + _('Locked by %s at %s') % (locked_by, locked_at) else: lock = '' escaped_command = original.replace('\\', '\\\\').replace('"', '\\"') if channel == 'global': doc = format(_('\n\nAlias for %q.%s'), flexargs, (biggestDollar, _('argument')), escaped_command, lock) else: doc = format(_('\n\nAlias for %q.%s'), channel, flexargs, (biggestDollar, _('argument')), escaped_command, lock) f = utils.python.changeFunctionName(f, name, doc) return f def _add_aka(self, channel, name, alias): if self.__parent.isCommandMethod(name): raise AkaError(_('You can\'t overwrite commands in ' 'this plugin.')) if self._db.has_aka(channel, name): raise AkaError(_('This Aka already exists.')) if len(name.split(' ')) > self.registryValue('maximumWordsInName'): raise AkaError(_('This Aka has too many spaces in its name.')) biggestDollar = findBiggestDollar(alias) biggestAt = findBiggestAt(alias) wildcard = '$*' in alias if biggestAt and wildcard: raise AkaError(_('Can\'t mix $* and optional args (@1, etc.)')) self._db.add_aka(channel, name, alias) def _remove_aka(self, channel, name, evenIfLocked=False): if not evenIfLocked: (locked, by, at) = self._db.get_aka_lock(channel, name) if locked: raise AkaError(_('This Aka is locked.')) self._db.remove_aka(channel, name) def add(self, irc, msg, args, optlist, name, alias): """[--channel <#channel>] Defines an alias that executes . The should be in the standard "command argument [nestedcommand argument]" arguments to the alias; they'll be filled with the first, second, etc. arguments. $1, $2, etc. can be used for required arguments. @1, @2, etc. can be used for optional arguments. $* simply means "all arguments that have not replaced $1, $2, etc.", ie. it will also include optional arguments. """ channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg if ' ' not in alias: # If it's a single word, they probably want $*. alias += ' $*' try: self._add_aka(channel, name, alias) self.log.info('Adding Aka %r for %r (from %s)', name, alias, msg.prefix) irc.replySuccess() except AkaError as e: irc.error(str(e)) add = wrap(add, [getopts({ 'channel': 'somethingWithoutSpaces', }), 'something', 'text']) def set(self, irc, msg, args, optlist, name, alias): """[--channel <#channel>] Overwrites an existing alias to execute instead. The should be in the standard "command argument [nestedcommand argument]" arguments to the alias; they'll be filled with the first, second, etc. arguments. $1, $2, etc. can be used for required arguments. @1, @2, etc. can be used for optional arguments. $* simply means "all arguments that have not replaced $1, $2, etc.", ie. it will also include optional arguments. """ channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg try: self._remove_aka(channel, name) except AkaError as e: irc.error(str(e), Raise=True) if ' ' not in alias: # If it's a single word, they probably want $*. alias += ' $*' try: self._add_aka(channel, name, alias) self.log.info('Setting Aka %r to %r (from %s)', name, alias, msg.prefix) irc.replySuccess() except AkaError as e: irc.error(str(e)) set = wrap(set, [getopts({ 'channel': 'somethingWithoutSpaces', }), 'something', 'text']) def remove(self, irc, msg, args, optlist, name): """[--channel <#channel>] Removes the given alias, if unlocked. """ channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg try: self._remove_aka(channel, name) self.log.info('Removing Aka %r (from %s)', name, msg.prefix) irc.replySuccess() except AkaError as e: irc.error(str(e)) remove = wrap(remove, [getopts({ 'channel': 'somethingWithoutSpaces', }), 'something']) def _checkManageCapabilities(self, irc, msg, channel): """Check if the user has any of the required capabilities to manage the regexp database.""" if channel != 'global': capability = ircdb.makeChannelCapability(channel, 'op') else: capability = 'admin' if not ircdb.checkCapability(msg.prefix, capability): irc.errorNoCapability(capability, Raise=True) def lock(self, irc, msg, args, optlist, user, name): """[--channel <#channel>] Locks an alias so that no one else can change it. """ channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg self._checkManageCapabilities(irc, msg, channel) try: self._db.lock_aka(channel, name, user.name) except AkaError as e: irc.error(str(e)) else: irc.replySuccess() lock = wrap(lock, [getopts({ 'channel': 'somethingWithoutSpaces', }), 'user', 'something']) def unlock(self, irc, msg, args, optlist, user, name): """[--channel <#channel>] Unlocks an alias so that people can define new aliases over it. """ channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg self._checkManageCapabilities(irc, msg, channel) try: self._db.unlock_aka(channel, name, user.name) except AkaError as e: irc.error(str(e)) else: irc.replySuccess() unlock = wrap(unlock, [getopts({ 'channel': 'somethingWithoutSpaces', }), 'user', 'something']) def show(self, irc, msg, args, optlist, name): """[--channel <#channel>] This command shows the content of an Aka. """ channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg command = self._db.get_alias(channel, name) if command: irc.reply(command) else: irc.error(_('This Aka does not exist.')) show = wrap(show, [getopts({'channel': 'somethingWithoutSpaces'}), 'text']) def importaliasdatabase(self, irc, msg, args): """takes no arguments Imports the Alias database into Aka's, and clean the former.""" alias_plugin = irc.getCallback('Alias') if alias_plugin is None: irc.error(_('Alias plugin is not loaded.'), Raise=True) errors = {} for (name, (command, locked, func)) in \ list(alias_plugin.aliases.items()): try: self._add_aka('global', name, command) except AkaError as e: errors[name] = e.args[0] else: alias_plugin.removeAlias(name, evenIfLocked=True) if errors: irc.error(format(_('Error occured when importing the %n: %L'), (len(errors), 'following', 'command'), ['%s (%s)' % x for x in errors.items()])) else: irc.replySuccess() importaliasdatabase = wrap(importaliasdatabase, ['owner']) def list(self, irc, msg, args, optlist): """[--channel <#channel>] [--keys] [--unlocked|--locked] Lists all Akas defined for . If is not specified, lists all global Akas. If --keys is given, lists only the Aka names and not their commands.""" channel = 'global' filterlocked = filterunlocked = False for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg if option == 'locked': filterlocked = True if option == 'unlocked': filterunlocked = True aka_list = self._db.get_aka_list(channel) if filterlocked and filterunlocked: irc.error(_('--locked and --unlocked are incompatible options.'), Raise=True) elif filterlocked: aka_list = [aka for aka in aka_list if self._db.get_aka_lock(channel, aka)[0]] elif filterunlocked: aka_list = [aka for aka in aka_list if not self._db.get_aka_lock(channel, aka)[0]] if aka_list: if 'keys' in dict(optlist): # Strange, aka_list is a list of one length tuples s = [k[0] for k in aka_list] oneToOne = True else: aka_values = [self._db.get_alias(channel, aka) for aka in aka_list] s = ('{0}: "{1}"'.format(ircutils.bold(k), v) for (k, v) in zip(aka_list, aka_values)) oneToOne = None irc.replies(s, oneToOne=oneToOne) else: irc.error(_("No Akas found.")) list = wrap(list, [getopts({'channel': 'channel', 'keys': '', 'locked': '', 'unlocked': ''})]) def search(self, irc, msg, args, optlist, query): """[--channel <#channel>] Searches Akas defined for . If is not specified, searches all global Akas.""" query = callbacks.canonicalName(query, preserve_spaces=True) channel = 'global' for (option, arg) in optlist: if option == 'channel': if not irc.isChannel(arg): irc.error(_('%r is not a valid channel.') % arg, Raise=True) channel = arg aka_list = self._db.get_aka_list(channel) aka_list = [callbacks.canonicalName(k[0], preserve_spaces=True) for k in aka_list] matching = [aka for aka in aka_list if query in aka] if matching: irc.replies(matching) else: irc.error(_("No matching Akas were found.")) search = wrap(search, [getopts({'channel': 'channel'}), 'text']) Class = Aka # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Aka/test.py0000644000175000017500000003470114535072470015633 0ustar00valval# -*- coding: utf8 -*- ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2013-2021, 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 * import supybot.conf as conf import supybot.httpserver as httpserver import supybot.plugin as plugin import supybot.registry as registry from supybot.utils.minisix import u from . import plugin as Aka class FunctionsTest(SupyTestCase): def testFindBiggestDollar(self): self.assertEqual(Aka.findBiggestDollar(''), 0) self.assertEqual(Aka.findBiggestDollar('foo'), 0) self.assertEqual(Aka.findBiggestDollar('$0'), 0) self.assertEqual(Aka.findBiggestDollar('$1'), 1) self.assertEqual(Aka.findBiggestDollar('$2'), 2) self.assertEqual(Aka.findBiggestDollar('$2 $10'), 10) self.assertEqual(Aka.findBiggestDollar('$3'), 3) self.assertEqual(Aka.findBiggestDollar('$3 $2 $1'), 3) self.assertEqual(Aka.findBiggestDollar('foo bar $1'), 1) self.assertEqual(Aka.findBiggestDollar('foo $2 $1'), 2) self.assertEqual(Aka.findBiggestDollar('foo $0 $1'), 1) self.assertEqual(Aka.findBiggestDollar('foo $1 $3'), 3) self.assertEqual(Aka.findBiggestDollar('$10 bar $1'), 10) class AkaChannelTestCase(ChannelPluginTestCase): plugins = ('Aka', 'Conditional', 'Filter', 'Math', 'Utilities', 'Format', 'Reply', 'String') def testHistsearch(self): self.assertNotError( r'aka add histsearch "last --from [cif true ' r'\"echo test\" \"echo test\"] ' r'--regexp [concat \"m/$1/\" [re s/g// \"@2\"]]"') self.assertResponse('echo foo', 'foo') self.assertResponse('histsearch .*foo.*', '@echo foo') def testDoesNotOverwriteCommands(self): # We don't have dispatcher commands anymore #self.assertError('aka add aka "echo foo bar baz"') self.assertError('aka add add "echo foo bar baz"') self.assertError('aka add remove "echo foo bar baz"') self.assertError('aka add lock "echo foo bar baz"') self.assertError('aka add unlock "echo foo bar baz"') def testAkaHelp(self): self.assertNotError(r'aka add slashdot "foo \"bar\" baz"') self.assertRegexp('help slashdot', r'Alias for "foo \\"bar\\" baz".') self.assertNotError('aka add nonascii echo éé') self.assertRegexp('help nonascii', r'Alias for "echo éé".') self.assertNotError('aka remove slashdot') self.assertNotError('aka add --channel %s slashdot foo' % self.channel) self.assertRegexp('help aka slashdot', "an alias on %s.*Alias for .*foo" % self.channel) self.assertNotError('aka remove --channel %s slashdot' % self.channel) def testShow(self): self.assertNotError('aka add foo bar') self.assertResponse('show foo', 'bar $*') self.assertNotError('aka add "foo bar" baz') self.assertResponse('show "foo bar"', 'baz $*') def testRemove(self): self.assertNotError('aka add foo echo bar') self.assertResponse('foo', 'bar') self.assertNotError('aka remove foo') self.assertError('foo') def testDollars(self): self.assertNotError('aka add rot26 "rot13 [rot13 $1]"') self.assertResponse('rot26 foobar', 'foobar') def testMoreDollars(self): self.assertNotError('aka add rev "echo $3 $2 $1"') self.assertResponse('rev foo bar baz', 'baz bar foo') def testAllArgs(self): self.assertNotError('aka add swap "echo $2 $1 $*"') self.assertResponse('swap 1 2 3 4 5', '2 1 3 4 5') self.assertError('aka add foo "echo $1 @1 $*"') self.assertNotError('aka add moo echo $1 $*') self.assertError('moo') self.assertResponse('moo foo', 'foo') self.assertResponse('moo foo bar', 'foo bar') self.assertNotError('aka add spam "echo [echo $*]"') self.assertResponse('spam egg', 'egg') self.assertResponse('spam egg bacon', 'egg bacon') self.assertNotError('aka add doublespam "echo [echo $* $*]"') self.assertResponse('doublespam egg', 'egg egg') self.assertResponse('doublespam egg bacon', 'egg bacon egg bacon') def testExpansionBomb(self): self.assertNotError('aka add bomb "bomb $* $* $* $* $*"') # if the mitigation doesn't work, this test will eat all memory on the # system. self.assertResponse('bomb foo', "Error: You've attempted more nesting " "than is currently allowed on this bot.") def testChannel(self): self.assertNotError('aka add channel echo $channel') self.assertResponse('aka channel', self.channel) def testAddRemoveAka(self): cb = self.irc.getCallback('Aka') cb._add_aka('global', 'foobar', 'echo sbbone') cb._db.lock_aka('global', 'foobar', 'evil_admin') self.assertResponse('foobar', 'sbbone') self.assertRegexp('aka list', 'foobar') self.assertRaises(Aka.AkaError, cb._remove_aka, 'global', 'foobar') cb._remove_aka('global', 'foobar', evenIfLocked=True) self.assertNotRegexp('aka list', 'foobar') self.assertError('foobar') def testOptionalArgs(self): self.assertNotError('aka add myrepr "repr @1"') self.assertResponse('myrepr foo', '"foo"') self.assertResponse('myrepr ""', '""') def testRequiredAndOptional(self): self.assertNotError('aka add reqopt "echo req=$1, opt=@1"') self.assertResponse('reqopt foo bar', 'req=foo, opt=bar') self.assertResponse('reqopt foo', 'req=foo, opt=') def testNoExtraSpaces(self): self.assertNotError('aka add foo "action takes $1\'s money"') self.assertResponse('foo bar', '\x01ACTION takes bar\'s money\x01') def testNoExtraQuotes(self): self.assertNotError('aka add myre "echo s/$1/$2/g"') self.assertResponse('myre foo bar', 's/foo/bar/g') def testSimpleAkaWithoutArgsImpliesDollarStar(self): self.assertNotError('aka add exo echo') self.assertResponse('exo foo bar baz', 'foo bar baz') def testChannelPriority(self): self.assertNotError('aka add spam "echo foo"') self.assertNotError('aka add --channel %s spam "echo bar"' % self.channel) self.assertResponse('spam', 'bar') self.assertNotError('aka add --channel %s egg "echo baz"' % self.channel) self.assertNotError('aka add egg "echo qux"') self.assertResponse('egg', 'baz') def testComplicatedNames(self): self.assertNotError(u('aka add café "echo coffee"')) self.assertResponse(u('café'), 'coffee') self.assertNotError('aka add "foo bar" "echo spam"') self.assertResponse('foo bar', 'spam') self.assertNotError('aka add "foo" "echo egg"') self.assertResponse('foo', 'egg') # You could expect 'spam' here, but in fact, this is dangerous. # Just imagine this session: # aka add "echo foo" quit # The operation succeeded. # ... # echo foo # * bot has quit self.assertResponse('foo bar', 'egg') def testNoOverride(self): self.assertNotError('aka add "echo foo" "echo bar"') self.assertResponse('echo foo', 'foo') self.assertNotError('aka add foo "echo baz"') self.assertNotError('aka add "foo bar" "echo qux"') self.assertResponse('foo bar', 'baz') def testRecursivity(self): self.assertNotError('aka add fact ' r'"cif [nceq $1 0] \"echo 1\" ' r'\"calc $1 * [fact [calc $1 - 1]]\""') self.assertResponse('fact 4', '24') self.assertRegexp('fact 50', 'more nesting') def testDollarStarNesting(self): self.assertResponse('aka add alias aka $*', 'The operation succeeded.') self.assertResponse('alias add a+ aka add $*', 'The operation succeeded.') self.assertResponse('a+ spam echo egg', 'The operation succeeded.') self.assertResponse('spam', 'egg') def testIgnore(self): self.assertResponse('aka add test ignore', 'The operation succeeded.') self.assertNoResponse('test') class AkaTestCase(PluginTestCase): plugins = ('Aka', 'Alias', 'User', 'Utilities') def testMaximumLength(self): self.assertNotError('aka add "foo bar baz qux quux" "echo test"') self.assertError('aka add "foo bar baz qux quux corge" "echo test"') def testAkaLockedHelp(self): self.assertNotError('register evil_admin foo') self.assertNotError('aka add slashdot foo') self.assertRegexp('help aka slashdot', "a global alias.*Alias for .*foo") self.assertNotRegexp('help aka slashdot', 'Locked by') self.assertNotError('aka lock slashdot') self.assertRegexp('help aka slashdot', 'Locked by evil_admin') self.assertNotError('aka unlock slashdot') self.assertNotRegexp('help aka slashdot', 'Locked by') def testAliasImport(self): self.assertNotError('alias add foo "echo bar"') self.assertNotError(u('alias add baz "echo café"')) self.assertNotError('aka add qux "echo quux"') self.assertResponse('alias foo', 'bar') self.assertResponse('alias baz', 'café') self.assertRegexp('aka foo', 'there is no command named') self.assertResponse('aka qux', 'quux') self.assertNotError('aka importaliasdatabase') self.assertRegexp('alias foo', 'there is no command named') self.assertResponse('aka foo', 'bar') self.assertResponse('aka baz', 'café') self.assertResponse('aka qux', 'quux') self.assertNotError('alias add foo "echo test"') self.assertNotError('alias add spam "echo egg"') self.assertNotError('alias lock spam') self.assertRegexp('aka importaliasdatabase', r'the 1 following command: foo \(This Aka already exists.\)$') self.assertResponse('aka foo', 'bar') self.assertResponse('alias foo', 'test') self.assertRegexp('alias spam', 'there is no command named') self.assertResponse('aka spam', 'egg') def testList(self): self.assertNotError('aka add foo bar') self.assertRegexp('aka list', r'foo.*?bar \$\*') self.assertNotError('aka add "foo bar" baz') self.assertRegexp('aka list', r'foo.*?bar \$\*.*?foo bar.*?baz \$\*') def testListLockedUnlocked(self): self.assertNotError('register tacocat hunter2') self.assertNotError('aka add foo bar') self.assertNotError('aka add abcd echo hi') self.assertNotError('aka lock foo') self.assertRegexp('aka list --locked', 'foo') self.assertNotRegexp('aka list --locked', 'abcd') self.assertNotRegexp('aka list --unlocked', 'foo') self.assertRegexp('aka list --unlocked', 'abcd') # Can't look up both. self.assertError('aka list --locked --unlocked abcd') def testSearch(self): self.assertNotError('aka add foo bar') self.assertNotError('aka add "many words" "much command"') self.assertRegexp('aka search f', 'foo') self.assertError('aka search abcdefghijklmnop') self.assertRegexp('aka search many', 'many words') # This should be case insensitive too. self.assertRegexp('aka search MaNY', 'many words') class AkaWebUITestCase(ChannelHTTPPluginTestCase): plugins = ('Aka',) config = { 'servers.http.keepAlive': True, 'plugins.Aka.web.enable': False, } def setUp(self): super(ChannelHTTPPluginTestCase, self).setUp() httpserver.startServer() def tearDown(self): httpserver.stopServer() super(ChannelHTTPPluginTestCase, self).tearDown() def testToggleWebEnable(self): self.assertHTTPResponse('/aka/', 404) self.assertNotError('config plugins.Aka.web.enable True') self.assertHTTPResponse('/aka/', 200) self.assertNotError('config plugins.Aka.web.enable False') self.assertHTTPResponse('/aka/', 404) def testGlobalPage(self): self.assertNotError('config plugins.Aka.web.enable True') self.assertNotError('aka add foo1 echo 1') self.assertNotError('aka add --channel #foo foo2 echo 2') self.assertNotError('aka add --channel #bar foo3 echo 3') (respCode, body) = self.request('/aka/list/global') self.assertEqual(respCode, 200) self.assertIn(b'foo1', body) self.assertNotIn(b'foo2', body) self.assertNotIn(b'foo3', body) def testChannelPage(self): self.assertNotError('config plugins.Aka.web.enable True') self.assertNotError('aka add foo1 echo 1') self.assertNotError('aka add --channel #foo foo2 echo 2') self.assertNotError('aka add --channel #bar foo3 echo 3') (respCode, body) = self.request('/aka/list/%23foo') self.assertEqual(respCode, 200) self.assertIn(b'foo1', body) self.assertIn(b'foo2', body) self.assertNotIn(b'foo3', body) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Alias/0000755000175000017500000000000014535072535014634 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/__init__.py0000644000175000017500000000513714535072470016751 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Allows aliases for other commands. NOTE THAT IT'S RECOMMENDED TO USE Aka PLUGIN INSTEAD! """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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! from .plugin import findBiggestDollar, AliasError, escapeAlias, unescapeAlias # for the tests. if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/config.py0000644000175000017500000000475314535072470016462 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Alias') def configure(advanced): # This will be called by supybot to configure this module. advanced is # a bool that specifies whether the user identified themself as an advanced # user or not. You should effect your configuration by manipulating the # registry as appropriate. from supybot.questions import expect, anything, something, yn conf.registerPlugin('Alias', True) Alias = conf.registerPlugin('Alias') conf.registerGroup(Alias, 'aliases') conf.registerGroup(Alias, 'escapedaliases') conf.registerGlobalValue(Alias, 'validName', registry.String(r'^[^\x00-\x20]+$', _("""Regex which alias names must match in order to be valid"""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Alias/locales/0000755000175000017500000000000014535072535016256 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/locales/de.po0000644000175000017500000001115414535072470017206 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:36+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: German\n" "X-Poedit-Country: GERMANY\n" #: config.py:48 msgid "Regex which alias names must match in order to be valid" msgstr "" #: plugin.py:48 msgid "" "Returns the channel the msg came over or the channel given in args.\n" "\n" " If the channel was given in args, args is modified (the channel is\n" " removed).\n" " " msgstr "" "Gibt den Kanal aus über den die Nachricht kam oder der Kanal der in den " "Argumenten gegeben wurde.\n" "\n" "Falls der Kanal in den Argumenten angegeben wurde, werden die Argumente " "bearbeitet (der Kanal wird entfernt." #: plugin.py:110 msgid "" "Encodes dots and pipes\n" " Format: aa((d|p))+." msgstr "" #: plugin.py:226 msgid " at least" msgstr "mindestens" #: plugin.py:228 plugin.py:233 msgid "" "\n" "\n" "Alias for %q." msgstr "" "\n" "\n" "Alias für %q." #: plugin.py:229 plugin.py:234 msgid "argument" msgstr "Argument" #: plugin.py:239 msgid "" "\n" " This plugin allows users to define aliases to commands and combinations\n" " of commands (via nesting).\n" " This plugin is only kept for backward compatibility, you should use the\n" " built-in Aka plugin instead (you can migrate your existing aliases " "using\n" " the 'importaliasdatabase' command.\n" "\n" " To add an alias, `trout`, which expects a word as an argument::\n" "\n" " @alias add trout \"action slaps $1 with a large trout\"\n" " jamessan: The operation succeeded.\n" " @trout me\n" " * bot slaps me with a large trout\n" "\n" " To add an alias, `lastfm`, which expects a last.fm user and replies " "with\n" " their recently played items::\n" "\n" " @alias add lastfm \"rss [format concat http://ws.audioscrobbler." "com/1.0/user/ [format concat [urlquote $1] /recenttracks.rss]]\"\n" "\n" " Note that if the nested commands being aliased hadn't been quoted, then\n" " those commands would have been run immediately, and `@lastfm` would " "always\n" " reply with the same information, the result of those commands.\n" " " msgstr "" #: plugin.py:357 msgid "" "\n" "\n" " Locks an alias so that no one else can change it.\n" " " msgstr "" "\n" "\n" "Versperrt ein Alias, sodass er nicht verändert werden kann." #: plugin.py:365 plugin.py:378 msgid "There is no such alias." msgstr "Es gibt keinen Alias mit diesem Namen." #: plugin.py:370 msgid "" "\n" "\n" " Unlocks an alias so that people can define new aliases over it.\n" " " msgstr "" "\n" "\n" "Entsperrt den Alias, sodass andere Personen ihn verändern können." #: plugin.py:386 msgid "That name isn't valid. Try %q instead." msgstr "Dieser Name ist nicht zulässig. Probiere anstatt %q." #: plugin.py:426 #, fuzzy msgid "" " \n" "\n" " Defines an alias that executes . The \n" " should be in the standard \"command argument [nestedcommand " "argument]\"\n" " arguments to the alias; they'll be filled with the first, second, " "etc.\n" " arguments. $1, $2, etc. can be used for required arguments. @1, " "@2,\n" " etc. can be used for optional arguments. $* simply means \"all\n" " remaining arguments,\" and cannot be combined with optional " "arguments.\n" " " msgstr "" " \n" "\n" "Definiert einen Alias der ausführt. sollte in der " "Standardform \"Befehl Argument [verschachtelter Befehl Argument\" angegeben " "werden." #: plugin.py:449 msgid "" "\n" "\n" " Removes the given alias, if unlocked.\n" " " msgstr "" "\n" "\n" "Entfernt den gegeben Alias, falls er nicht gesperrt ist." #: plugin.py:463 msgid "" "[--locked|--unlocked]\n" "\n" " Lists alias names of a particular type, defaults to all aliases if " "no\n" " --locked or --unlocked option is given.\n" " " msgstr "" #: plugin.py:470 msgid "Cannot specify --locked and --unlocked simultaneously" msgstr "" #: plugin.py:486 msgid "There are no aliases of that type." msgstr "" #: plugin.py:488 #, fuzzy msgid "There are no aliases." msgstr "Es gibt keinen Alias mit diesem Namen." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/locales/fi.po0000644000175000017500000001316714535072470017222 0ustar00valval# Alias plugin in Limnoria. # Copyright (C) 2011, 2012 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Supybot Alias plugin\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:47+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: suomi <>\n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" "X-Generator: Poedit 1.6.10\n" #: config.py:48 msgid "Regex which alias names must match in order to be valid" msgstr "" #: plugin.py:48 msgid "" "Returns the channel the msg came over or the channel given in args.\n" "\n" " If the channel was given in args, args is modified (the channel is\n" " removed).\n" " " msgstr "" "Palauttaa kanavan, jolta viesti tuli tai kanavan, joka on annettu " "parametreissä.\n" "\n" " Jos kanava annetaan parametreissä, parametriä muokataan (kanava\n" " poistetaan).\n" " " #: plugin.py:110 #, fuzzy msgid "" "Encodes dots and pipes\n" " Format: aa((d|p))+." msgstr "" "Salaa [a-z0-9.]+ sisään [a-z][a-z0-9].\n" " Muoto: aa(d)+." #: plugin.py:226 msgid " at least" msgstr "vähintään" #: plugin.py:228 plugin.py:233 msgid "" "\n" "\n" "Alias for %q." msgstr "" "\n" "\n" "Alias %q:lle." #: plugin.py:229 plugin.py:234 msgid "argument" msgstr "parametri" #: plugin.py:239 msgid "" "\n" " This plugin allows users to define aliases to commands and combinations\n" " of commands (via nesting).\n" " This plugin is only kept for backward compatibility, you should use the\n" " built-in Aka plugin instead (you can migrate your existing aliases " "using\n" " the 'importaliasdatabase' command.\n" "\n" " To add an alias, `trout`, which expects a word as an argument::\n" "\n" " @alias add trout \"action slaps $1 with a large trout\"\n" " jamessan: The operation succeeded.\n" " @trout me\n" " * bot slaps me with a large trout\n" "\n" " To add an alias, `lastfm`, which expects a last.fm user and replies " "with\n" " their recently played items::\n" "\n" " @alias add lastfm \"rss [format concat http://ws.audioscrobbler." "com/1.0/user/ [format concat [urlquote $1] /recenttracks.rss]]\"\n" "\n" " Note that if the nested commands being aliased hadn't been quoted, then\n" " those commands would have been run immediately, and `@lastfm` would " "always\n" " reply with the same information, the result of those commands.\n" " " msgstr "" #: plugin.py:357 msgid "" "\n" "\n" " Locks an alias so that no one else can change it.\n" " " msgstr "" "\n" "\n" " Lukitsee aliaksen, niin ettei kukaan muu voi muuttaa sitä.\n" " " #: plugin.py:365 plugin.py:378 msgid "There is no such alias." msgstr "Tuollaista aliasta ei ole." #: plugin.py:370 msgid "" "\n" "\n" " Unlocks an alias so that people can define new aliases over it.\n" " " msgstr "" "\n" "\n" " Poistaa lukituksen aliaksesta, jotta ihmiset vouvat määrittää uusia " "aliaksia sen päälle.\n" " " #: plugin.py:386 msgid "That name isn't valid. Try %q instead." msgstr "Tuo nimi ei ole kelvollinen. Yritä sen sijaan %q:ta." #: plugin.py:426 msgid "" " \n" "\n" " Defines an alias that executes . The \n" " should be in the standard \"command argument [nestedcommand " "argument]\"\n" " arguments to the alias; they'll be filled with the first, second, " "etc.\n" " arguments. $1, $2, etc. can be used for required arguments. @1, " "@2,\n" " etc. can be used for optional arguments. $* simply means \"all\n" " remaining arguments,\" and cannot be combined with optional " "arguments.\n" " " msgstr "" " \n" "\n" " Määrittää aliaksen , joka suorittaa . \n" " pitäisi olla tavallinen \"komento parametri [sisäkkäinen komento " "parametrit]\"\n" " parametrejä aliakselle; ne täytetään ensimmäinen, toinen, jne.\n" " Parametrit. $1, $2, jne. ovat vaadittava parametrejä. @1, @2,\n" " jne. ovat valinnaisia parametrejä. $* tarkoittaa yksinkertaisesti " "\"kaikki\n" " jäljellä olevat parametrit,\" ja johon ei voida yhdistää vaihtoehtoisia " "parametrejä.\n" " " #: plugin.py:449 msgid "" "\n" "\n" " Removes the given alias, if unlocked.\n" " " msgstr "" "\n" "\n" " Poistaa annetun aliaksen jos se ei ole lukittu.\n" " " #: plugin.py:463 msgid "" "[--locked|--unlocked]\n" "\n" " Lists alias names of a particular type, defaults to all aliases if " "no\n" " --locked or --unlocked option is given.\n" " " msgstr "" #: plugin.py:470 msgid "Cannot specify --locked and --unlocked simultaneously" msgstr "" #: plugin.py:486 msgid "There are no aliases of that type." msgstr "" #: plugin.py:488 #, fuzzy msgid "There are no aliases." msgstr "Tuollaista aliasta ei ole." #, fuzzy #~ msgid "" #~ "This plugin allows users to define aliases to commands and combinations\n" #~ " of commands (via nesting)." #~ msgstr "" #~ "Tämä plugini sallii käyttäjien määrittää aliaksia komennoille ja " #~ "komentojen yhdistelmille (sisäytyksellä)." #~ msgid "You've attempted more nesting than is currently allowed on this bot." #~ msgstr "" #~ "Yritit sisällyttää useampia komentoja, kuin tämä botti sallii juuri nyt." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/locales/fr.po0000644000175000017500000001170314535072470017225 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:48 msgid "Regex which alias names must match in order to be valid" msgstr "" #: plugin.py:48 msgid "" "Returns the channel the msg came over or the channel given in args.\n" "\n" " If the channel was given in args, args is modified (the channel is\n" " removed).\n" " " msgstr "" "Retourne le canal duquel vient le message ou le canal donné en argument.\n" "\n" "Si le canal était donné en argument, args est modifié (le canal est " "supprimé)." #: plugin.py:110 #, fuzzy msgid "" "Encodes dots and pipes\n" " Format: aa((d|p))+." msgstr "." #: plugin.py:226 msgid " at least" msgstr "au moins" #: plugin.py:228 plugin.py:233 msgid "" "\n" "\n" "Alias for %q." msgstr "" "\n" "\n" "Alias pour %q." #: plugin.py:229 plugin.py:234 msgid "argument" msgstr "argument" #: plugin.py:239 msgid "" "\n" " This plugin allows users to define aliases to commands and combinations\n" " of commands (via nesting).\n" " This plugin is only kept for backward compatibility, you should use the\n" " built-in Aka plugin instead (you can migrate your existing aliases " "using\n" " the 'importaliasdatabase' command.\n" "\n" " To add an alias, `trout`, which expects a word as an argument::\n" "\n" " @alias add trout \"action slaps $1 with a large trout\"\n" " jamessan: The operation succeeded.\n" " @trout me\n" " * bot slaps me with a large trout\n" "\n" " To add an alias, `lastfm`, which expects a last.fm user and replies " "with\n" " their recently played items::\n" "\n" " @alias add lastfm \"rss [format concat http://ws.audioscrobbler." "com/1.0/user/ [format concat [urlquote $1] /recenttracks.rss]]\"\n" "\n" " Note that if the nested commands being aliased hadn't been quoted, then\n" " those commands would have been run immediately, and `@lastfm` would " "always\n" " reply with the same information, the result of those commands.\n" " " msgstr "" #: plugin.py:357 msgid "" "\n" "\n" " Locks an alias so that no one else can change it.\n" " " msgstr "" "\n" "\n" "Vérouille un alias pour que personne d'autre ne puisse le changer." #: plugin.py:365 plugin.py:378 msgid "There is no such alias." msgstr "Cet alias n'existe pas." #: plugin.py:370 msgid "" "\n" "\n" " Unlocks an alias so that people can define new aliases over it.\n" " " msgstr "" "\n" "\n" "Déverrouille un alias de façon à ce que des gens puissent le redéfinir." #: plugin.py:386 msgid "That name isn't valid. Try %q instead." msgstr "Ce nom n'est pas valide. Essayez plutôt %q." #: plugin.py:426 msgid "" " \n" "\n" " Defines an alias that executes . The \n" " should be in the standard \"command argument [nestedcommand " "argument]\"\n" " arguments to the alias; they'll be filled with the first, second, " "etc.\n" " arguments. $1, $2, etc. can be used for required arguments. @1, " "@2,\n" " etc. can be used for optional arguments. $* simply means \"all\n" " remaining arguments,\" and cannot be combined with optional " "arguments.\n" " " msgstr "" " \n" "\n" "Défini un alias qui exécute . L' peut être dans le " "standard \"commande argument [commandeimbriquee argument]\". Les arguments " "donnés à l'alias doivent être donnés dans l'ordre. Vous pouvez utiliser $1, " "$2, etc pour symboliser les arguments obligatoires qui seront donnés à " "l'alias, et @1, @2, etc pour symboliser ceux optionnels. $* signifie " "simplement *tous* les arguments restants, et ne peut être combiné avec des " "arguments optionnels." #: plugin.py:449 msgid "" "\n" "\n" " Removes the given alias, if unlocked.\n" " " msgstr "" "\n" "\n" "Supprime l'alias donné, si il n'est pas verrouillé." #: plugin.py:463 msgid "" "[--locked|--unlocked]\n" "\n" " Lists alias names of a particular type, defaults to all aliases if " "no\n" " --locked or --unlocked option is given.\n" " " msgstr "" #: plugin.py:470 msgid "Cannot specify --locked and --unlocked simultaneously" msgstr "" #: plugin.py:486 msgid "There are no aliases of that type." msgstr "" #: plugin.py:488 #, fuzzy msgid "There are no aliases." msgstr "Cet alias n'existe pas." #~ msgid "You've attempted more nesting than is currently allowed on this bot." #~ msgstr "" #~ "Vous avez essayé d’utiliser plus d’imbrication que ce qui est " #~ "actuellement autorisé sur ce bot." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/locales/hu.po0000644000175000017500000001207014535072470017230 0ustar00valval# Limnoria Alias plugin. # Copyright (C) 2011 Limnoria # nyuszika7h , 2011. # Mikaela Suomalainen , 2012. # msgid "" msgstr "" "Project-Id-Version: Limnoria Alias\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:12+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:48 msgid "Regex which alias names must match in order to be valid" msgstr "" #: plugin.py:48 msgid "" "Returns the channel the msg came over or the channel given in args.\n" "\n" " If the channel was given in args, args is modified (the channel is\n" " removed).\n" " " msgstr "" "Kiírja a csatorna nevét, ahonnan az üzenet jött, vagy a paraméterként " "megadott csatornát.\n" "\n" "Ha a csatorna meg volt adva paraméterként, a paraméterek módosulnak (a " "csatorna eltávolításra kerül)." #: plugin.py:110 msgid "" "Encodes dots and pipes\n" " Format: aa((d|p))+." msgstr "" #: plugin.py:226 msgid " at least" msgstr " legalább" #: plugin.py:228 plugin.py:233 msgid "" "\n" "\n" "Alias for %q." msgstr "" " @alias add trout \"action slaps $1 with a large trout\"\n" " jamessan: The operation succeeded.\n" " @trout me\n" " * bot slaps me with a large trout\n" "\n" " To add an alias, `lastfm`, which expects a last.fm user and replies " "with\n" " their recently played items::\n" "\n" " @alias add lastfm \"rss [format concat http://ws.audioscrobbler." "com/1.0/user/ [format concat [urlquote $1] /recenttracks.rss]]\"\n" "\n" " Note that if the nested commands being aliased hadn't been quoted, then\n" " those commands would have been run immediately, and `@lastfm` would " "always\n" " reply with the same information, the result of those commands.\n" " " msgstr "" #: plugin.py:357 msgid "" "\n" "\n" " Locks an alias so that no one else can change it.\n" " " msgstr "" "<álnév>\n" "\n" "Lezár egy álnevet, hogy senki más ne változtathassa meg." #: plugin.py:365 plugin.py:378 msgid "There is no such alias." msgstr "Nincs ilyen álnév." #: plugin.py:370 msgid "" "\n" "\n" " Unlocks an alias so that people can define new aliases over it.\n" " " msgstr "" "<álnév>\n" "\n" "Feloldja egy álnév lezárását, hogy az emberek új álnevekkel írhassák felül." #: plugin.py:386 msgid "That name isn't valid. Try %q instead." msgstr "Az a név nem érvényes. Próbáld meg %q-t inkább." #: plugin.py:426 msgid "" " \n" "\n" " Defines an alias that executes . The \n" " should be in the standard \"command argument [nestedcommand " "argument]\"\n" " arguments to the alias; they'll be filled with the first, second, " "etc.\n" " arguments. $1, $2, etc. can be used for required arguments. @1, " "@2,\n" " etc. can be used for optional arguments. $* simply means \"all\n" " remaining arguments,\" and cannot be combined with optional " "arguments.\n" " " msgstr "" " <álnév>\n" "\n" "Meghatároz egy nevű álnevet, amely futtatja <álnév> parancsot. Az " "<álnév>-nek a szabványos \"parancs paraméter [beágyazottparancs paraméter]\" " "álnév paraméterei formában kell lennie; ezek ki lesznek töltve az első, " "második stb. paraméterekkel. $1, $2 stb. használható kötelező " "paraméterekhez. @1, @2 stb. használható választható paraméterekhez. $* azt " "jelenti, \"az összes hátralévő paraméter,\" és nem kombinálható választható " "paraméterekkel." #: plugin.py:449 msgid "" "\n" "\n" " Removes the given alias, if unlocked.\n" " " msgstr "" "\n" "\n" "Eltávolítja a megadott álnevet, ha nincs lezárva." #: plugin.py:463 msgid "" "[--locked|--unlocked]\n" "\n" " Lists alias names of a particular type, defaults to all aliases if " "no\n" " --locked or --unlocked option is given.\n" " " msgstr "" #: plugin.py:470 msgid "Cannot specify --locked and --unlocked simultaneously" msgstr "" #: plugin.py:486 msgid "There are no aliases of that type." msgstr "" #: plugin.py:488 #, fuzzy msgid "There are no aliases." msgstr "Nincs ilyen álnév." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/locales/it.po0000644000175000017500000001146714535072470017241 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-07 08:23+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:48 msgid "Regex which alias names must match in order to be valid" msgstr "" #: plugin.py:48 msgid "" "Returns the channel the msg came over or the channel given in args.\n" "\n" " If the channel was given in args, args is modified (the channel is\n" " removed).\n" " " msgstr "" "Restituisce il canale da dove proviene il messaggio o il canale fornito come " "argomento.\n" "\n" " Se il canale è stato dato come argomento, quest'ultimo viene modificato " "(il canale\n" " viene rimosso).\n" " " #: plugin.py:110 msgid "" "Encodes dots and pipes\n" " Format: aa((d|p))+." msgstr "" #: plugin.py:226 msgid " at least" msgstr " almeno" #: plugin.py:228 plugin.py:233 msgid "" "\n" "\n" "Alias for %q." msgstr "" "\n" "\n" "Alias per %q." #: plugin.py:229 plugin.py:234 msgid "argument" msgstr "argomento" #: plugin.py:239 msgid "" "\n" " This plugin allows users to define aliases to commands and combinations\n" " of commands (via nesting).\n" " This plugin is only kept for backward compatibility, you should use the\n" " built-in Aka plugin instead (you can migrate your existing aliases " "using\n" " the 'importaliasdatabase' command.\n" "\n" " To add an alias, `trout`, which expects a word as an argument::\n" "\n" " @alias add trout \"action slaps $1 with a large trout\"\n" " jamessan: The operation succeeded.\n" " @trout me\n" " * bot slaps me with a large trout\n" "\n" " To add an alias, `lastfm`, which expects a last.fm user and replies " "with\n" " their recently played items::\n" "\n" " @alias add lastfm \"rss [format concat http://ws.audioscrobbler." "com/1.0/user/ [format concat [urlquote $1] /recenttracks.rss]]\"\n" "\n" " Note that if the nested commands being aliased hadn't been quoted, then\n" " those commands would have been run immediately, and `@lastfm` would " "always\n" " reply with the same information, the result of those commands.\n" " " msgstr "" #: plugin.py:357 msgid "" "\n" "\n" " Locks an alias so that no one else can change it.\n" " " msgstr "" "\n" "\n" " Blocca un alias affinché nessun altro possa modificarlo.\n" " " #: plugin.py:365 plugin.py:378 msgid "There is no such alias." msgstr "Non c'è nessun alias." #: plugin.py:370 msgid "" "\n" "\n" " Unlocks an alias so that people can define new aliases over it.\n" " " msgstr "" "\n" "\n" " Sblocca un alias affinché chiunque possa ridefinirne di nuovi.\n" " " #: plugin.py:386 msgid "That name isn't valid. Try %q instead." msgstr "Nome non valido. Prova %q invece." #: plugin.py:426 msgid "" " \n" "\n" " Defines an alias that executes . The \n" " should be in the standard \"command argument [nestedcommand " "argument]\"\n" " arguments to the alias; they'll be filled with the first, second, " "etc.\n" " arguments. $1, $2, etc. can be used for required arguments. @1, " "@2,\n" " etc. can be used for optional arguments. $* simply means \"all\n" " remaining arguments,\" and cannot be combined with optional " "arguments.\n" " " msgstr "" " \n" "\n" " Definisce un che esegue . deve essere " "nello\n" " standard \"comando argomento [comando_nidificato argomento]\"; gli\n" " argomenti dati devono essere riportati in sequenza. Per gli " "argomenti\n" " richiesti è possibile utilizzare $1, $2, ecc., mentre @1, @2, ecc. " "per\n" " quelli opzionali. $* significa semplicemente \"tutti gli argomenti\n" " rimanenti\" e non può essere combinato con quelli opzionali.\n" " " #: plugin.py:449 msgid "" "\n" "\n" " Removes the given alias, if unlocked.\n" " " msgstr "" "\n" "\n" " Rimuove l'alias specificato, se questo non è bloccato.\n" " " #: plugin.py:463 msgid "" "[--locked|--unlocked]\n" "\n" " Lists alias names of a particular type, defaults to all aliases if " "no\n" " --locked or --unlocked option is given.\n" " " msgstr "" #: plugin.py:470 msgid "Cannot specify --locked and --unlocked simultaneously" msgstr "" #: plugin.py:486 msgid "There are no aliases of that type." msgstr "" #: plugin.py:488 #, fuzzy msgid "There are no aliases." msgstr "Non c'è nessun alias." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/plugin.py0000644000175000017500000004527214535072470016514 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2014, James McCoy # Copyright (c) 2012-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import sys import types import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.utils.minisix as minisix import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Alias') # Copied from the old privmsgs.py. def getChannel(irc, msg, args): """Returns the channel the msg came over or the channel given in args. If the channel was given in args, args is modified (the channel is removed). """ if args and msg.channel: if conf.supybot.reply.requireChannelCommandsToBeSentInChannel(): if args[0] != msg.channel: s = 'Channel commands must be sent in the channel to which ' \ 'they apply; if this is not the behavior you desire, ' \ 'ask the bot\'s administrator to change the registry ' \ 'variable ' \ 'supybot.reply.requireChannelCommandsToBeSentInChannel ' \ 'to False.' raise callbacks.Error(s) return args.pop(0) elif msg.channel: return msg.channel else: raise callbacks.Error('Command must be sent in a channel or ' \ 'include a channel in its arguments.') def getArgs(args, required=1, optional=0, wildcard=0): if len(args) < required: raise callbacks.ArgumentError if len(args) < required + optional: ret = list(args) + ([''] * (required + optional - len(args))) elif len(args) >= required + optional: if not wildcard: ret = list(args[:required + optional - 1]) ret.append(' '.join(args[required + optional - 1:])) else: ret = list(args) return ret class AliasError(Exception): pass dollarRe = re.compile(r'\$(\d+)') def findBiggestDollar(alias): dollars = dollarRe.findall(alias) dollars = list(map(int, dollars)) dollars.sort() if dollars: return dollars[-1] else: return 0 atRe = re.compile(r'@(\d+)') def findBiggestAt(alias): ats = atRe.findall(alias) ats = list(map(int, ats)) ats.sort() if ats: return ats[-1] else: return 0 def needsEscaping(alias): return '.' in alias or '|' in alias def escapeAlias(alias): """Encodes dots and pipes Format: aa((d|p))+.""" prefix = '' new_alias = '' prefixes = 0 for index, char in enumerate(alias): if char == '.': prefix += '%sd' % index prefixes += 1 elif char == '|': prefix += '%sp' % index prefixes += 1 else: new_alias += char pre_prefix = 'a%ia' % prefixes return pre_prefix + prefix + new_alias def unescapeAlias(alias): alias = alias[1:] # Strip the leading 'a' escaped_nb = '' while alias[0] in '0123456789': escaped_nb += alias[0] alias = alias[1:] alias = alias[1:] escaped_nb = int(escaped_nb) escaped_chars = [] while alias[0] in '0123456789': current_group = '' while alias[0] in '0123456789': current_group += alias[0] alias = alias[1:] if alias[0] == 'd': char = '.' elif alias[0] == 'p': char = '|' else: char = alias[0] alias = alias[1:] escaped_chars.append((int(current_group), char)) if len(escaped_chars) == escaped_nb: break new_alias = '' index = 0 for char in alias: if escaped_chars and index == escaped_chars[0][0]: new_alias += escaped_chars[0][1] escaped_chars.pop(0) index += 1 new_alias += char index += 1 return new_alias def makeNewAlias(name, alias): original = alias biggestDollar = findBiggestDollar(original) biggestAt = findBiggestAt(original) wildcard = '$*' in original if biggestAt and wildcard: raise AliasError('Can\'t mix $* and optional args (@1, etc.)') if original.count('$*') > 1: raise AliasError('There can be only one $* in an alias.') testTokens = callbacks.tokenize(original) if testTokens and isinstance(testTokens[0], list): raise AliasError('Commands may not be the result of nesting.') def f(self, irc, msg, args): alias = original.replace('$nick', msg.nick) if '$channel' in original: channel = getChannel(irc, msg, args) alias = alias.replace('$channel', channel) tokens = callbacks.tokenize(alias) if biggestDollar or biggestAt: args = getArgs(args, required=biggestDollar, optional=biggestAt, wildcard=wildcard) max_len = conf.supybot.reply.maximumLength() args = list([x[:max_len] for x in args]) def regexpReplace(m): idx = int(m.group(1)) return args[idx-1] def replace(tokens, replacer): for (i, token) in enumerate(tokens): if isinstance(token, list): replace(token, replacer) else: tokens[i] = replacer(token) replace(tokens, lambda s: dollarRe.sub(regexpReplace, s)) if biggestAt: assert not wildcard args = args[biggestDollar:] replace(tokens, lambda s: atRe.sub(regexpReplace, s)) if wildcard: assert not biggestAt # Gotta remove the things that have already been subbed in. i = biggestDollar while i: args.pop(0) i -= 1 def everythingReplace(tokens): for (i, token) in enumerate(tokens): if isinstance(token, list): if everythingReplace(token): return if token == '$*': tokens[i:i+1] = args return True elif '$*' in token: tokens[i] = token.replace('$*', ' '.join(args)) return True return False everythingReplace(tokens) # Limit memory use by constraining the size of the message being passed # in to the alias. Also tracking nesting to avoid endless recursion. maxLength = conf.supybot.reply.maximumLength() tokens = [t[:maxLength] for t in tokens] self.Proxy(irc, msg, tokens, nested=irc.nested + 1) flexargs = '' if biggestDollar and (wildcard or biggestAt): flexargs = _(' at least') try: doc = format(_('\n\nAlias for %q.'), flexargs, (biggestDollar, _('argument')), alias) except UnicodeDecodeError: if minisix.PY2: alias = alias.decode('utf8') doc = format(_('\n\nAlias for %q.'), flexargs, (biggestDollar, _('argument')), alias) f = utils.python.changeFunctionName(f, name, doc) return f class Alias(callbacks.Plugin): """ This plugin allows users to define aliases to commands and combinations of commands (via nesting). This plugin is only kept for backward compatibility, you should use the built-in Aka plugin instead (you can migrate your existing aliases using the 'importaliasdatabase' command. To add an alias, ``trout``, which expects a word as an argument:: @alias add trout "action slaps $1 with a large trout" jamessan: The operation succeeded. @trout me * bot slaps me with a large trout Add an alias, ``randpercent``, which returns a random percentage value:: @alias add randpercent "squish [dice 1d100]%" This requires the ``Filter`` and ``Games`` plugins to be loaded. Note that nested commands in an alias should be quoted, or they will only run once when you create the alias, and not each time the alias is called. (In this case, not quoting the nested command would mean that ``@randpercent`` always responds with the same value!) """ def __init__(self, irc): self.__parent = super(Alias, self) self.__parent.__init__(irc) # Schema: {alias: [command, locked, commandMethod]} self.aliases = {} # XXX This should go. aliases should be a space separate list, etc. group = conf.supybot.plugins.Alias.aliases group2 = conf.supybot.plugins.Alias.escapedaliases prefixLen = len(registry.split('supybot.plugins.alias.aliases')) for (name, alias) in registry._cache.items(): name = name.lower() nameSplit = registry.split(name) if len(nameSplit) > prefixLen+1: continue if name.startswith('supybot.plugins.alias.aliases.'): name = nameSplit[-1] conf.registerGlobalValue(group, name, registry.String('', '')) conf.registerGlobalValue(group.get(name), 'locked', registry.Boolean(False, '')) elif name.startswith('supybot.plugins.alias.escapedaliases.'): name = nameSplit[-1] conf.registerGlobalValue(group2, name, registry.String('', '')) conf.registerGlobalValue(group2.get(name), 'locked', registry.Boolean(False, '')) for (name, value) in group.getValues(fullNames=False): name = name.lower() # Just in case. command = value() locked = value.locked() self.aliases[name] = [command, locked, None] for (name, value) in group2.getValues(fullNames=False): name = name.lower() # Just in case. command = value() locked = value.locked() self.aliases[unescapeAlias(name)] = [command, locked, None] for (alias, (command, locked, _)) in self.aliases.copy().items(): try: self.addAlias(irc, alias, command, locked) except Exception as e: self.log.exception('Exception when trying to add alias %s. ' 'Removing from the Alias database.', alias) del self.aliases[alias] def isCommandMethod(self, name): if not self.__parent.isCommandMethod(name): if name in self.aliases: return True else: return False else: return True def listCommands(self): return self.__parent.listCommands(self.aliases.keys()) def getCommandMethod(self, command): try: return self.__parent.getCommandMethod(command) except AttributeError: return self.aliases[command[0]][2] def aliasRegistryGroup(self, name): if needsEscaping(name): return self.registryValue('escapedaliases', value=False) else: return self.registryValue('aliases', value=False) def aliasRegistryNode(self, name): group = self.aliasRegistryGroup(name) if needsEscaping(name): return group.get(escapeAlias(name)) else: return group.get(name) def aliasRegistryRemove(self, name): group = self.aliasRegistryGroup(name) if needsEscaping(name): group.unregister(escapeAlias(name)) else: group.unregister(name) def setLocked(self, name, value): self.aliases[name][1] = value self.aliasRegistryNode(name).locked.setValue(value) def isValidName(self, name): if not re.search(self.registryValue('validName'), name): return False if not registry.isValidRegistryName(name): return False return True @internationalizeDocstring def lock(self, irc, msg, args, name): """ Locks an alias so that no one else can change it. """ if name in self.aliases and self.isCommandMethod(name): self.setLocked(name, True) irc.replySuccess() else: irc.error(_('There is no such alias.')) lock = wrap(lock, [('checkCapability', 'admin'), 'commandName']) @internationalizeDocstring def unlock(self, irc, msg, args, name): """ Unlocks an alias so that people can define new aliases over it. """ if name in self.aliases and self.isCommandMethod(name): self.setLocked(name, False) irc.replySuccess() else: irc.error(_('There is no such alias.')) unlock = wrap(unlock, [('checkCapability', 'admin'), 'commandName']) def addAlias(self, irc, name, alias, lock=False): if not self.isValidName(name): raise AliasError('Invalid alias name.') realName = callbacks.canonicalName(name) if name != realName: s = format(_('That name isn\'t valid. Try %q instead.'), realName) raise AliasError(s) name = realName if self.isCommandMethod(name): if realName not in self.aliases: s = 'You can\'t overwrite commands in this plugin.' raise AliasError(s) if name in self.aliases: (currentAlias, locked, _) = self.aliases[name] if locked and currentAlias != alias: raise AliasError(format('Alias %q is locked.', name)) f = makeNewAlias(name, alias) f = types.MethodType(f, self) if name in self.aliases: # We gotta remove it so its value gets updated. self.aliasRegistryRemove(name) aliasGroup = self.aliasRegistryGroup(name) if needsEscaping(name): confname = escapeAlias(name) else: confname = name conf.registerGlobalValue(aliasGroup, confname, registry.String(alias, '')) conf.registerGlobalValue(aliasGroup.get(confname), 'locked', registry.Boolean(lock, '')) self.aliases[name] = [alias, lock, f] def removeAlias(self, name, evenIfLocked=False): name = callbacks.canonicalName(name) if name in self.aliases and self.isCommandMethod(name): if evenIfLocked or not self.aliases[name][1]: del self.aliases[name] self.aliasRegistryRemove(name) else: raise AliasError('That alias is locked.') else: raise AliasError('There is no such alias.') @internationalizeDocstring def add(self, irc, msg, args, name, alias): """ Defines an alias that executes . The should be in the standard "command argument [nestedcommand argument]" arguments to the alias; they'll be filled with the first, second, etc. arguments. $1, $2, etc. can be used for required arguments. @1, @2, etc. can be used for optional arguments. $* simply means "all remaining arguments," and cannot be combined with optional arguments. """ if ' ' not in alias: # If it's a single word, they probably want $*. alias += ' $*' try: self.addAlias(irc, name, alias) self.log.info('Adding alias %q for %q (from %s)', name, alias, msg.prefix) irc.replySuccess() except AliasError as e: irc.error(str(e)) add = wrap(add, ['commandName', 'text']) @internationalizeDocstring def remove(self, irc, msg, args, name): """ Removes the given alias, if unlocked. """ try: self.removeAlias(name) self.log.info('Removing alias %q (from %s)', name, msg.prefix) irc.replySuccess() except AliasError as e: irc.error(str(e)) remove = wrap(remove, ['commandName']) @internationalizeDocstring def list(self, irc, msg, args, optlist): """[--locked|--unlocked] Lists alias names of a particular type, defaults to all aliases if no --locked or --unlocked option is given. """ optlist = dict(optlist) if len(optlist)>1: irc.error(_('Cannot specify --locked and --unlocked simultaneously')) return aliases = [] for name in self.aliases.keys(): if self.isCommandMethod(name): if 'locked' in optlist: if self.aliases[name][1]: aliases.append(name) elif 'unlocked' in optlist: if not self.aliases[name][1]: aliases.append(name) else: aliases.append(name) if aliases: aliases.sort() irc.reply(format('%L', aliases)) else: if len(optlist): irc.reply(_('There are no aliases of that type.')) else: irc.reply(_('There are no aliases.')) list = wrap(list, [getopts({'locked':'', 'unlocked':''})]) Class = Alias # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Alias/test.py0000644000175000017500000001754714535072470016201 0ustar00valval# -*- coding: utf8 -*- ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import supybot.conf as conf import supybot.plugin as plugin import supybot.registry as registry from supybot.utils.minisix import u from . import plugin as Alias class FunctionsTest(SupyTestCase): def testFindBiggestDollar(self): self.assertEqual(Alias.findBiggestDollar(''), 0) self.assertEqual(Alias.findBiggestDollar('foo'), 0) self.assertEqual(Alias.findBiggestDollar('$0'), 0) self.assertEqual(Alias.findBiggestDollar('$1'), 1) self.assertEqual(Alias.findBiggestDollar('$2'), 2) self.assertEqual(Alias.findBiggestDollar('$2 $10'), 10) self.assertEqual(Alias.findBiggestDollar('$3'), 3) self.assertEqual(Alias.findBiggestDollar('$3 $2 $1'), 3) self.assertEqual(Alias.findBiggestDollar('foo bar $1'), 1) self.assertEqual(Alias.findBiggestDollar('foo $2 $1'), 2) self.assertEqual(Alias.findBiggestDollar('foo $0 $1'), 1) self.assertEqual(Alias.findBiggestDollar('foo $1 $3'), 3) self.assertEqual(Alias.findBiggestDollar('$10 bar $1'), 10) class AliasTestCase(ChannelPluginTestCase): plugins = ('Alias', 'Filter', 'Utilities', 'Format', 'Reply') def testNoAliasWithNestedCommandName(self): self.assertError('alias add foo "[bar] baz"') def testDoesNotOverwriteCommands(self): # We don't have dispatcher commands anymore #self.assertError('alias add alias "echo foo bar baz"') self.assertError('alias add add "echo foo bar baz"') self.assertError('alias add remove "echo foo bar baz"') self.assertError('alias add lock "echo foo bar baz"') self.assertError('alias add unlock "echo foo bar baz"') def testAliasHelp(self): self.assertNotError('alias add slashdot foo') self.assertRegexp('help slashdot', "Alias for .*foo") self.assertNotError('alias add nonascii echo éé') self.assertRegexp('help nonascii', "Alias for .*echo éé") def testRemove(self): self.assertNotError('alias add foo echo bar') self.assertResponse('foo', 'bar') self.assertNotError('alias remove foo') self.assertError('foo') def testDollars(self): self.assertNotError('alias add rot26 "rot13 [rot13 $1]"') self.assertResponse('rot26 foobar', 'foobar') def testMoreDollars(self): self.assertNotError('alias add rev "echo $3 $2 $1"') self.assertResponse('rev foo bar baz', 'baz bar foo') def testAllArgs(self): self.assertNotError('alias add swap "echo $2 $1 $*"') self.assertResponse('swap 1 2 3 4 5', '2 1 3 4 5') self.assertError('alias add foo "echo $1 @1 $*"') self.assertNotError('alias add moo echo $1 $*') self.assertError('moo') self.assertResponse('moo foo', 'foo') self.assertResponse('moo foo bar', 'foo bar') def testChannel(self): self.assertNotError('alias add channel echo $channel') self.assertResponse('alias channel', self.channel) def testNick(self): self.assertNotError('alias add sendingnick "rot13 [rot13 $nick]"') self.assertResponse('sendingnick', self.nick) def testAddRemoveAlias(self): cb = self.irc.getCallback('Alias') cb.addAlias(self.irc, 'foobar', 'echo sbbone', lock=True) self.assertResponse('foobar', 'sbbone') self.assertRaises(Alias.AliasError, cb.removeAlias, 'foobar') cb.removeAlias('foobar', evenIfLocked=True) self.assertNotIn('foobar', cb.aliases) self.assertError('foobar') self.assertRegexp('alias add abc\x07 ignore', 'Error.*Invalid') def testOptionalArgs(self): self.assertNotError('alias add myrepr "repr @1"') self.assertResponse('myrepr foo', '"foo"') self.assertResponse('myrepr ""', '""') def testNoExtraSpaces(self): self.assertNotError('alias add foo "action takes $1\'s money"') self.assertResponse('foo bar', '\x01ACTION takes bar\'s money\x01') def testNoExtraQuotes(self): self.assertNotError('alias add myre "echo s/$1/$2/g"') self.assertResponse('myre foo bar', 's/foo/bar/g') def testUnicode(self): self.assertNotError(u('alias add \u200b echo foo')) self.assertResponse(u('\u200b'), 'foo') self.assertNotError('alias add café echo bar') self.assertResponse('café', 'bar') def testSimpleAliasWithoutArgsImpliesDollarStar(self): self.assertNotError('alias add exo echo') self.assertResponse('exo foo bar baz', 'foo bar baz') class EscapedAliasTestCase(ChannelPluginTestCase): plugins = ('Alias', 'Utilities') def setUp(self): registry._cache.update( {'supybot.plugins.Alias.escapedaliases.a1a3dfoobar': 'echo baz', 'supybot.plugins.Alias.escapedaliases.a1a3dfoobar.locked': 'False'}) super(EscapedAliasTestCase, self).setUp() def testReadDatabase(self): self.assertResponse('foo.bar', 'baz') def testAdd(self): self.assertNotError('alias add spam.egg echo hi') self.assertResponse('spam.egg', 'hi') self.assertNotError('alias add spam|egg echo hey') self.assertResponse('spam|egg', 'hey') self.assertNotError('alias remove spam.egg') self.assertError('spam.egg') self.assertNotError('spam|egg') self.assertNotError('alias remove spam|egg') self.assertError('spam.egg') self.assertError('spam|egg') def testWriteDatabase(self): self.assertNotError('alias add fooo.spam echo egg') self.assertResponse('fooo.spam', 'egg') self.assertTrue(hasattr(conf.supybot.plugins.Alias.escapedaliases, 'a1a4dfooospam')) self.assertEqual(conf.supybot.plugins.Alias.escapedaliases.a1a4dfooospam(), 'echo egg') self.assertNotError('alias add foo.spam.egg echo supybot') self.assertResponse('foo.spam.egg', 'supybot') self.assertTrue(hasattr(conf.supybot.plugins.Alias.escapedaliases, 'a2a3d8dfoospamegg')) self.assertEqual(conf.supybot.plugins.Alias.escapedaliases.a2a3d8dfoospamegg(), 'echo supybot') self.assertEqual(Alias.unescapeAlias('a2a3d8dfoospamegg'), 'foo.spam.egg') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Anonymous/0000755000175000017500000000000014535072535015573 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/__init__.py0000644000175000017500000000474014535072470017707 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ Allows folks to talk through the bot anonymously. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/config.py0000644000175000017500000000667414535072470017425 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Anonymous') 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('Anonymous', True) Anonymous = conf.registerPlugin('Anonymous') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Anonymous, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) conf.registerChannelValue(conf.supybot.plugins.Anonymous, 'requirePresenceInChannel', registry.Boolean(True, _("""Determines whether the bot should require people trying to use this plugin to be in the channel they wish to anonymously send to."""))) conf.registerChannelValue(conf.supybot.plugins.Anonymous, 'requireRegistration', registry.Boolean(True, _("""Determines whether the bot should require people trying to use this plugin to be registered."""))) conf.registerChannelValue(conf.supybot.plugins.Anonymous, 'requireCapability', registry.String('', _("""Determines what capability (if any) the bot should require people trying to use this plugin to have."""))) conf.registerGlobalValue(conf.supybot.plugins.Anonymous, 'allowPrivateTarget', registry.Boolean(False, _("""Determines whether the bot will allow the "tell" command to be used. If true, the bot will allow the "tell" command to send private messages to other users."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Anonymous/locales/0000755000175000017500000000000014535072535017215 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/locales/de.po0000644000175000017500000001463614535072470020155 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-28 12:55+0100\n" "Last-Translator: Florian Besser \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\n" " the bot should require people trying to use this plugin to be in the\n" " channel they wish to anonymously send to." msgstr "Legt fest ob Leute im Kanal sein müssen, an den anonym senden wollen." #: config.py:54 msgid "" "Determines whether the bot should require\n" " people trying to use this plugin to be registered." msgstr "" "Legt fest ob Nutzer registiert sein müssen um dieses Plugin zu benutze." #: config.py:57 msgid "" "Determines what capability (if any) the bot should\n" " require people trying to use this plugin to have." msgstr "" "Legt fest welche Fähgikeiten (falls überhaupt) der Bot verlangt von Personen " "die versuchen dieses Plugin zu benutzen." #: config.py:60 #, fuzzy msgid "" "Determines whether the bot will allow the\n" " \"tell\" command to be used. If true, the bot will allow the \"tell\"\n" " command to send private messages to other users." msgstr "" "Legt fest ob der Bot verlangt, dass Ziele des Befehls \"say\" öffentlich " "sind (z.B. Kanäle). Falls das auf True gesetzt ist, wird ber Bot Nutzern " "erlauben den Befehl \"say\" zu nutzen um anderen Nutzern private Nachrichten " "zu senden." #: plugin.py:45 #, fuzzy msgid "" "\n" " This plugin allows users to act through the bot anonymously. The 'do'\n" " command has the bot perform an anonymous action in a given channel, and\n" " the 'say' command allows other people to speak through the bot. Since\n" " this can be fairly well abused, you might want to set\n" " supybot.plugins.Anonymous.requireCapability so only users with that\n" " capability can use this plugin. For extra security, you can require " "that\n" " the user be *in* the channel they are trying to address anonymously " "with\n" " supybot.plugins.Anonymous.requirePresenceInChannel, or you can require\n" " that the user be registered by setting\n" " supybot.plugins.Anonymous.requireRegistration.\n" "\n" " Example: Proving that you are the owner\n" " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " When you ask for cloak/vhost for your bot, the network operators will\n" " often ask you to prove that you own the bot. You can do this for " "example\n" " with the following method::\n" "\n" " @load Anonymous\n" " @config plugins.anonymous.requirecapability owner\n" " @config plugins.anonymous.allowprivatetarget True\n" " @anonymous say Hi, my owner is :)\n" "\n" " This\n" " * Loads the plugin.\n" " * Makes the plugin require that you are the owner\n" "\n" " * If anyone could send private messages as the bot, they could also\n" " access network services.\n" "\n" " * Allows sending private messages\n" " * Sends message ``Hi, my owner is :)`` to ``operator " "nick``.\n" "\n" " * Note that you won't see the messages that are sent to the bot.\n" "\n" " " msgstr "" "Das Plugin erlaubt Nutzern durch den Bot anonym zu bleiben. Der 'do' Befehl " "lässt den Bot eine anonyme Aktion in einem Kanal ausführen und der 'say' " "Befehl lässt Nutzer durch den Bot sprechen. Da das Ganze natürlich leicht " "missbraucht werden kann, willst du vielleicht supybot.plugins.Anonymous." "requireCapability setzen, sodass nur Nutzer mit dieser Fähigkeit das Plugin " "benutzen können. Für etwas mehr Sicherheit kannst du mit supybot.plugins." "Anonymous.requirePresenceInChannel verlangen, dass der Nutzer in dem Kanal " "sein muss in dem er anonym senden will oder du kannst verlangen, dass der " "Nutzer registriert sein muss indem du supybot.plugins.Anonymous." "requireRegistration setzt." #: plugin.py:98 msgid "You must be in %s to %q in there." msgstr "Du musst in %s sein um %q dort auszuführen." #: plugin.py:102 msgid "I'm lobotomized in %s." msgstr "Ich bin hirnamputiert in %s." #: plugin.py:105 msgid "" "That channel has set its capabilities so as to disallow the use of this " "plugin." msgstr "" "Für den Kanal sind die Fähigkeiten so gesetzt, dass sie das benutzen dieses " "Plugins nicht erlauben." #: plugin.py:108 msgid "" "This command is disabled (supybot.plugins.Anonymous.allowPrivateTarget is " "False)." msgstr "" #: plugin.py:112 #, fuzzy msgid "" " \n" "\n" " Sends to .\n" " " msgstr "" " \n" "\n" "Führt die im aus." #: plugin.py:124 #, fuzzy msgid "" " \n" "\n" " Sends to . Can only be used if\n" " supybot.plugins.Anonymous.allowPrivateTarget is True.\n" " " msgstr "" " \n" "\n" "Sendet an . Kann nur an senden wenn supybot." "plugins.Anonymous.allowPrivateTarget auf True gesetzt ist." #: plugin.py:137 msgid "" " \n" "\n" " Performs in .\n" " " msgstr "" " \n" "\n" "Führt die im aus." #: plugin.py:148 msgid "" " \n" "\n" " Sends the to 's last message.\n" " is typically a smiley or an emoji.\n" "\n" " This may not be supported on the current network, as this\n" " command depends on IRCv3 features.\n" " This is also not supported if\n" " supybot.protocols.irc.experimentalExtensions disabled\n" " (don't enable it unless you know what you are doing).\n" " " msgstr "" #: plugin.py:162 msgid "" "Unable to react, supybot.protocols.irc.experimentalExtensions is disabled." msgstr "" #: plugin.py:167 msgid "Unable to react, the network does not support message-tags." msgstr "" #: plugin.py:172 msgid "" "Unable to react, the network does not allow draft/reply and/or draft/react." msgstr "" #: plugin.py:181 msgid "I couldn't find a message from %s in my history of %s messages." msgstr "" #: plugin.py:189 msgid "Unable to react, %s's last message does not have a message id." msgstr "" #~ msgid "%q cannot be used to send private messages." #~ msgstr "%q kann nicht verwendet werden um private Nachrichten zu versenden." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/locales/fi.po0000644000175000017500000001633014535072470020154 0ustar00valval# Anonymous plugin in Limnoria. # Copyright (C) 2011-2014 Limnoria # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: Supybot Anonymous\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.6.10\n" #: config.py:50 msgid "" "Determines whether\n" " the bot should require people trying to use this plugin to be in the\n" " channel they wish to anonymously send to." msgstr "" "Määrittää vaatiiko \n" "botti ihmisten, jotka yrittävät käyttää tätä lisäosaa, olemaan\n" "kanavalla, jonne he tahtovat lähettää viestin tuntemattomasti." #: config.py:54 msgid "" "Determines whether the bot should require\n" " people trying to use this plugin to be registered." msgstr "" "Määrittää vaatiiko botti\n" "ihmisiä, jotka yrittävät käyttää tätä lisäosaa, olemaan rekisteröityneitä." #: config.py:57 msgid "" "Determines what capability (if any) the bot should\n" " require people trying to use this plugin to have." msgstr "" "Määrittää minkä valtuuden (jos minkään) botti vaatii\n" " ihmisiltä, jotka yrittävät käyttää tätä lisäosaa." #: config.py:60 #, fuzzy msgid "" "Determines whether the bot will allow the\n" " \"tell\" command to be used. If true, the bot will allow the \"tell\"\n" " command to send private messages to other users." msgstr "" "Määrittää salliiko botti \"tell\"-komennon käytön. Jos tämä on True, botti " "sallii \"tell\"-komennon\n" " lähettävän yksityisviestejä toisille käyttäjille." #: plugin.py:45 #, fuzzy msgid "" "\n" " This plugin allows users to act through the bot anonymously. The 'do'\n" " command has the bot perform an anonymous action in a given channel, and\n" " the 'say' command allows other people to speak through the bot. Since\n" " this can be fairly well abused, you might want to set\n" " supybot.plugins.Anonymous.requireCapability so only users with that\n" " capability can use this plugin. For extra security, you can require " "that\n" " the user be *in* the channel they are trying to address anonymously " "with\n" " supybot.plugins.Anonymous.requirePresenceInChannel, or you can require\n" " that the user be registered by setting\n" " supybot.plugins.Anonymous.requireRegistration.\n" "\n" " Example: Proving that you are the owner\n" " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " When you ask for cloak/vhost for your bot, the network operators will\n" " often ask you to prove that you own the bot. You can do this for " "example\n" " with the following method::\n" "\n" " @load Anonymous\n" " @config plugins.anonymous.requirecapability owner\n" " @config plugins.anonymous.allowprivatetarget True\n" " @anonymous say Hi, my owner is :)\n" "\n" " This\n" " * Loads the plugin.\n" " * Makes the plugin require that you are the owner\n" "\n" " * If anyone could send private messages as the bot, they could also\n" " access network services.\n" "\n" " * Allows sending private messages\n" " * Sends message ``Hi, my owner is :)`` to ``operator " "nick``.\n" "\n" " * Note that you won't see the messages that are sent to the bot.\n" "\n" " " msgstr "" "Tämä lisäosa sallii käyttäjien toimia botin kautta tuntemattomasti.\n" " Komento 'do' sallii botin tehdä tuntemattoman toiminnon annetulla " "kanavalla ja\n" " komento 'say' sallii toisten ihmisten puhua botin läpi. Koska\n" " tätä voidaan väärinkäyttää helposti voit tahtoa asettaa asetuksen\n" " supybot.plugins.Anonymous.requireCapability niin, että vain käyttäjät " "tuolla\n" " valtuudella voivat käyttää tätä lisäosaa. Lisäturvallisuuden vuoksi voit " "vaatia, että käyttäjien täytyy *olla* kanavilla joita he yrittävät puhutella " "tuntemattomasti asetuksella supybot.plugins.Anonymous." "requirePresenceInChannel, tai sinä voit vaatia,\n" " että tuo käyttäjä on rekisteröitynyt asetuksella\n" " supybot.plugins.Anonymous.requireRegistration" #: plugin.py:98 msgid "You must be in %s to %q in there." msgstr "Sinun täytyy olla kanavalla %s %q sinne." #: plugin.py:102 msgid "I'm lobotomized in %s." msgstr "Minut on lobotomoitu kanavalla %s." #: plugin.py:105 msgid "" "That channel has set its capabilities so as to disallow the use of this " "plugin." msgstr "Tuo kanava on asettanut valtuudet kieltämään tämän Pluginin käytön." #: plugin.py:108 msgid "" "This command is disabled (supybot.plugins.Anonymous.allowPrivateTarget is " "False)." msgstr "" "Tämä komento on poistettu käytöstä (supybot.plugins.Anonymous." "allowPrivateTarget on False)." #: plugin.py:112 msgid "" " \n" "\n" " Sends to .\n" " " msgstr "" " \n" " Lähettää ." #: plugin.py:124 msgid "" " \n" "\n" " Sends to . Can only be used if\n" " supybot.plugins.Anonymous.allowPrivateTarget is True.\n" " " msgstr "" " \n" "\n" " Lähettää . voidaan lähettää\n" " viestejä vain asetusarvon supybot.plugins.Anonymous.allowPrivateTarget\n" " ollessa True.\n" " " #: plugin.py:137 msgid "" " \n" "\n" " Performs in .\n" " " msgstr "" " \n" "\n" "Suorittaa .\n" " " #: plugin.py:148 msgid "" " \n" "\n" " Sends the to 's last message.\n" " is typically a smiley or an emoji.\n" "\n" " This may not be supported on the current network, as this\n" " command depends on IRCv3 features.\n" " This is also not supported if\n" " supybot.protocols.irc.experimentalExtensions disabled\n" " (don't enable it unless you know what you are doing).\n" " " msgstr "" #: plugin.py:162 msgid "" "Unable to react, supybot.protocols.irc.experimentalExtensions is disabled." msgstr "" #: plugin.py:167 msgid "Unable to react, the network does not support message-tags." msgstr "" #: plugin.py:172 msgid "" "Unable to react, the network does not allow draft/reply and/or draft/react." msgstr "" #: plugin.py:181 msgid "I couldn't find a message from %s in my history of %s messages." msgstr "" #: plugin.py:189 msgid "Unable to react, %s's last message does not have a message id." msgstr "" #~ msgid "%q cannot be used to send private messages." #~ msgstr "%q:ta ei voi käyttää yksityisviestien lähettämiseen." #~ msgid "" #~ " \n" #~ "\n" #~ " Sends to . Can only send to if\n" #~ " supybot.plugins.Anonymous.allowPrivateTarget is True.\n" #~ " " #~ msgstr "" #~ " \n" #~ "\n" #~ " Lähettää . voidaan lähettää " #~ "viestejä vain \n" #~ " asetusarvon supybot.plugins.Anonymous.allowPrivateTarget ollessa " #~ "True.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/locales/fr.po0000644000175000017500000001341714535072470020170 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:50 msgid "" "Determines whether\n" " the bot should require people trying to use this plugin to be in the\n" " channel they wish to anonymously send to." msgstr "" "Détermine si le bot requiérera que les gens soient sur le canal pour y " "envoyer des messages anonymement." #: config.py:54 msgid "" "Determines whether the bot should require\n" " people trying to use this plugin to be registered." msgstr "" "Détermine si le bot requiérera que les personnes utilisant ce plugin soient " "enregistrées." #: config.py:57 msgid "" "Determines what capability (if any) the bot should\n" " require people trying to use this plugin to have." msgstr "" "Détermine quelle capacité (s'il y en a une) le bot requiéra que les gens " "utilisant ce plugin aient." #: config.py:60 #, fuzzy msgid "" "Determines whether the bot will allow the\n" " \"tell\" command to be used. If true, the bot will allow the \"tell\"\n" " command to send private messages to other users." msgstr "" "Détermine si le bot requiérera que les cibles de la commande \"say\" soient " "publiques (c'est à dire des canaux). Si c'est True, le bot autorisera les " "personnes à utiliser la commande \"say\" pour envoyer des messages à " "d'autres utilisateurs en privé." #: plugin.py:45 msgid "" "\n" " This plugin allows users to act through the bot anonymously. The 'do'\n" " command has the bot perform an anonymous action in a given channel, and\n" " the 'say' command allows other people to speak through the bot. Since\n" " this can be fairly well abused, you might want to set\n" " supybot.plugins.Anonymous.requireCapability so only users with that\n" " capability can use this plugin. For extra security, you can require " "that\n" " the user be *in* the channel they are trying to address anonymously " "with\n" " supybot.plugins.Anonymous.requirePresenceInChannel, or you can require\n" " that the user be registered by setting\n" " supybot.plugins.Anonymous.requireRegistration.\n" "\n" " Example: Proving that you are the owner\n" " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " When you ask for cloak/vhost for your bot, the network operators will\n" " often ask you to prove that you own the bot. You can do this for " "example\n" " with the following method::\n" "\n" " @load Anonymous\n" " @config plugins.anonymous.requirecapability owner\n" " @config plugins.anonymous.allowprivatetarget True\n" " @anonymous say Hi, my owner is :)\n" "\n" " This\n" " * Loads the plugin.\n" " * Makes the plugin require that you are the owner\n" "\n" " * If anyone could send private messages as the bot, they could also\n" " access network services.\n" "\n" " * Allows sending private messages\n" " * Sends message ``Hi, my owner is :)`` to ``operator " "nick``.\n" "\n" " * Note that you won't see the messages that are sent to the bot.\n" "\n" " " msgstr "" #: plugin.py:98 msgid "You must be in %s to %q in there." msgstr "Vous devez être sur %s pour y utiliser %q." #: plugin.py:102 msgid "I'm lobotomized in %s." msgstr "Je suis lobotomisé sur %s." #: plugin.py:105 msgid "" "That channel has set its capabilities so as to disallow the use of this " "plugin." msgstr "" "Ce canal a définit ses capacités de façon à désactiver l'utilisation de ce " "plugin." #: plugin.py:108 msgid "" "This command is disabled (supybot.plugins.Anonymous.allowPrivateTarget is " "False)." msgstr "" #: plugin.py:112 #, fuzzy msgid "" " \n" "\n" " Sends to .\n" " " msgstr "" " \n" "\n" "Effectue l' sur le ." #: plugin.py:124 #, fuzzy msgid "" " \n" "\n" " Sends to . Can only be used if\n" " supybot.plugins.Anonymous.allowPrivateTarget is True.\n" " " msgstr "" " \n" "\n" "Envoie le au . Vous ne pouvez envoyer à que si " "supybot.plugins.Anonymous.allowPrivateTarget vaut True." #: plugin.py:137 msgid "" " \n" "\n" " Performs in .\n" " " msgstr "" " \n" "\n" "Effectue l' sur le ." #: plugin.py:148 msgid "" " \n" "\n" " Sends the to 's last message.\n" " is typically a smiley or an emoji.\n" "\n" " This may not be supported on the current network, as this\n" " command depends on IRCv3 features.\n" " This is also not supported if\n" " supybot.protocols.irc.experimentalExtensions disabled\n" " (don't enable it unless you know what you are doing).\n" " " msgstr "" #: plugin.py:162 msgid "" "Unable to react, supybot.protocols.irc.experimentalExtensions is disabled." msgstr "" #: plugin.py:167 msgid "Unable to react, the network does not support message-tags." msgstr "" #: plugin.py:172 msgid "" "Unable to react, the network does not allow draft/reply and/or draft/react." msgstr "" #: plugin.py:181 msgid "I couldn't find a message from %s in my history of %s messages." msgstr "" #: plugin.py:189 msgid "Unable to react, %s's last message does not have a message id." msgstr "" #~ msgid "%q cannot be used to send private messages." #~ msgstr "%q ne peut pas être utilisé pour envoyer des messages privés." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/locales/hu.po0000644000175000017500000001535514535072470020200 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Anonymous\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-21 17:32+0100\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:50 msgid "" "Determines whether\n" " the bot should require people trying to use this plugin to be in the\n" " channel they wish to anonymously send to." msgstr "" "Meghatározza, hogy a bot megkövetelje-e, hogy a bővítményt használni kívánó " "emberek abban a csatornában legyenek, ahová névtelenül szeretnének küldeni." #: config.py:54 msgid "" "Determines whether the bot should require\n" " people trying to use this plugin to be registered." msgstr "" "Meghatározza, hogy a bot megkövetelje-e, hogy a bővítményt használni kívánó " "emberek regisztrálva legyenek." #: config.py:57 msgid "" "Determines what capability (if any) the bot should\n" " require people trying to use this plugin to have." msgstr "" "Meghatározza, hogy milyen képesség (ha van ilyen) legyen szükséges a " "bővítmény használatához." #: config.py:60 #, fuzzy msgid "" "Determines whether the bot will allow the\n" " \"tell\" command to be used. If true, the bot will allow the \"tell\"\n" " command to send private messages to other users." msgstr "" "Meghatározza, hogy a bot megkövetelje-e, hogy a \"say\" parancs célja " "publikus legyen (pl., csatornák). Ha ez True, a bot megengedi az embereknek, " "hogy a \"say\" paranccsal privát üzenetet küldjenek másoknak." #: plugin.py:45 #, fuzzy msgid "" "\n" " This plugin allows users to act through the bot anonymously. The 'do'\n" " command has the bot perform an anonymous action in a given channel, and\n" " the 'say' command allows other people to speak through the bot. Since\n" " this can be fairly well abused, you might want to set\n" " supybot.plugins.Anonymous.requireCapability so only users with that\n" " capability can use this plugin. For extra security, you can require " "that\n" " the user be *in* the channel they are trying to address anonymously " "with\n" " supybot.plugins.Anonymous.requirePresenceInChannel, or you can require\n" " that the user be registered by setting\n" " supybot.plugins.Anonymous.requireRegistration.\n" "\n" " Example: Proving that you are the owner\n" " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " When you ask for cloak/vhost for your bot, the network operators will\n" " often ask you to prove that you own the bot. You can do this for " "example\n" " with the following method::\n" "\n" " @load Anonymous\n" " @config plugins.anonymous.requirecapability owner\n" " @config plugins.anonymous.allowprivatetarget True\n" " @anonymous say Hi, my owner is :)\n" "\n" " This\n" " * Loads the plugin.\n" " * Makes the plugin require that you are the owner\n" "\n" " * If anyone could send private messages as the bot, they could also\n" " access network services.\n" "\n" " * Allows sending private messages\n" " * Sends message ``Hi, my owner is :)`` to ``operator " "nick``.\n" "\n" " * Note that you won't see the messages that are sent to the bot.\n" "\n" " " msgstr "" "Ez a bővítmény megengedi a felhasználóknak, hogy névtelenül cselekedjenek a " "boton keresztül. A 'do' parancs hatására a bot végrehajt egy névtelen " "tevékenységet egy megadott csatornában, és a 'say' parancs megengedi " "másoknak, hogy a boton keresztül beszéljenek. Mivel ezzel elég könnyen " "vissza lehet élni, érdemes beállítani a supybot.plugins.Anonymous." "requireCapability-t, hogy csak az adott képességgel rendelkező felhasználók " "használhassák ezt a bővítményt. Extra biztonságért a supybot.plugins." "Anonymous.requirePresenceInChannel-lel megkövetelheted, hogy a felhasználó " "abban a csatornában legyen, amelyet névtelenül szeretnének megcímezni, vagy " "a supybot.plugins.Anonymous.requireRegistration-nel megkövetelheted, hogy a " "felhasználó regisztálva legyen." #: plugin.py:98 msgid "You must be in %s to %q in there." msgstr "%s-ban kell lenned, hogy a %q parancsot használd ott." #: plugin.py:102 msgid "I'm lobotomized in %s." msgstr "Némítva vagyok %s-ban." #: plugin.py:105 msgid "" "That channel has set its capabilities so as to disallow the use of this " "plugin." msgstr "" "A megadott csatornában nem használhatod a bővítményt a csatorna képességei " "miatt." #: plugin.py:108 msgid "" "This command is disabled (supybot.plugins.Anonymous.allowPrivateTarget is " "False)." msgstr "" #: plugin.py:112 #, fuzzy msgid "" " \n" "\n" " Sends to .\n" " " msgstr "" " \n" "\n" "Végrehajtja -et -ban." #: plugin.py:124 #, fuzzy msgid "" " \n" "\n" " Sends to . Can only be used if\n" " supybot.plugins.Anonymous.allowPrivateTarget is True.\n" " " msgstr "" " \n" "\n" "Elküldi -et -nek. Csak akkor küldhet -nek, ha a " "supybot.plugins.Anonymous.allowPrivateTarget True." #: plugin.py:137 msgid "" " \n" "\n" " Performs in .\n" " " msgstr "" " \n" "\n" "Végrehajtja -et -ban." #: plugin.py:148 msgid "" " \n" "\n" " Sends the to 's last message.\n" " is typically a smiley or an emoji.\n" "\n" " This may not be supported on the current network, as this\n" " command depends on IRCv3 features.\n" " This is also not supported if\n" " supybot.protocols.irc.experimentalExtensions disabled\n" " (don't enable it unless you know what you are doing).\n" " " msgstr "" #: plugin.py:162 msgid "" "Unable to react, supybot.protocols.irc.experimentalExtensions is disabled." msgstr "" #: plugin.py:167 msgid "Unable to react, the network does not support message-tags." msgstr "" #: plugin.py:172 msgid "" "Unable to react, the network does not allow draft/reply and/or draft/react." msgstr "" #: plugin.py:181 msgid "I couldn't find a message from %s in my history of %s messages." msgstr "" #: plugin.py:189 msgid "Unable to react, %s's last message does not have a message id." msgstr "" #~ msgid "%q cannot be used to send private messages." #~ msgstr "A %q parancs nem használható privát üzenetek küldésére." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/locales/it.po0000644000175000017500000001460714535072470020177 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-10 00:13+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\n" " the bot should require people trying to use this plugin to be in the\n" " channel they wish to anonymously send to." msgstr "" "Determina se il bot richieda che gli utenti siano in canale\n" " per inviare messaggi in forma anonima." #: config.py:54 msgid "" "Determines whether the bot should require\n" " people trying to use this plugin to be registered." msgstr "" "Determina se il bot richieda che gli utenti siano registrati\n" " per usare il plugin." #: config.py:57 msgid "" "Determines what capability (if any) the bot should\n" " require people trying to use this plugin to have." msgstr "" "Determina quali capacità (eventuali) debbano\n" " avere gli utenti per utilizzare questo plugin." #: config.py:60 #, fuzzy msgid "" "Determines whether the bot will allow the\n" " \"tell\" command to be used. If true, the bot will allow the \"tell\"\n" " command to send private messages to other users." msgstr "" "Determina se il bot richiederà che le destinazioni del comando \"say\" " "siano\n" " pubbliche (ovvero i canali). Se impostato a True, il bot permetterà\n" " di usare il comando \"say\" per inviare messaggi privati ad altri utenti." #: plugin.py:45 #, fuzzy msgid "" "\n" " This plugin allows users to act through the bot anonymously. The 'do'\n" " command has the bot perform an anonymous action in a given channel, and\n" " the 'say' command allows other people to speak through the bot. Since\n" " this can be fairly well abused, you might want to set\n" " supybot.plugins.Anonymous.requireCapability so only users with that\n" " capability can use this plugin. For extra security, you can require " "that\n" " the user be *in* the channel they are trying to address anonymously " "with\n" " supybot.plugins.Anonymous.requirePresenceInChannel, or you can require\n" " that the user be registered by setting\n" " supybot.plugins.Anonymous.requireRegistration.\n" "\n" " Example: Proving that you are the owner\n" " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " When you ask for cloak/vhost for your bot, the network operators will\n" " often ask you to prove that you own the bot. You can do this for " "example\n" " with the following method::\n" "\n" " @load Anonymous\n" " @config plugins.anonymous.requirecapability owner\n" " @config plugins.anonymous.allowprivatetarget True\n" " @anonymous say Hi, my owner is :)\n" "\n" " This\n" " * Loads the plugin.\n" " * Makes the plugin require that you are the owner\n" "\n" " * If anyone could send private messages as the bot, they could also\n" " access network services.\n" "\n" " * Allows sending private messages\n" " * Sends message ``Hi, my owner is :)`` to ``operator " "nick``.\n" "\n" " * Note that you won't see the messages that are sent to the bot.\n" "\n" " " msgstr "" "Questo plugin permette agli utenti di agire attraverso il bot in modo " "anonimo.\n" " Il comando \"do\" esegue un'azione anonima in un dato canale, mentre " "\"say\"\n" " permette di parlare tramite il bot. Giacché si può facilmente abusarne, " "è\n" " possibile impostare supybot.plugins.Anonymous.requireCapability in modo " "che\n" " solo gli utenti con determinate capacità possano usare il plugin. Per " "una\n" " maggiore sicurezza è ppossibile richiedere con supybot.plugins.Anonymous." "requirePresenceInChannel\n" " che l'utente intenzionato a parlare anonimamente sia in canale; o anche, " "tramite\n" " supybot.plugins.Anonymous.requireRegistration, che l'utente sia " "registrato. " #: plugin.py:98 msgid "You must be in %s to %q in there." msgstr "Devi essere in %s per %q." #: plugin.py:102 msgid "I'm lobotomized in %s." msgstr "In %s sono lobotomizzato." #: plugin.py:105 msgid "" "That channel has set its capabilities so as to disallow the use of this " "plugin." msgstr "" "Questo canale ha le capacità impostate in modo da impedire l'utilizzo di " "questo plugin." #: plugin.py:108 msgid "" "This command is disabled (supybot.plugins.Anonymous.allowPrivateTarget is " "False)." msgstr "" #: plugin.py:112 #, fuzzy msgid "" " \n" "\n" " Sends to .\n" " " msgstr "" " \n" "\n" " Esegue in .\n" " " #: plugin.py:124 #, fuzzy msgid "" " \n" "\n" " Sends to . Can only be used if\n" " supybot.plugins.Anonymous.allowPrivateTarget is True.\n" " " msgstr "" " \n" "\n" " Invia a . Può solo inviare a se\n" " supybot.plugins.Anonymous.allowPrivateTarget è impostato a True.\n" " " #: plugin.py:137 msgid "" " \n" "\n" " Performs in .\n" " " msgstr "" " \n" "\n" " Esegue in .\n" " " #: plugin.py:148 msgid "" " \n" "\n" " Sends the to 's last message.\n" " is typically a smiley or an emoji.\n" "\n" " This may not be supported on the current network, as this\n" " command depends on IRCv3 features.\n" " This is also not supported if\n" " supybot.protocols.irc.experimentalExtensions disabled\n" " (don't enable it unless you know what you are doing).\n" " " msgstr "" #: plugin.py:162 msgid "" "Unable to react, supybot.protocols.irc.experimentalExtensions is disabled." msgstr "" #: plugin.py:167 msgid "Unable to react, the network does not support message-tags." msgstr "" #: plugin.py:172 msgid "" "Unable to react, the network does not allow draft/reply and/or draft/react." msgstr "" #: plugin.py:181 msgid "I couldn't find a message from %s in my history of %s messages." msgstr "" #: plugin.py:189 msgid "Unable to react, %s's last message does not have a message id." msgstr "" #~ msgid "%q cannot be used to send private messages." #~ msgstr "%q non può essere usato per inviare messaggi privati." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/plugin.py0000644000175000017500000002153614535072470017450 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2014, James McCoy # Copyright (c) 2010-2021, 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 functools import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.callbacks as callbacks import supybot.ircutils as ircutils from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Anonymous') class Anonymous(callbacks.Plugin): """ This plugin allows users to act through the bot anonymously. The 'do' command has the bot perform an anonymous action in a given channel, and the 'say' command allows other people to speak through the bot. Since this can be fairly well abused, you might want to set supybot.plugins.Anonymous.requireCapability so only users with that capability can use this plugin. For extra security, you can require that the user be *in* the channel they are trying to address anonymously with supybot.plugins.Anonymous.requirePresenceInChannel, or you can require that the user be registered by setting supybot.plugins.Anonymous.requireRegistration. Example: Proving that you are the owner ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When you ask for cloak/vhost for your bot, the network operators will often ask you to prove that you own the bot. You can do this for example with the following method:: @load Anonymous @config plugins.anonymous.requirecapability owner @config plugins.anonymous.allowprivatetarget True @anonymous say Hi, my owner is :) This * Loads the plugin. * Makes the plugin require that you are the owner * If anyone could send private messages as the bot, they could also access network services. * Allows sending private messages * Sends message ``Hi, my owner is :)`` to ``operator nick``. * Note that you won't see the messages that are sent to the bot. """ def _preCheck(self, irc, msg, target, action): if self.registryValue('requireRegistration', target, irc.network): try: foo = ircdb.users.getUser(msg.prefix) except KeyError: irc.errorNotRegistered(Raise=True) capability = self.registryValue('requireCapability', target, irc.network) if capability: if not ircdb.checkCapability(msg.prefix, capability): irc.errorNoCapability(capability, Raise=True) if action != 'tell': require_presence = self.registryValue('requirePresenceInChannel', target, irc.network) if require_presence and \ msg.nick not in irc.state.channels[target].users: irc.error(format(_('You must be in %s to %q in there.'), target, action), Raise=True) c = ircdb.channels.getChannel(target) if c.lobotomized: irc.error(format(_('I\'m lobotomized in %s.'), target), Raise=True) if not c._checkCapability(self.name()): irc.error(_('That channel has set its capabilities so as to ' 'disallow the use of this plugin.'), Raise=True) elif not self.registryValue('allowPrivateTarget'): irc.error(_('This command is disabled (supybot.plugins.Anonymous.' 'allowPrivateTarget is False).'), Raise=True) def say(self, irc, msg, args, target, text): """ Sends to . """ self._preCheck(irc, msg, target, 'say') self.log.info('Saying %q in %s due to %s.', text, target, msg.prefix) irc.queueMsg(ircmsgs.privmsg(target, text)) irc.noReply() say = wrap(say, ['inChannel', 'text']) def tell(self, irc, msg, args, target, text): """ Sends to . Can only be used if supybot.plugins.Anonymous.allowPrivateTarget is True. """ self._preCheck(irc, msg, target, 'tell') self.log.info('Telling %q to %s due to %s.', text, target, msg.prefix) irc.queueMsg(ircmsgs.privmsg(target, text)) irc.noReply() tell = wrap(tell, ['nick', 'text']) def do(self, irc, msg, args, channel, text): """ Performs in . """ self._preCheck(irc, msg, channel, 'do') self.log.info('Performing %q in %s due to %s.', text, channel, msg.prefix) irc.reply(text, action=True, to=channel) do = wrap(do, ['inChannel', 'text']) def react(self, irc, msg, args, channel, reaction, nick): """ Sends the to 's last message. is typically a smiley or an emoji. This may not be supported on the current network, as this command depends on IRCv3 features. This is also not supported if supybot.protocols.irc.experimentalExtensions disabled (don't enable it unless you know what you are doing). """ self._preCheck(irc, msg, channel, 'react') if not conf.supybot.protocols.irc.experimentalExtensions(): irc.error(_('Unable to react, ' 'supybot.protocols.irc.experimentalExtensions is ' 'disabled.'), Raise=True) if not 'message-tags' in irc.state.capabilities_ack: irc.error(_('Unable to react, the network does not support ' 'message-tags.'), Raise=True) if irc.state.getClientTagDenied('draft/reply') \ or irc.state.getClientTagDenied('draft/react'): irc.error(_('Unable to react, the network does not allow ' 'draft/reply and/or draft/react.'), Raise=True) iterable = filter(functools.partial(self._validLastMsg, irc), reversed(irc.state.history)) for react_to_msg in iterable: if react_to_msg.nick == nick: break else: irc.error(_('I couldn\'t find a message from %s in ' 'my history of %s messages.') % (nick, len(irc.state.history)), Raise=True) react_to_msgid = react_to_msg.server_tags.get('msgid') if not react_to_msgid: irc.error(_('Unable to react, %s\'s last message does not have ' 'a message id.') % nick, Raise=True) self.log.info('Reacting with %q in %s due to %s.', reaction, channel, msg.prefix) reaction_msg = ircmsgs.IrcMsg(command='TAGMSG', args=(channel,), server_tags={'+draft/reply': react_to_msgid, '+draft/react': reaction}) irc.queueMsg(reaction_msg) react = wrap( react, ['inChannel', 'somethingWithoutSpaces', 'nickInChannel']) def _validLastMsg(self, irc, msg): return msg.prefix and \ msg.command == 'PRIVMSG' and \ msg.channel Anonymous = internationalizeDocstring(Anonymous) Class = Anonymous # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Anonymous/test.py0000644000175000017500000001534214535072470017127 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2014, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf from supybot.test import * class AnonymousTestCase(ChannelPluginTestCase): plugins = ('Anonymous',) def testSay(self): self.assertError('anonymous say %s I love you!' % self.channel) with conf.supybot.plugins.Anonymous.requireRegistration.context(False): m = self.assertNotError('anonymous say %s foo!' % self.channel) self.assertEqual(m.args[1], 'foo!') def testTell(self): self.assertError('anonymous tell %s I love you!' % self.nick) with conf.supybot.plugins.Anonymous.requireRegistration.context(False): self.assertError('anonymous tell %s foo!' % self.channel) with conf.supybot.plugins.Anonymous.allowPrivateTarget.context(True): m = self.assertNotError('anonymous tell %s foo!' % self.nick) self.assertEqual(m.args[1], 'foo!') def testAction(self): m = self.assertError('anonymous do %s loves you!' % self.channel) with conf.supybot.plugins.Anonymous.requireRegistration.context(False): m = self.assertNotError('anonymous do %s loves you!'%self.channel) self.assertEqual(m.args, ircmsgs.action(self.channel, 'loves you!').args) def testReact(self): with self.subTest('nick not in channel'): self.assertRegexp('anonymous react :) blah', 'blah is not in %s' % self.channel) self.irc.feedMsg(ircmsgs.IrcMsg( ':blah!foo@example JOIN %s' % self.channel)) with self.subTest('require registration'): self.assertRegexp('anonymous react :) blah', 'must be registered') self.assertIsNone(self.irc.takeMsg()) with conf.supybot.plugins.Anonymous.requireRegistration.context(False): with self.subTest('experimental extensions disabled'): self.assertRegexp('anonymous react :) blah', 'protocols.irc.experimentalExtensions is disabled') self.assertIsNone(self.irc.takeMsg()) with conf.supybot.plugins.Anonymous.requireRegistration.context(False), \ conf.supybot.protocols.irc.experimentalExtensions.context(True): with self.subTest('server support missing'): self.assertRegexp('anonymous react :) blah', 'network does not support message-tags') self.assertIsNone(self.irc.takeMsg()) self.irc.state.capabilities_ack.add('message-tags') with self.subTest('no message from the target'): self.assertRegexp('anonymous react :) blah', 'couldn\'t find a message') self.assertIsNone(self.irc.takeMsg()) self.irc.feedMsg(ircmsgs.IrcMsg( ':blah!foo@example PRIVMSG %s :hello' % self.channel)) with self.subTest('original message not tagged with msgid'): self.assertRegexp('anonymous react :) blah', 'not have a message id') self.assertIsNone(self.irc.takeMsg()) self.irc.feedMsg(ircmsgs.IrcMsg( '@msgid=123 :blah!foo@example PRIVMSG %s :hello' % self.channel)) # Works with self.subTest('canonical working case'): m = self.getMsg('anonymous react :) blah') self.assertEqual(m, ircmsgs.IrcMsg( '@+draft/reply=123;+draft/react=:) TAGMSG %s' % self.channel)) def testReactClienttagdeny(self): self.irc.feedMsg(ircmsgs.IrcMsg( ':blah!foo@example JOIN %s' % self.channel)) self.irc.feedMsg(ircmsgs.IrcMsg( '@msgid=123 :blah!foo@example PRIVMSG %s :hello' % self.channel)) self.irc.state.capabilities_ack.add('message-tags') with conf.supybot.plugins.Anonymous.requireRegistration.context(False), \ conf.supybot.protocols.irc.experimentalExtensions.context(True): # Works self.irc.state.supported['CLIENTTAGDENY'] = 'foo,bar' for value in ('draft/reply', 'draft/react', '*,-draft/reply', '*,draft/react'): self.irc.state.supported['CLIENTTAGDENY'] = value with self.subTest('denied by CLIENTTAGDENY=%s' % value): self.assertRegexp('anonymous react :) blah', 'draft/reply and/or draft/react') self.assertIsNone(self.irc.takeMsg()) # Works for value in ('foo,bar', '*,-draft/reply,-draft/react'): self.irc.state.supported['CLIENTTAGDENY'] = value with self.subTest('allowed by CLIENTTAGDENY=%s' % value): m = self.getMsg('anonymous react :) blah') self.assertEqual(m, ircmsgs.IrcMsg( '@+draft/reply=123;+draft/react=:) TAGMSG %s' % self.channel)) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/AutoMode/0000755000175000017500000000000014535072535015320 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/__init__.py0000644000175000017500000000521114535072470017426 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Automatically ops, voices, or halfops, or bans people when they join a channel, according to their capabilities. If you want your bot automatically op users when they join your channel, this is the plugin to load. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/config.py0000644000175000017500000001065114535072470017140 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('AutoMode') 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('AutoMode', True) AutoMode = conf.registerPlugin('AutoMode') conf.registerChannelValue(AutoMode, 'enable', registry.Boolean(True, _("""Determines whether this plugin is enabled. """))) conf.registerGlobalValue(AutoMode, 'owner', registry.Boolean(False, _("""Determines whether this plugin will automode owners even if they don't have op/halfop/voice/whatever capability."""))) conf.registerChannelValue(AutoMode, 'alternativeCapabilities', registry.Boolean(True, _("""Determines whether the bot will check for 'alternative capabilities' (ie. autoop, autohalfop, autovoice) in addition to/instead of classic ones."""))) conf.registerChannelValue(AutoMode, 'fallthrough', registry.Boolean(True, _("""Determines whether the bot will "fall through" to halfop/voicing when auto-opping is turned off but auto-halfopping/voicing are turned on."""))) conf.registerChannelValue(AutoMode, 'op', registry.Boolean(False, _("""Determines whether the bot will automatically op people with the ,op capability when they join the channel. """))) conf.registerChannelValue(AutoMode, 'halfop', registry.Boolean(False, _("""Determines whether the bot will automatically halfop people with the ,halfop capability when they join the channel."""))) conf.registerChannelValue(AutoMode, 'voice', registry.Boolean(False, _("""Determines whether the bot will automatically voice people with the ,voice capability when they join the channel."""))) conf.registerChannelValue(AutoMode, 'ban', registry.Boolean(True, _("""Determines whether the bot will automatically ban people who join the channel and are on the banlist."""))) conf.registerChannelValue(AutoMode.ban, 'period', registry.PositiveInteger(86400, _("""Determines how many seconds the bot will automatically ban a person when banning."""))) conf.registerChannelValue(AutoMode, 'delay', registry.Integer(0, _("""Determines how many seconds the bot will wait before applying a mode. Has no effect on bans."""))) conf.registerChannelValue(AutoMode, 'extra', registry.SpaceSeparatedListOfStrings([], _("""Extra modes that will be applied to a user. Example syntax: user1+o-v user2+v user3-v"""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/AutoMode/locales/0000755000175000017500000000000014535072535016742 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/locales/de.po0000644000175000017500000001015214535072470017667 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:38+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: German\n" "X-Poedit-Country: GERMANY\n" #: config.py:47 msgid "" "Determines whether this plugin is enabled.\n" " " msgstr "Legt fest ob das Plugin aktiv ist." #: config.py:50 msgid "" "Determines whether this plugin will automode\n" " owners even if they don't have op/halfop/voice/whatever capability." msgstr "" "Legt fest ob das Plugin Besitzern automatisch den Modus zuweist, obwohl sie " "nicht die op/halfop/voice/wasauchimmer Fähigkeit besitzen." #: config.py:53 msgid "" "Determines whether the bot will\n" " check for 'alternative capabilities' (ie. autoop, autohalfop,\n" " autovoice) in addition to/instead of classic ones." msgstr "" #: config.py:57 #, fuzzy msgid "" "Determines whether the bot will \"fall\n" " through\" to halfop/voicing when auto-opping is turned off but\n" " auto-halfopping/voicing are turned on." msgstr "" "Legt fest ob der Bot \"zurückfällt\" auf Halboperator/Sprechrechte wenn " "automatischer Operator abgeschaltet ist, aber automatische Halboperator/" "Sprecherechte angeschaltet sind." #: config.py:61 msgid "" "Determines whether the bot will automatically\n" " op people with the ,op capability when they join the channel.\n" " " msgstr "" "Legt fest ob der Bot Personen, mit der ,op Fähigkeit, automatisch " "Operatorrechte zuweisen soll, wenn sie den Kanal betreten." #: config.py:65 msgid "" "Determines whether the bot will automatically\n" " halfop people with the ,halfop capability when they join the\n" " channel." msgstr "" "Legt fest ob der Bot Personen, mit der ,halfop Fähigkeit, automatisch " "Halboperatorrechte zuweisen soll, wenn sie den Kanal betreten." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " voice people with the ,voice capability when they join the\n" " channel." msgstr "" "Legt fest ob der Bot Personen, mit der ,voice Fähigkeit, automatisch " "Sprechrechte zuweisen soll, wenn sie den Kanal betreten." #: config.py:73 msgid "" "Determines whether the bot will automatically\n" " ban people who join the channel and are on the banlist." msgstr "" "Legt fest ob der Bot automatisch Personen bannen soll die auf der Banliste " "stehen." #: config.py:76 msgid "" "Determines how many seconds the bot\n" " will automatically ban a person when banning." msgstr "Legt fest wieviele Sekunden der Bot Personen automatisch bannt." #: config.py:80 #, fuzzy msgid "" "Determines how many seconds the bot will wait\n" " before applying a mode. Has no effect on bans." msgstr "Legt fest wieviele Sekunden der Bot Personen automatisch bannt." #: config.py:84 msgid "" "Extra modes that will be\n" " applied to a user. Example syntax: user1+o-v user2+v user3-v" msgstr "" #: plugin.py:49 msgid "" "\n" " This plugin, when configured, allows the bot to automatically set modes\n" " on users when they join.\n" "\n" " * if ``plugins.automode.op`` is set to ``True``, users with the\n" " ``#channel,op`` capability are opped when they join.\n" " * if ``plugins.automode.halfop`` is set to ``True``, users with the\n" " ``#channel,halfop`` are halfopped when they join.\n" " * if ``plugins.automode.voice`` is set to ``True``, users with the\n" " ``#channel,voice`` are voiced when they join.\n" "\n" " This plugin also kbans people on ``@channel ban list``\n" " (``config plugins.automode.ban``) when they join and if moding users " "with\n" " lower capability is enabled, that is also applied to users with higher\n" " capability (``config plugins.automode.fallthrough``).\n" "\n" " " msgstr "" #: plugin.py:97 msgid "" "Determines whether or not a mode has already\n" " been applied." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/locales/fi.po0000644000175000017500000001170214535072470017677 0ustar00valval# AutoMode plugin in Limnoria. # Copyright (C) 2011, 2012 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Supybot AutoMode\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11:38+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: suomi <>\n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" "X-Generator: Poedit 1.6.10\n" #: config.py:47 msgid "" "Determines whether this plugin is enabled.\n" " " msgstr "Määrittää onko tämä lisäosa käytössä." #: config.py:50 msgid "" "Determines whether this plugin will automode\n" " owners even if they don't have op/halfop/voice/whatever capability." msgstr "" "Määrittää vaihtaako botti omistajien tilaa automaattisesti, vaikka heillä ei " "olisi\n" " op/halfop/voice/ihansama valtuutta." #: config.py:53 msgid "" "Determines whether the bot will\n" " check for 'alternative capabilities' (ie. autoop, autohalfop,\n" " autovoice) in addition to/instead of classic ones." msgstr "" "Määrittää tarkistaako botti 'vaihtoehtoisten valtuuksien' varalta (esim. " "autoop,\n" " autohalfop, autovoice) klassisten lisäksi/sijaan." #: config.py:57 msgid "" "Determines whether the bot will \"fall\n" " through\" to halfop/voicing when auto-opping is turned off but\n" " auto-halfopping/voicing are turned on." msgstr "" "Määrittää \"alentuuko\" botti halfoppaamiseen/voicen antamiseen kun " "automaattinen\n" " oprtaattori aseman antaminen on poistettu käytöstä, mutta automaattinen\n" " puolioperaattori aseman/äänen antaminen on käytössä." #: config.py:61 msgid "" "Determines whether the bot will automatically\n" " op people with the ,op capability when they join the channel.\n" " " msgstr "" "Määrittää oppaako botti\n" " ihmiset, joilla on #,op valtuus automaattisesti, kun he liittyvät " "kanavalle." #: config.py:65 msgid "" "Determines whether the bot will automatically\n" " halfop people with the ,halfop capability when they join the\n" " channel." msgstr "" "Määrittää antaako botti puolioperaattorin aseman automaattisesti, kun " "ihmiset, joilla on #,halfop valtuus liittyvät kanavalle." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " voice people with the ,voice capability when they join the\n" " channel." msgstr "" "Määrittää antaako botti automaattisesti äänen ihmisille, joilla on #," "voice\n" " valtuus heidän liittyessään kanavalle." #: config.py:73 msgid "" "Determines whether the bot will automatically\n" " ban people who join the channel and are on the banlist." msgstr "" "Määrittää antaako botti porttikiellon ihmisille,\n" " jotka liittyvät kanavalle ja ovat porttikieltolistalla." #: config.py:76 msgid "" "Determines how many seconds the bot\n" " will automatically ban a person when banning." msgstr "" "Määrittää kuinka moneksi sekuntiksi botti antaa porttikiellon henkilöle, " "antaessaan\n" " porttikieltoa automaattisesti." #: config.py:80 msgid "" "Determines how many seconds the bot will wait\n" " before applying a mode. Has no effect on bans." msgstr "" "Määrittää kuinka monta sekuntia botti odottaa ennen, kuin asettaa tilan. " "Tällä ei ole\n" " vaikutusta porttikieltoihin." #: config.py:84 msgid "" "Extra modes that will be\n" " applied to a user. Example syntax: user1+o-v user2+v user3-v" msgstr "" "Ylimääräiset tilat, jotka asetetaan käyttäjään.\n" " Esimerkki syntaksi: käyttäjä1+o-v käyttäjä2+v käyttäjä3-v" #: plugin.py:49 msgid "" "\n" " This plugin, when configured, allows the bot to automatically set modes\n" " on users when they join.\n" "\n" " * if ``plugins.automode.op`` is set to ``True``, users with the\n" " ``#channel,op`` capability are opped when they join.\n" " * if ``plugins.automode.halfop`` is set to ``True``, users with the\n" " ``#channel,halfop`` are halfopped when they join.\n" " * if ``plugins.automode.voice`` is set to ``True``, users with the\n" " ``#channel,voice`` are voiced when they join.\n" "\n" " This plugin also kbans people on ``@channel ban list``\n" " (``config plugins.automode.ban``) when they join and if moding users " "with\n" " lower capability is enabled, that is also applied to users with higher\n" " capability (``config plugins.automode.fallthrough``).\n" "\n" " " msgstr "" #: plugin.py:97 msgid "" "Determines whether or not a mode has already\n" " been applied." msgstr "Määrittää asettaako botti tilan vai eikö, jos tila se on jo asetettu." #, fuzzy #~ msgid "" #~ "This plugin, when configured, allows the bot to automatically set modes\n" #~ " on users when they join." #~ msgstr "" #~ "Määritettynä tämä plugin voi asettaa tiloja kanavalle liittyjiin " #~ "automaattisesti." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/locales/fr.po0000644000175000017500000001064014535072470017710 0ustar00valval# Valentin Lorentz , 2012. msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-01-21 19:33+0100\n" "Last-Translator: \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: config.py:47 msgid "" "Determines whether this plugin is enabled.\n" " " msgstr "Détermine si ce plugin est activé." #: config.py:50 msgid "" "Determines whether this plugin will automode\n" " owners even if they don't have op/halfop/voice/whatever capability." msgstr "" "Détermine si ce plugin mettra des modes automatiques sur les owners, même si " "ils n'ont pas la capacité op/halfop/voice/..." #: config.py:53 msgid "" "Determines whether the bot will\n" " check for 'alternative capabilities' (ie. autoop, autohalfop,\n" " autovoice) in addition to/instead of classic ones." msgstr "" "Détermine si le bot vérifiera les « capacités alternatives » (ie. autoop, " "autohalfop, autovoice) en plus/à la place des capacités classiques." #: config.py:57 msgid "" "Determines whether the bot will \"fall\n" " through\" to halfop/voicing when auto-opping is turned off but\n" " auto-halfopping/voicing are turned on." msgstr "" "Détermine si le bot ne halfopera/voicera pas lorsque l'auto-op est " "désactivé, même si l'auto-halfopvoice est activé." #: config.py:61 msgid "" "Determines whether the bot will automatically\n" " op people with the ,op capability when they join the channel.\n" " " msgstr "" "Détermine si le bot opera automatiquement les gens qui ont la capacité " ",op lorsqu'ils rejoignent le canal." #: config.py:65 msgid "" "Determines whether the bot will automatically\n" " halfop people with the ,halfop capability when they join the\n" " channel." msgstr "" "Détermine si le bot halfopera les gens qui ont la capacité ,halfop " "lorsqu'ils rejoignent le canal." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " voice people with the ,voice capability when they join the\n" " channel." msgstr "" "Détermine si le bot voicera automatiquement les gens avec la capacité " ",voice lorsqu'ils rejoingent le canal." #: config.py:73 msgid "" "Determines whether the bot will automatically\n" " ban people who join the channel and are on the banlist." msgstr "" "Détermine si le bot bannira automatiquement les personnes qui rejoignent le " "canal et qui sont sur la liste de bannissement." #: config.py:76 msgid "" "Determines how many seconds the bot\n" " will automatically ban a person when banning." msgstr "" "Détermine combien de secondes durera le bannissement que le bot posera sur " "une personne." #: config.py:80 msgid "" "Determines how many seconds the bot will wait\n" " before applying a mode. Has no effect on bans." msgstr "" "Détermine combien de secondes le bot attendra avant d'appliquer un mode. " "Cela n'a aucun effet sur les bannissements." #: config.py:84 msgid "" "Extra modes that will be\n" " applied to a user. Example syntax: user1+o-v user2+v user3-v" msgstr "" "Des modes supplémentaires à appliquer à un utilisateur. Par exemple : " "user1+o-v user2+v user3-v" #: plugin.py:49 msgid "" "\n" " This plugin, when configured, allows the bot to automatically set modes\n" " on users when they join.\n" "\n" " * if ``plugins.automode.op`` is set to ``True``, users with the\n" " ``#channel,op`` capability are opped when they join.\n" " * if ``plugins.automode.halfop`` is set to ``True``, users with the\n" " ``#channel,halfop`` are halfopped when they join.\n" " * if ``plugins.automode.voice`` is set to ``True``, users with the\n" " ``#channel,voice`` are voiced when they join.\n" "\n" " This plugin also kbans people on ``@channel ban list``\n" " (``config plugins.automode.ban``) when they join and if moding users " "with\n" " lower capability is enabled, that is also applied to users with higher\n" " capability (``config plugins.automode.fallthrough``).\n" "\n" " " msgstr "" #: plugin.py:97 msgid "" "Determines whether or not a mode has already\n" " been applied." msgstr "Détermine si un mode a déjà été appliqué ou non." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/locales/it.po0000644000175000017500000001004314535072470017712 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-23 19:29+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:47 msgid "" "Determines whether this plugin is enabled.\n" " " msgstr "" "Determina se il plugin è abilitato.\n" " " #: config.py:50 msgid "" "Determines whether this plugin will automode\n" " owners even if they don't have op/halfop/voice/whatever capability." msgstr "" "Determina se il plugin darà automaticamente il mode ai proprietari.\n" " anche se non hanno la capacità op, halfop, voice, ecc..." #: config.py:53 msgid "" "Determines whether the bot will\n" " check for 'alternative capabilities' (ie. autoop, autohalfop,\n" " autovoice) in addition to/instead of classic ones." msgstr "" #: config.py:57 msgid "" "Determines whether the bot will \"fall\n" " through\" to halfop/voicing when auto-opping is turned off but\n" " auto-halfopping/voicing are turned on." msgstr "" "Determina se il the bot non darà l'halfop o il voice\n" " quando l'op automatico è disabilitato ma l'halfop o il voice\n" " automatico sono attivati." #: config.py:61 msgid "" "Determines whether the bot will automatically\n" " op people with the ,op capability when they join the channel.\n" " " msgstr "" "Determina se il bot darà automaticamente l'op agli utenti che hanno\n" " la capacità ,op quando entrano in canale.\n" " " #: config.py:65 msgid "" "Determines whether the bot will automatically\n" " halfop people with the ,halfop capability when they join the\n" " channel." msgstr "" "Determina se il bot darà automaticamente l'halfop agli utenti che hanno\n" " la capacità ,halfop quando entrano in canale.\n" " " #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " voice people with the ,voice capability when they join the\n" " channel." msgstr "" "Determina se il bot darà automaticamente il voice agli utenti che hanno\n" " la capacità ,voice quando entrano in canale.\n" " " #: config.py:73 msgid "" "Determines whether the bot will automatically\n" " ban people who join the channel and are on the banlist." msgstr "" "Determina se il bot bannerà automaticamente gli utenti che\n" " entrano in canale e sono nella lista dei ban.\n" " " #: config.py:76 msgid "" "Determines how many seconds the bot\n" " will automatically ban a person when banning." msgstr "Determina quanti secondi durerà il ban applicato a un utente." #: config.py:80 msgid "" "Determines how many seconds the bot will wait\n" " before applying a mode. Has no effect on bans." msgstr "" "Determina quanti secondi aspetterà il bot prima di applicare un mode.\n" " Non ha effetto sui ban." #: config.py:84 msgid "" "Extra modes that will be\n" " applied to a user. Example syntax: user1+o-v user2+v user3-v" msgstr "" #: plugin.py:49 msgid "" "\n" " This plugin, when configured, allows the bot to automatically set modes\n" " on users when they join.\n" "\n" " * if ``plugins.automode.op`` is set to ``True``, users with the\n" " ``#channel,op`` capability are opped when they join.\n" " * if ``plugins.automode.halfop`` is set to ``True``, users with the\n" " ``#channel,halfop`` are halfopped when they join.\n" " * if ``plugins.automode.voice`` is set to ``True``, users with the\n" " ``#channel,voice`` are voiced when they join.\n" "\n" " This plugin also kbans people on ``@channel ban list``\n" " (``config plugins.automode.ban``) when they join and if moding users " "with\n" " lower capability is enabled, that is also applied to users with higher\n" " capability (``config plugins.automode.fallthrough``).\n" "\n" " " msgstr "" #: plugin.py:97 msgid "" "Determines whether or not a mode has already\n" " been applied." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/plugin.py0000644000175000017500000001753514535072470017201 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import time import supybot.log as log import supybot.conf as conf import supybot.ircdb as ircdb import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.schedule as schedule import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('AutoMode') class Continue(Exception): pass # Used below, look in the "do" function nested in doJoin. class AutoMode(callbacks.Plugin): """ This plugin, when configured, allows the bot to automatically set modes on users when they join. * if ``plugins.automode.op`` is set to ``True``, users with the ``#channel,op`` capability are opped when they join. * if ``plugins.automode.halfop`` is set to ``True``, users with the ``#channel,halfop`` are halfopped when they join. * if ``plugins.automode.voice`` is set to ``True``, users with the ``#channel,voice`` are voiced when they join. This plugin also kbans people on ``@channel ban list`` (``config plugins.automode.ban``) when they join and if moding users with lower capability is enabled, that is also applied to users with higher capability (``config plugins.automode.fallthrough``). """ def doJoin(self, irc, msg): channel = msg.channel network = irc.network if ircutils.strEqual(irc.nick, msg.nick): return if not self.registryValue('enable', channel, network): return fallthrough = self.registryValue('fallthrough', channel, network) def do(type): cap = ircdb.makeChannelCapability(channel, type) cap_auto = ircdb.makeChannelCapability(channel, 'auto'+type) try: apply_mode = ircdb.checkCapability(msg.prefix, cap, ignoreOwner=not self.registryValue('owner'), ignoreChannelOp=True, ignoreDefaultAllow=True) except KeyError: apply_mode = False if self.registryValue('alternativeCapabilities', channel, network): try: override = ircdb.checkCapability(msg.prefix, cap_auto, ignoreOwner=not self.registryValue('owner'), ignoreChannelOp=True, ignoreDefaultAllow=True) except KeyError: override = False else: override = False if apply_mode or override: if override or self.registryValue(type, channel, network): self.log.info('Scheduling auto-%s of %s in %s @ %s.', type, msg.prefix, channel, network) def dismiss(): """Determines whether or not a mode has already been applied.""" l = getattr(irc.state.channels[channel], type+'s') return (msg.nick in l) msgmaker = getattr(ircmsgs, type) schedule_msg(msgmaker(channel, msg.nick), dismiss) raise Continue # Even if fallthrough, let's only do one. elif not fallthrough: self.log.debug('%s has %s, but supybot.plugins.AutoMode.%s' ' is not enabled in %s @ %s, refusing to ' 'fall through.', msg.prefix, cap, type, channel, network) raise Continue def schedule_msg(msg, dismiss): def f(): if not dismiss(): irc.queueMsg(msg) else: self.log.info('Dismissing auto-mode for %s.', msg.args[2]) delay = self.registryValue('delay', channel, network) if delay: schedule.addEvent(f, time.time() + delay) else: f() def extra_modes(): try: user = ircdb.users.getUser(ircdb.users.getUserId(msg.prefix)) except KeyError: return pattern = re.compile(r'-|\+') for item in self.registryValue('extra', channel, network): try: username, modes = pattern.split(item, maxsplit=1) modes = item[len(username)] + modes except ValueError: # No - or + in item log.error(('%r is not a valid item for ' 'supybot.plugins.AutoMode.extra') % item) continue if username != user.name: continue else: self.log.info('Scheduling auto-modes %s of %s in %s @ %s.', modes, msg.prefix, channel, network) modes = [modes] + \ ([msg.nick]*len(pattern.sub('', modes))) schedule_msg(ircmsgs.mode(channel, modes), lambda :False) break try: do('op') if 'h' in irc.state.supported.get('prefix', ''): do('halfop') except Continue: return finally: extra_modes() c = ircdb.channels.getChannel(channel) if c.checkBan(msg.prefix) and self.registryValue('ban', channel, network): period = self.registryValue('ban.period', channel, network) if period: def unban(): try: if msg.prefix in irc.state.channels[channel].bans: irc.queueMsg(ircmsgs.unban(channel, msg.prefix)) except KeyError: # We're not in the channel anymore. pass schedule.addEvent(unban, time.time()+period) banmask =conf.supybot.protocols.irc.banmask.makeBanmask(msg.prefix) irc.queueMsg(ircmsgs.ban(channel, banmask)) irc.queueMsg(ircmsgs.kick(channel, msg.nick)) try: do('voice') except Continue: return Class = AutoMode # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/AutoMode/test.py0000644000175000017500000000337314535072470016655 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class AutoModeTestCase(PluginTestCase): plugins = ('AutoMode',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Autocomplete/0000755000175000017500000000000014535072535016244 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Autocomplete/__init__.py0000644000175000017500000000560214535072470020356 0ustar00valval### # Copyright (c) 2020-2021, 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. ### """ Autocomplete: Provides command autocompletion for IRC clients that support it. This plugin implements an early draft of the IRCv3 autocompletion client tags. As this is not yet a released specification, it does nothing unless ``supybot.protocols.irc.experimentalExtensions`` is set to True (keep it set to False unless you know what you are doing). If you are interested in this feature, please contribute to `the discussion `_ """ import sys import supybot from supybot import world # Use this for the version of this plugin. __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 importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Autocomplete/config.py0000644000175000017500000000537014535072470020066 0ustar00valval### # Copyright (c) 2020-2021, 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 import conf, registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization("Autocomplete") 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("Autocomplete", True) Autocomplete = conf.registerPlugin("Autocomplete") # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Autocomplete, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue( Autocomplete, "enabled", registry.Boolean( False, _( """Whether the bot should reply to autocomplete requests from clients.""" ), ), ) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Autocomplete/plugin.py0000644000175000017500000001401614535072470020114 0ustar00valval### # Copyright (c) 2020-2021, 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 import conf, ircutils, ircmsgs, callbacks from supybot.i18n import PluginInternationalization _ = PluginInternationalization("Autocomplete") REQUEST_TAG = "+draft/autocomplete-request" RESPONSE_TAG = "+draft/autocomplete-response" def _commonPrefix(L): """Takes a list of lists, and returns their longest common prefix.""" assert L if len(L) == 1: return L[0] for n in range(1, max(map(len, L)) + 1): prefix = L[0][:n] for item in L[1:]: if prefix != item[:n]: return prefix[0:-1] assert False def _getAutocompleteResponse(irc, msg, payload): """Returns the value of the +draft/autocomplete-response tag for the given +draft/autocomplete-request payload.""" tokens = callbacks.tokenize( payload, channel=msg.channel, network=irc.network ) normalized_payload = " ".join(tokens) candidate_commands = _getCandidates(irc, normalized_payload) if len(candidate_commands) == 0: # No result return None elif len(candidate_commands) == 1: # One result, return it directly commands = candidate_commands else: # Multiple results, return only the longest common prefix + one word tokenized_candidates = [ callbacks.tokenize(c, channel=msg.channel, network=irc.network) for c in candidate_commands ] common_prefix = _commonPrefix(tokenized_candidates) words_after_prefix = { candidate[len(common_prefix)] for candidate in tokenized_candidates } commands = [ " ".join(common_prefix + [word]) for word in words_after_prefix ] # strip what the user already typed assert all(command.startswith(normalized_payload) for command in commands) normalized_payload_length = len(normalized_payload) response_items = [ command[normalized_payload_length:] for command in commands ] return "\t".join(sorted(response_items)) def _getCandidates(irc, normalized_payload): """Returns a list of commands starting with the normalized_payload.""" candidates = set() for cb in irc.callbacks: cb_commands = cb.listCommands() # copy them with the plugin name (optional when calling a command) # at the beginning plugin_name = cb.canonicalName() cb_commands += [plugin_name + " " + command for command in cb_commands] candidates |= { command for command in cb_commands if command.startswith(normalized_payload) } return candidates class Autocomplete(callbacks.Plugin): """Provides command completion for IRC clients that support it.""" def _enabled(self, irc, msg): return ( conf.supybot.protocols.irc.experimentalExtensions() and self.registryValue("enabled", msg.channel, irc.network) ) def doTagmsg(self, irc, msg): if REQUEST_TAG not in msg.server_tags: return if "msgid" not in msg.server_tags: return if not self._enabled(irc, msg): return msgid = msg.server_tags["msgid"] text = msg.server_tags[REQUEST_TAG] # using callbacks._addressed instead of callbacks.addressed, as # callbacks.addressed would tag the m payload = callbacks._addressed(irc, msg, payload=text) if not payload: # not addressed return # marks used by '_addressed' are usually prefixes (char, string, # nick), but may also be suffixes (with # supybot.reply.whenAddressedBy.nick.atEnd); but there is no way to # have it in the middle of the message AFAIK. assert payload in text if not text.endswith(payload): # If there is a suffix, it means the end of the text is used to # address the bot, so it can't be a method to be completed. return autocomplete_response = _getAutocompleteResponse(irc, msg, payload) if not autocomplete_response: return target = msg.channel or ircutils.nickFromHostmask(msg.prefix) irc.queueMsg( ircmsgs.IrcMsg( server_tags={ "+draft/reply": msgid, RESPONSE_TAG: autocomplete_response, }, command="TAGMSG", args=[target], ) ) Class = Autocomplete # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Autocomplete/test.py0000644000175000017500000001475314535072470017605 0ustar00valval### # Copyright (c) 2020-2021, 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 import conf, ircmsgs from supybot.test import * class AutocompleteTestCase(PluginTestCase): plugins = ("Autocomplete", "Later", "Misc") # Later and Misc both have a 'tell' command, this allows checking # deduplication. def _sendRequest(self, request): self.irc.feedMsg( ircmsgs.IrcMsg( prefix="foo!bar@baz", server_tags={ "msgid": "1234", "+draft/autocomplete-request": request, }, command="TAGMSG", args=[self.nick], ) ) def _assertAutocompleteResponse(self, request, expectedResponse): self._sendRequest(request) m = self.irc.takeMsg() self.assertEqual( m.server_tags["+draft/autocomplete-response"], expectedResponse ) self.assertEqual( m, ircmsgs.IrcMsg( server_tags={ "+draft/reply": "1234", "+draft/autocomplete-response": expectedResponse, }, command="TAGMSG", args=["foo"], ), ) def testResponse(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse("apro", "pos") def testSingleCommandName(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse("apro", "pos") self._assertAutocompleteResponse("apr", "opos") self._assertAutocompleteResponse("tel", "l") def testTwoResults(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse("te", "ll\tstplugin") def testCommandNameAndPluginName(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse("misc t", "ell") self._assertAutocompleteResponse( "misc c", "learmores\tompletenick" ) def testSinglePluginName(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse( "lat", "er notes\ter remove\ter tell\ter undo" ) def testNextWord(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse( "later", " notes\t remove\t tell\t undo" ) def testNoResponse(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): self._sendRequest("apro") self.assertIsNone(self.irc.takeMsg()) with conf.supybot.plugins.Autocomplete.enabled.context(True): self._sendRequest("apro") self.assertIsNone(self.irc.takeMsg()) class AutocompleteChannelTestCase(ChannelPluginTestCase): plugins = ("Autocomplete", "Later", "Misc") def _sendRequest(self, request): self.irc.feedMsg( ircmsgs.IrcMsg( prefix="foo!bar@baz", server_tags={ "msgid": "1234", "+draft/autocomplete-request": request, }, command="TAGMSG", args=[self.channel], ) ) def _assertAutocompleteResponse(self, request, expectedResponse): self._sendRequest(request) m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg( server_tags={ "+draft/reply": "1234", "+draft/autocomplete-response": expectedResponse, }, command="TAGMSG", args=[self.channel], ), ) def testResponse(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Autocomplete.enabled.context(True): self._assertAutocompleteResponse("@apro", "pos") def testNoResponse(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): self._sendRequest("@apro") self.assertIsNone(self.irc.takeMsg()) with conf.supybot.plugins.Autocomplete.enabled.context(True): self._sendRequest("@apro") self.assertIsNone(self.irc.takeMsg()) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/BadWords/0000755000175000017500000000000014535072535015310 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/__init__.py0000644000175000017500000000520714535072470017423 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Filters bad words on outgoing messages from the bot, so the bot can't be made to say bad words. As an additional capability, it can (optionally) kick users who use such words from channels that have that capability enabled. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/config.py0000644000175000017500000001402314535072470017125 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 __future__ import division import time import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('BadWords') def configure(advanced): from supybot.questions import output, expect, anything, something, yn conf.registerPlugin('BadWords', True) if yn(_('Would you like to add some bad words?')): words = anything(_('What words? (separate individual words by ' 'spaces)')) conf.supybot.plugins.BadWords.words.set(words) class LastModifiedSpaceSeparatedSetOfStrings(registry.SpaceSeparatedSetOfStrings): lastModified = 0 def setValue(self, v): self.lastModified = time.time() registry.SpaceSeparatedListOfStrings.setValue(self, v) class LastModifiedCommaSeparatedSetOfStrings(registry.CommaSeparatedSetOfStrings): lastModified = 0 def set(self, v): if not v.strip(): self.setValue(set()) else: super().set(v) def setValue(self, v): self.lastModified = time.time() registry.CommaSeparatedListOfStrings.setValue(self, v) BadWords = conf.registerPlugin('BadWords') conf.registerGlobalValue(BadWords, 'words', LastModifiedSpaceSeparatedSetOfStrings([], _("""Determines what words are considered to be 'bad' so the bot won't say them."""))) conf.registerChannelValue(BadWords,'requireWordBoundaries', registry.Boolean(False, _("""Determines whether the bot will require bad words to be independent words, or whether it will censor them within other words. For instance, if 'darn' is a bad word, then if this is true, 'darn' will be censored, but 'darnit' will not. You probably want this to be false. After changing this setting, the BadWords regexp needs to be regenerated by adding/removing a word to the list, or reloading the plugin."""))) conf.registerGlobalValue(BadWords, 'phrases', LastModifiedCommaSeparatedSetOfStrings([], _("""Comma-separated groups of words that are considered to be 'bad'."""))) class String256(registry.String): def __call__(self): s = registry.String.__call__(self) return s * (1024//len(s)) def __str__(self): return self() conf.registerGlobalValue(BadWords, 'nastyChars', String256('!@#&', _("""Determines what characters will replace bad words; a chunk of these characters matching the size of the replaced bad word will be used to replace the bad words you've configured."""))) class ReplacementMethods(registry.OnlySomeStrings): validStrings = ('simple', 'nastyCharacters') conf.registerGlobalValue(BadWords, 'replaceMethod', ReplacementMethods('nastyCharacters', _("""Determines the manner in which bad words will be replaced. 'nastyCharacters' (the default) will replace a bad word with the same number of 'nasty characters' (like those used in comic books; configurable by supybot.plugins.BadWords.nastyChars). 'simple' will replace a bad word with a simple strings (regardless of the length of the bad word); this string is configurable via supybot.plugins.BadWords.simpleReplacement."""))) conf.registerGlobalValue(BadWords,'simpleReplacement', registry.String('[CENSORED]', _("""Determines what word will replace bad words if the replacement method is 'simple'."""))) conf.registerGlobalValue(BadWords, 'stripFormatting', registry.Boolean(True, _("""Determines whether the bot will strip formatting characters from messages before it checks them for bad words. If this is False, it will be relatively trivial to circumvent this plugin's filtering. If it's True, however, it will interact poorly with other plugins that do coloring or bolding of text."""))) conf.registerChannelValue(BadWords, 'selfCensor', registry.Boolean(True, _("""Determines whether the bot will filter its own messages."""))) conf.registerChannelValue(BadWords, 'kick', registry.Boolean(False, _("""Determines whether the bot will kick people with a warning when they use bad words."""))) conf.registerChannelValue(BadWords.kick, 'message', registry.NormalizedString(_("""You have been kicked for using a word prohibited in the presence of this bot. Please use more appropriate language in the future."""), _("""Determines the kick message used by the bot when kicking users for saying bad words."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/BadWords/locales/0000755000175000017500000000000014535072535016732 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/locales/fi.po0000644000175000017500000001531614535072470017674 0ustar00valval# BadWords plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Supybot BadWords\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Finnish\n" "X-Poedit-Country: FINLAND\n" #: config.py:43 msgid "Would you like to add some bad words?" msgstr "Haluaisitko lisätä joitakin pahoja sanoja?" #: config.py:44 msgid "What words? (separate individual words by spaces)" msgstr "Mitkä sanat? (Rajoita erilliset sanat käyttämällä välilyöntiä)." #: config.py:68 msgid "" "Determines what words are\n" " considered to be 'bad' so the bot won't say them." msgstr "" "Määrittää mitkä sanat ovat\n" " 'pahoja', jottei botti sano niitä." #: config.py:71 msgid "" "Determines whether the bot will require bad\n" " words to be independent words, or whether it will censor them within " "other\n" " words. For instance, if 'darn' is a bad word, then if this is true, " "'darn'\n" " will be censored, but 'darnit' will not. You probably want this to be\n" " false. After changing this setting, the BadWords regexp needs to be\n" " regenerated by adding/removing a word to the list, or reloading the\n" " plugin." msgstr "" "Määrittää vaatiiko botti pahojen sanojen\n" " olevan toisistaan riippumattomia sanoja, vai sensuroiko se ne toisten " "sanojen\n" " sisältä. Esimerkiksi, jos 'pah' on paha sana, ja jos tämä on asetus on " "true, 'pah'\n" " sensuroidaan, mutta 'pahus' ei sensuroida. Sinä luultavasti tahdot " "pitää tämän\n" " false:na. Tämän asetuksen muuttamisen jälkeen, BadWords säännöllinen " "lauseke täytyy\n" " luoda uudelleen lisäämällä/poistamalla sana listalta, tai lataamalla " "lisäosa \n" " uudelleen." #: config.py:79 msgid "" "Comma-separated groups\n" " of words that are considered to be 'bad'." msgstr "" #: config.py:91 msgid "" "Determines what characters will replace bad words; a\n" " chunk of these characters matching the size of the replaced bad word " "will\n" " be used to replace the bad words you've configured." msgstr "" "Määrittä mitkä merkit korvaavat pahat sanat; \n" " osia näistä merkeistä, jotka sopivat pahan sanan kokoon,\n" " käytetään määrittämiesi pahojen sanojen korvaamisessa." #: config.py:99 msgid "" "Determines the manner in which\n" " bad words will be replaced. 'nastyCharacters' (the default) will " "replace a\n" " bad word with the same number of 'nasty characters' (like those used in\n" " comic books; configurable by supybot.plugins.BadWords.nastyChars).\n" " 'simple' will replace a bad word with a simple strings (regardless of " "the\n" " length of the bad word); this string is configurable via\n" " supybot.plugins.BadWords.simpleReplacement." msgstr "" "Määrittää millä tavalla\n" " pahat sanat korvataan. 'nastyCharacters' (oletus) korvaa\n" " pahan sanan samalla määrällä 'häijyjä merkkejä' (kuten ne jotka ovat\n" " sarjakuvissa; muokattavissa supybot.plugins.BadWords.nastyChars " "asetuksella).\n" " 'simple' korvaa pahan sanan yksinkertaisella merkkiketjulla " "(riippumatta\n" " pahan sanan koosta); tämä merkkiketju on muokattavissa\n" " asetuksella supybot.plugins.BadWords.simpleReplacement." #: config.py:107 msgid "" "Determines what word will replace bad\n" " words if the replacement method is 'simple'." msgstr "" "Määrittää mikä sana korvaa pahat\n" " sanat jos korvausmenetelmä on 'simple'." #: config.py:110 msgid "" "Determines whether the bot will strip\n" " formatting characters from messages before it checks them for bad " "words.\n" " If this is False, it will be relatively trivial to circumvent this " "plugin's\n" " filtering. If it's True, however, it will interact poorly with other\n" " plugins that do coloring or bolding of text." msgstr "" "Määrittää riisuuko botti\n" " muotoilun merkeistä ennen kuin se tarkistaa ne pahojen sanojen varalta.\n" " Jos tämä on 'False', on hyvin pinnallista kiertää tämän lisäosan\n" " suodatusta. Jos se kuitenkin on 'True', se on huonosti " "vuorovaikutuksessa\n" " tekstin värittämistä tai korostamista tekevien lisäosien kanssa." #: config.py:117 msgid "" "Determines whether the bot will kick people with\n" " a warning when they use bad words." msgstr "" "Määrittää potkiiko botti ihmiset\n" " varoituksella jos he käyttävät pahoja sanoja." #: config.py:120 msgid "" "You have been kicked for using a word\n" " prohibited in the presence of this bot. Please use more appropriate\n" " language in the future." msgstr "" " Sinut on potkittu kielletyn sanan\n" " käytöstä tämän botin läsnäollessa. Ole hyvä ja käytä asianmukaisempaa\n" " kieltä tulevaisuudessa." #: config.py:122 msgid "" "Determines the kick message used by the\n" " bot when kicking users for saying bad words." msgstr "" "Määrittää potkimisviestin, jota\n" " botti käyttää, kun potkii käyttäjiä pahojen sanojen käyttämistä." #: plugin.py:46 msgid "" "Maintains a list of words that the bot is not allowed to say.\n" " Can also be used to kick people that say these words, if the bot\n" " has op." msgstr "" "Säilyttää listaa sanoista, joita botin ei ole sallittua sanoa.\n" " Voidaan myös käyttää potkimaan ihmisiä, jotka sanovat näitä sanoja, jos " "botilla\n" " on opit." #: plugin.py:127 msgid "" "takes no arguments\n" "\n" " Returns the list of words being censored.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa listan sanoista, joita sensuroidaan.\n" " " #: plugin.py:137 msgid "I'm not currently censoring any bad words." msgstr "En ole sensuroimassa yhtään pahaa sanaa juuri nyt." #: plugin.py:142 msgid "" " [ ...]\n" "\n" " Adds all s to the list of words being censored.\n" " " msgstr "" " [ ...]\n" "\n" " Lisää kaikki (t) sensuroitaviin sanoihin.\n" " " #: plugin.py:162 msgid "" " [ ...]\n" "\n" " Removes s from the list of words being censored.\n" " " msgstr "" " [ ...]\n" "\n" " Poistaa (t) sensuroitujen sanojen listalta.\n" " " #~ msgid "" #~ "\n" #~ "Filters bad words on outgoing messages from the bot, so the bot can't be " #~ "made\n" #~ "to say bad words.\n" #~ msgstr "" #~ "\n" #~ "Suodattaa botin ulostulevista viesteistä pahat sanat, jotta bottia ei " #~ "saada\n" #~ "sanomaan pahoja sanoja.\n" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/locales/fr.po0000644000175000017500000001374014535072470017704 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:43 msgid "Would you like to add some bad words?" msgstr "Voulez-vous ajouter quelques mots interdits ?" #: config.py:44 msgid "What words? (separate individual words by spaces)" msgstr "Quels mots ? (séparez chaque mot par un espace)" #: config.py:68 msgid "" "Determines what words are\n" " considered to be 'bad' so the bot won't say them." msgstr "" "Détermine quels mots sont considérés comme interdits, donc le bot ne les " "dira pas." #: config.py:71 msgid "" "Determines whether the bot will require bad\n" " words to be independent words, or whether it will censor them within " "other\n" " words. For instance, if 'darn' is a bad word, then if this is true, " "'darn'\n" " will be censored, but 'darnit' will not. You probably want this to be\n" " false. After changing this setting, the BadWords regexp needs to be\n" " regenerated by adding/removing a word to the list, or reloading the\n" " plugin." msgstr "" "Détermine si le bot requiert que les gros mots soient indépendants, ou si " "ils peuvent être dans d'autres mots. Par exemple, si 'merde' est censuré et " "que c'est True, 'merde' sera censuré, mais pas 'emmerder'. Il est " "probablement mieux que ceci soit False. Après avoir modifié ce paramètre, " "l'expression régulière de BadWords doit être regénérée en ajoutant/" "supprimant un mot de la liste, ou en rechargeant le plugin." #: config.py:79 msgid "" "Comma-separated groups\n" " of words that are considered to be 'bad'." msgstr "" #: config.py:91 msgid "" "Determines what characters will replace bad words; a\n" " chunk of these characters matching the size of the replaced bad word " "will\n" " be used to replace the bad words you've configured." msgstr "" "Détermine par quels caractères seront remplacés les gros mots ; un morceau " "de ces caractères ayant la même taille que le mot remplacé sera utilisé pour " "remplacer les gros mots que vous avez configurés." #: config.py:99 msgid "" "Determines the manner in which\n" " bad words will be replaced. 'nastyCharacters' (the default) will " "replace a\n" " bad word with the same number of 'nasty characters' (like those used in\n" " comic books; configurable by supybot.plugins.BadWords.nastyChars).\n" " 'simple' will replace a bad word with a simple strings (regardless of " "the\n" " length of the bad word); this string is configurable via\n" " supybot.plugins.BadWords.simpleReplacement." msgstr "" "Détermine la manière dont les gros mots sont remplacés. " "'nastyCharacters' (par défaut) remplacera un gros mot par le même nombre de " "'caractères obscènes' (comme ceux utilisés dans les bandes dessinées ; " "configurables dans supybot.plugins.BadWords.nastyChars). 'simple' remplacera " "le mot pas une simple chaîne (peu importe la taille du gros mot) ; cette " "chaîne est configurable dans supybot.plugins.BadWords.simpleReplacement." #: config.py:107 msgid "" "Determines what word will replace bad\n" " words if the replacement method is 'simple'." msgstr "" "Détermin quel mot remplacera les mots interdits, si la méthode de " "remplacement est 'simple'." #: config.py:110 msgid "" "Determines whether the bot will strip\n" " formatting characters from messages before it checks them for bad " "words.\n" " If this is False, it will be relatively trivial to circumvent this " "plugin's\n" " filtering. If it's True, however, it will interact poorly with other\n" " plugins that do coloring or bolding of text." msgstr "" "Détermine si le bot retirera les caractères de formattage avant de vérifier " "qu'ils contiennent des gros mots. Si ceci est False, ce sera relativement " "simple de contourner la protection. Si c'est True, toutefois, il fera perdre " "toute couleur ou formattage des autres plugins" #: config.py:117 msgid "" "Determines whether the bot will kick people with\n" " a warning when they use bad words." msgstr "" "Détermine si le bot kickera les gens avec un avertissement lorsqu'ils " "utilisent des gros mots." #: config.py:120 msgid "" "You have been kicked for using a word\n" " prohibited in the presence of this bot. Please use more appropriate\n" " language in the future." msgstr "" "Vous avez été kické(e) parce que vous avez utilisé un mot interdit en la " "présence de ce bot. Veuillez utiliser un langage plus approprié par le futur." #: config.py:122 msgid "" "Determines the kick message used by the\n" " bot when kicking users for saying bad words." msgstr "" "Détermine le message de kick utilisé par le bot lorsqu'il kick des " "utilisateurs utilisant des gros mots." #: plugin.py:46 msgid "" "Maintains a list of words that the bot is not allowed to say.\n" " Can also be used to kick people that say these words, if the bot\n" " has op." msgstr "" #: plugin.py:127 msgid "" "takes no arguments\n" "\n" " Returns the list of words being censored.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne une liste de mots qui sont censurés." #: plugin.py:137 msgid "I'm not currently censoring any bad words." msgstr "Je ne censure actuellement aucun mot." #: plugin.py:142 msgid "" " [ ...]\n" "\n" " Adds all s to the list of words being censored.\n" " " msgstr "" " [ ...]\n" "\n" "Ajoute tous les mots à la liste des mots que le bot ne doit pas dire." #: plugin.py:162 msgid "" " [ ...]\n" "\n" " Removes s from the list of words being censored.\n" " " msgstr "" " [ ...]\n" "\n" "Retire les s de la liste des mots que le bot ne doit pas dire." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/locales/it.po0000644000175000017500000001420514535072470017706 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-28 23:29+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:43 msgid "Would you like to add some bad words?" msgstr "Vuoi aggiungere delle parole volgari?" #: config.py:44 msgid "What words? (separate individual words by spaces)" msgstr "Quali parole? (separa ciascuna con uno spazio)" #: config.py:68 msgid "" "Determines what words are\n" " considered to be 'bad' so the bot won't say them." msgstr "" "Determina quali parole sono considerate \"volgari\" per cui il bot non le " "pronuncerà." #: config.py:71 msgid "" "Determines whether the bot will require bad\n" " words to be independent words, or whether it will censor them within " "other\n" " words. For instance, if 'darn' is a bad word, then if this is true, " "'darn'\n" " will be censored, but 'darnit' will not. You probably want this to be\n" " false. After changing this setting, the BadWords regexp needs to be\n" " regenerated by adding/removing a word to the list, or reloading the\n" " plugin." msgstr "" "Determina se il bot richieda che le parole volgari siano indipendenti o " "all'interno\n" " di altre parole. Ad esempio, se \"merda\" è volgare e questa opzione è " "impostata\n" " a True, verrà censurata, ma \"merdata\" non lo sarà; probabilmente si " "preferisce\n" " che questa sia su False. Dopo aver modificato questa impostazione, la " "regexp di\n" " BadWords deve essere rigenerata con l'aggiunta o la rimozione di una " "parola\n" " dall'elenco o ricaricando il plugin." #: config.py:79 msgid "" "Comma-separated groups\n" " of words that are considered to be 'bad'." msgstr "" #: config.py:91 msgid "" "Determines what characters will replace bad words; a\n" " chunk of these characters matching the size of the replaced bad word " "will\n" " be used to replace the bad words you've configured." msgstr "" "Determina quali caratteri sostituiranno le parole volgari, verranno " "utilizzati\n" " rimpiazzando la parola da censurare in tutta la sua lunghezza." #: config.py:99 msgid "" "Determines the manner in which\n" " bad words will be replaced. 'nastyCharacters' (the default) will " "replace a\n" " bad word with the same number of 'nasty characters' (like those used in\n" " comic books; configurable by supybot.plugins.BadWords.nastyChars).\n" " 'simple' will replace a bad word with a simple strings (regardless of " "the\n" " length of the bad word); this string is configurable via\n" " supybot.plugins.BadWords.simpleReplacement." msgstr "" "Determina come verranno sostituite le parole volgari. \"nastyCharacters" "\" (predefinito)\n" " rimpiazzerà la parola con lo stesso numero di \"brutti caratteri\" (come " "quelli usati\n" " nei fumetti; configurabile da supybot.plugins.BadWords.nastyChars). " "\"simple\" sostituirà\n" " una parola volgare con una stringa semplice (indipendentemente dalla " "lunghezza della parola);\n" " questa stringa è configurabile tramite supybot.plugins.BadWords." "simpleReplacement." #: config.py:107 msgid "" "Determines what word will replace bad\n" " words if the replacement method is 'simple'." msgstr "" "Determina quale parola sostituirà quelle volgari se viene usato il metodo " "\"simple\"." #: config.py:110 msgid "" "Determines whether the bot will strip\n" " formatting characters from messages before it checks them for bad " "words.\n" " If this is False, it will be relatively trivial to circumvent this " "plugin's\n" " filtering. If it's True, however, it will interact poorly with other\n" " plugins that do coloring or bolding of text." msgstr "" "Determina se il bot rimuoverà i caratteri di formattazione prima di " "controllare\n" " che contengano parole volgari. Se impostato a False sarà relativamente " "facile\n" " aggirare i filtri di questo plugin; tuttavia se impostato a True non " "interagirà\n" " con altri plugin che colorano o rendono il testo grassetto." #: config.py:117 msgid "" "Determines whether the bot will kick people with\n" " a warning when they use bad words." msgstr "" "Determina se il bot caccerà (kick) gli utenti con un avvertimento quando " "usano volgarità." #: config.py:120 msgid "" "You have been kicked for using a word\n" " prohibited in the presence of this bot. Please use more appropriate\n" " language in the future." msgstr "" "Sei stato/a cacciato/a per aver usato parole proibite in presenza del bot.\n" " In futuro utilizza un linguaggio più appropriato." #: config.py:122 msgid "" "Determines the kick message used by the\n" " bot when kicking users for saying bad words." msgstr "" "Determina il messaggio del kick utilizzato dal bot per espellere gli utenti " "che scrivono volgarità." #: plugin.py:46 msgid "" "Maintains a list of words that the bot is not allowed to say.\n" " Can also be used to kick people that say these words, if the bot\n" " has op." msgstr "" "Mantiene un elenco di parole che il bot non può dire.\n" " Se il bot ha lo stato di operatore, può essere anche utilizzato\n" " per cacciare (kick) utenti che scrivono queste parole." #: plugin.py:127 msgid "" "takes no arguments\n" "\n" " Returns the list of words being censored.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta l'elenco delle parole censurate.\n" " " #: plugin.py:137 msgid "I'm not currently censoring any bad words." msgstr "Al momento non ho alcuna parola censurata." #: plugin.py:142 msgid "" " [ ...]\n" "\n" " Adds all s to the list of words being censored.\n" " " msgstr "" " [ ...]\n" "\n" " Aggiunge all'elenco di quelle da censurare.\n" " " #: plugin.py:162 msgid "" " [ ...]\n" "\n" " Removes s from the list of words being censored.\n" " " msgstr "" " [ ...]\n" "\n" " Rimuove dall'elenco di quelle da censurare.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/plugin.py0000644000175000017500000001646314535072470017170 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import time import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb import supybot.ircmsgs as ircmsgs from supybot.commands import * import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('BadWords') class BadWords(callbacks.Privmsg): """Maintains a list of words that the bot is not allowed to say. Can also be used to kick people that say these words, if the bot has op.""" def __init__(self, irc): self.__parent = super(BadWords, self) self.__parent.__init__(irc) # This is so we can not filter certain outgoing messages (like list, # which would be kinda useless if it were filtered). self.filtering = True self.lastModified = 0 self.words = conf.supybot.plugins.BadWords.words self.phrases = conf.supybot.plugins.BadWords.phrases def callCommand(self, name, irc, msg, *args, **kwargs): if ircdb.checkCapability(msg.prefix, 'admin'): self.__parent.callCommand(name, irc, msg, *args, **kwargs) else: irc.errorNoCapability('admin') def sub(self, m): replaceMethod = self.registryValue('replaceMethod') if replaceMethod == 'simple': return self.registryValue('simpleReplacement') elif replaceMethod == 'nastyCharacters': return self.registryValue('nastyChars')[:len(m.group(1))] def inFilter(self, irc, msg): self.filtering = True # We need to check for bad words here rather than in doPrivmsg because # messages don't get to doPrivmsg if the user is ignored. if msg.command == 'PRIVMSG' and (self.words() or self.phrases()): channel = msg.channel self.updateRegexp(channel, irc.network) s = ircutils.stripFormatting(msg.args[1]) if irc.isChannel(channel) \ and self.registryValue('kick', channel, irc.network): if self.regexp.search(s): c = irc.state.channels[channel] cap = ircdb.makeChannelCapability(channel, 'op') if c.isHalfopPlus(irc.nick): if c.isHalfopPlus(msg.nick) or \ ircdb.checkCapability(msg.prefix, cap): self.log.debug("Not kicking %s from %s, because " "they are halfop+ or can't be " "kicked.", msg.nick, channel) else: message = self.registryValue('kick.message', channel, irc.network) irc.queueMsg(ircmsgs.kick(channel, msg.nick, message)) else: self.log.warning('Should kick %s from %s, but not opped.', msg.nick, channel) return msg def updateRegexp(self, channel, network): if self.lastModified < self.words.lastModified \ or self.lastModified < self.phrases.lastModified: self.makeRegexp(self.words() | self.phrases(), channel, network) self.lastModified = time.time() def outFilter(self, irc, msg): channel = msg.channel if self.filtering and msg.command == 'PRIVMSG' \ and (self.words() or self.phrases()) \ and self.registryValue('selfCensor', channel, irc.network): self.updateRegexp(channel, irc.network) s = msg.args[1] if self.registryValue('stripFormatting'): s = ircutils.stripFormatting(s) t = self.regexp.sub(self.sub, s) if t != s: msg = ircmsgs.privmsg(msg.args[0], t, msg=msg) return msg def makeRegexp(self, iterable, channel, network): s = '(%s)' % '|'.join(map(re.escape, iterable)) if self.registryValue('requireWordBoundaries', channel, network): s = r'\b%s\b' % s self.regexp = re.compile(s, re.I) @internationalizeDocstring def list(self, irc, msg, args): """takes no arguments Returns the list of words being censored. """ L = list(self.words() | self.phrases()) if L: self.filtering = False utils.sortBy(str.lower, L) irc.reply(format('%L', L)) else: irc.reply(_('I\'m not currently censoring any bad words.')) list = wrap(list, ['admin']) @internationalizeDocstring def add(self, irc, msg, args, new_words): """ [ ...] Adds all s to the list of words being censored. """ words = self.words() phrases = self.phrases() for word in new_words: if ' ' in word: phrases.add(word) else: words.add(word) self.words.setValue(words) self.phrases.setValue(phrases) irc.replySuccess() add = wrap(add, ['admin', many('something')]) @internationalizeDocstring def remove(self, irc, msg, args, old_words): """ [ ...] Removes s from the list of words being censored. """ words = self.words() phrases = self.phrases() for word in old_words: words.discard(word) phrases.discard(word) self.words.setValue(words) self.phrases.setValue(phrases) irc.replySuccess() remove = wrap(remove, ['admin', many('something')]) Class = BadWords # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/BadWords/test.py0000644000175000017500000000754714535072470016654 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf from supybot.test import * class BadWordsTestCase(PluginTestCase): plugins = ('BadWords', 'Utilities', 'Format', 'Filter') badwords = ('shit', 'ass', 'fuck') def tearDown(self): # .default() doesn't seem to be working for BadWords.words #default = conf.supybot.plugins.BadWords.words.default() #conf.supybot.plugins.BadWords.words.setValue(default) conf.supybot.plugins.BadWords.words.setValue([]) def _test(self): for word in self.badwords: self.assertRegexp('echo %s' % word, '(?!%s)' % word) self.assertRegexp('echo [colorize %s]' % word, '(?!%s)' % word) self.assertRegexp('echo foo%sbar' % word, '(?!%s)' % word) self.assertRegexp('echo foo %s bar' % word, '(?!%s)' % word) self.assertRegexp('echo [format join "" %s]' % ' '.join(word), '(?!%s)' % word) with conf.supybot.plugins.BadWords.requireWordBoundaries \ .context(True): self.assertRegexp('echo foo%sbar' % word, word) self.assertRegexp('echo foo %sbar' % word, word) self.assertRegexp('echo foo%s bar' % word, word) self.assertRegexp('echo foo %s bar' % word, '(?!%s)' % word) def _NegTest(self): for word in self.badwords: self.assertRegexp('echo %s' % word, word) self.assertRegexp('echo foo%sbar' % word, word) self.assertRegexp('echo [format join "" %s]' % ' '.join(word),word) def testAddbadwords(self): self.assertNotError('badwords add %s' % ' '.join(self.badwords)) self._test() def testDefault(self): self._NegTest() def testRemovebadwords(self): self.assertNotError('badwords add %s' % ' '.join(self.badwords)) self.assertNotError('badwords remove %s' % ' '.join(self.badwords)) self._NegTest() def testList(self): self.assertNotError('badwords list') self.assertNotError('badwords add shit') self.assertNotError('badwords add ass') self.assertNotError('badwords add "fuck you"') self.assertResponse('badwords list', 'ass, fuck you, and shit') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3297546 limnoria-2023.11.18/plugins/Channel/0000755000175000017500000000000014535072535015153 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/__init__.py0000644000175000017500000000466014535072470017270 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Basic channel management commands. Many of these commands require their caller to have the #channel,op capability. 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = { supybot.authors.skorobeus: ['enable', 'disable'], } from . import config from . import plugin from importlib import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/config.py0000644000175000017500000000665114535072470017000 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.utils as utils import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Channel') 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('Channel', True) Channel = conf.registerPlugin('Channel') conf.registerChannelValue(Channel, 'alwaysRejoin', registry.Boolean(True, _("""Determines whether the bot will always try to rejoin a channel whenever it's kicked from the channel."""))) conf.registerChannelValue(Channel, 'nicksInPrivate', registry.Boolean(True, _("""Determines whether the output of 'nicks' will be sent in private. This prevents mass-highlights of a channel's users, accidental or on purpose."""))) conf.registerChannelValue(Channel, 'rejoinDelay', registry.NonNegativeInteger(0, _("""Determines how many seconds the bot will wait before rejoining a channel if kicked and supybot.plugins.Channel.alwaysRejoin is on."""))) conf.registerChannelValue(Channel, 'partMsg', registry.String('Limnoria $version', _("""Determines what part message should be used by default. If the part command is called without a part message, this will be used. If this value is empty, then no part message will be used (they are optional in the IRC protocol). The standard substitutions ($version, $nick, etc.) are all handled appropriately."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Channel/locales/0000755000175000017500000000000014535072535016575 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/locales/de.po0000644000175000017500000007731014535072470017533 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: sdf\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:33+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \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" #: config.py:49 msgid "" "Determines whether the bot will always try to\n" " rejoin a channel whenever it's kicked from the channel." msgstr "" "Legt fest ob der Bot immer probieren soll den Kanal wieder zu betreten, " "falls er gekickt wurde." #: config.py:52 msgid "" "Determines whether the output of 'nicks' will\n" " be sent in private. This prevents mass-highlights of a channel's users,\n" " accidental or on purpose." msgstr "" #: config.py:56 msgid "" "Determines how many seconds the bot will wait\n" " before rejoining a channel if kicked and\n" " supybot.plugins.Channel.alwaysRejoin is on." msgstr "" #: config.py:60 msgid "" "Determines what part message should be\n" " used by default. If the part command is called without a part " "message,\n" " this will be used. If this value is empty, then no part message " "will\n" " be used (they are optional in the IRC protocol). The standard\n" " substitutions ($version, $nick, etc.) are all handled appropriately." msgstr "" #: plugin.py:48 msgid "" "This plugin provides various commands for channel management, such\n" " as setting modes and channel-wide bans/ignores/capabilities. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:94 msgid "" "[] [ ...]\n" "\n" " Sets the mode in to , sending the arguments given.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Setzt den Modus für auf , mit den angegeben Argumenten. " " ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet " "wurde." #: plugin.py:101 msgid "change the mode" msgstr "ändert den Modus" #: plugin.py:105 msgid "" "[] []\n" "\n" " Sets the channel limit to . If is 0, or isn't " "given,\n" " removes the channel limit. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Setzt das Kanal Limit auf . Falls 0 ist, oder nicht angegeben " "wird, wird das Kanal Limit entfernt. ist nur notwendig, wenn die " "Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:115 msgid "change the limit" msgstr "ändert das Limit" #: plugin.py:120 msgid "" "[]\n" "\n" " Sets +m on , making it so only ops and voiced users can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Setzt +m auf , sodass nur Ops und Voice Benutzer Nachrichten an den " "Kanal senden können. ist nur notwendig, wenn die Nachricht nicht im " "Kanal selbst gesendet wurde." #: plugin.py:127 msgid "moderate the channel" msgstr "moderiert den Kanel" #: plugin.py:131 msgid "" "[]\n" "\n" " Sets -m on , making it so everyone can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Setzt -m auf , sodass jeder Nachrichten an den Kanal senden kann. " " ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet " "wurde." #: plugin.py:139 msgid "unmoderate the channel" msgstr "Kanal nicht mehr moderieren" #: plugin.py:143 msgid "" "[] []\n" "\n" " Sets the keyword in to . If is not given, " "removes\n" " the keyword requirement to join . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Setzt das Schlüsselwort für auf . Fall nicht " "angegeben wird, wird die Schlüsselwort entfernt, und jeder kann dem " "beitreten. ist nur notwendig, wenn die Nachricht nicht im Kanal " "selbst gesendet wurde." #: plugin.py:155 msgid "change the keyword" msgstr "ändert das Schlüsselwort" #: plugin.py:160 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give all the " "s\n" " you provide ops. If you don't provide any s, this will op " "you.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dies allen s die du " "angibst Op geben. Falls du keine s angibst, wird dir Op gegeben. " " ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet " "wurde." #: plugin.py:172 msgid "op someone" msgstr "jemandem Operatorrechte geben." #: plugin.py:176 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,halfop capability, this will give all the\n" " s you provide halfops. If you don't provide any s, " "this\n" " will give you halfops. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,halfop Fähigkeit hast, wird allen angegeben s " "halb-Op gegeben. Falls du keine s angibst wird dir halb-Op gegeben. " " ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet " "wurde." #: plugin.py:188 msgid "halfop someone" msgstr "jemandem Halfop geben" #: plugin.py:209 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,voice capability, this will voice all the\n" " s you provide. If you don't provide any s, this will\n" " voice you. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,voice Fähigkeit hast, wird allen angegeben s " "Voice gegeben. Falls du keine s angibst wird dir Voice gegeben. " " ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet " "wurde." #: plugin.py:217 msgid "voice someone" msgstr "Sprechrecht geben" #: plugin.py:222 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird allen angegeben Nicks Op " "entzogen. Falls keine Nicks angegeben sind, wird Op der Person entzogen die " "den Befehl gab." #: plugin.py:229 msgid "" "I cowardly refuse to deop myself. If you really want me deopped, tell me to " "op you and then deop me yourself." msgstr "" "Ich verweigere es mir selbst Op zu entziehen. Falls du mir wirklich op " "entziehen willst, befehle mir dir Op zu geben und tu es selbst." #: plugin.py:237 msgid "deop someone" msgstr "Operator entziehen" #: plugin.py:242 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove half-" "operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " half-operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird allen angegeben Nicks halb-Op " "entzogen. Falls keine Nicks angegeben sind, wird halb-Op der Person entzogen " "die den Befehl gab." #: plugin.py:249 msgid "" "I cowardly refuse to dehalfop myself. If you really want me dehalfopped, " "tell me to op you and then dehalfop me yourself." msgstr "" "Ich verweigere mich mir halb-Op zu entziehen. Falls du mir halb-Op entziehen " "willst, befehle mir dir Op zu geben und tu es selbst." #: plugin.py:257 msgid "dehalfop someone" msgstr "entzieht jemanden halb-Op" #: plugin.py:262 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove voice from " "all\n" " the nicks given. If no nicks are given, removes voice from the " "person\n" " sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird allen angegeben Nicks Voice " "entzogen. Falls keine Nicks angegeben sind, wird Voice der Person entzogen " "die den Befehl gab." #: plugin.py:274 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will cause the bot to\n" " \"cycle\", or PART and then JOIN the channel. is only " "necessary\n" " if the message isn't sent in the channel itself. If is not\n" " specified, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if neither a cycle reason nor a default part message is given.\n" " " msgstr "" "[]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird der Bot den Kanal verlassen " "und danach wieder betreten. ist nur notwendig, wenn die Nachricht " "nicht im Kanal selbst gesendet wurde." #: plugin.py:292 msgid "" "[] [, , ...] []\n" "\n" " Kicks (s) from for . If isn't " "given,\n" " uses the nick of the person making the command as the reason.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [, , ...]\n" "\n" "Kickt die s aus dem mit der . Falls keine " " angegeben wurde, wird der Nick der Person die den Befehl " "gesendet hat, als Begründung genommen. ist nur notwendig, wenn die " "Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:300 msgid "I cowardly refuse to kick myself." msgstr "Ich bin zu feige um mich selbst zu kicken." #: plugin.py:305 msgid "" "The reason you gave is longer than the allowed length for a KICK reason on " "this server." msgstr "" "Der Grund den du mir gegeben hast war länger als die erlaubte für KICK " "Gründe auf diesem Server." #: plugin.py:310 msgid "kick someone" msgstr "jemanden kicken" #: plugin.py:316 msgid "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " If you have the #channel,op capability, this will kickban " "for\n" " as many seconds as you specify, or else (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact bans only the exact hostmask; --nick bans just the nick;\n" " --user bans just the user, and --host bans just the host. You can\n" " combine these options as you choose. is a reason to give " "for\n" " the kick.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird den kickbannen, solange " "wie in Sekunden angegeben, oder andernfalls (falls 0 Sekunden oder gar keine " "Sekunden angeben werden) wird die Person für immer gebannt. --exact bannt " "nur die exakte Hostmaske; --nick bannt nur den Nick; --user bannt nur den " "Benuter und --host bannt nur diese Hostmaske. Du kannst die Optionen " "miteinander kombinieren wie du willst. ist die Begründung für " "den Kick. ist nur notwendig, wenn die Nachricht nicht im Kanal " "selbst gesendet wurde." #: plugin.py:333 msgid "kick or ban someone" msgstr "jemanden kicken oder bannen" #: plugin.py:341 #, fuzzy msgid "" "[] [--{exact,nick,user,host}] []\n" "\n" " If you have the #channel,op capability, this will ban for\n" " as many seconds as you specify, otherwise (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact can be used to specify an exact hostmask. You can combine " "the\n" " exact, nick, user, and host options as you choose. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird den kickbannen, solange " "wie in Sekunden angegeben, oder andernfalls (falls 0 Sekunden oder gar keine " "Sekunden angeben werden) wird die Person für immer gebannt. --exact bannt " "nur die exakte Hostmaske; --nick bannt nur den Nick; --user bannt nur den " "Benuter und --host bannt nur diese Hostmaske. Du kannst die Optionen " "miteinander kombinieren wie du willst. ist die Begründung für " "den Kick. ist nur notwendig, wenn die Nachricht nicht im Kanal " "selbst gesendet wurde." #: plugin.py:355 plugin.py:581 #, fuzzy msgid "ban someone" msgstr "jemanden entbannen" #: plugin.py:375 msgid "I haven't seen %s." msgstr "Ich habe % nicht gesehen." #: plugin.py:385 msgid "I cowardly refuse to kickban myself." msgstr "Ich bin zu feige um mich selbst zu kicken und zu bannen." #: plugin.py:394 msgid "I cowardly refuse to ban myself." msgstr "Ich bin zu feige um mich selbst zu bannen." #: plugin.py:422 msgid "%s has %s too, you can't ban them." msgstr "%s hat auch %s, du kannst ihn/sie/es nicht bannen." #: plugin.py:433 #, fuzzy msgid "" "[] []\n" "\n" " Unbans on . If is not given, unbans\n" " any hostmask currently banned on that matches your " "current\n" " hostmask. Especially useful for unbanning yourself when you get\n" " unexpectedly (or accidentally) banned from the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Entbannt im . Falls die nicht angegeben wird, " "werden alle Bans entfernt, die momenaten im gebannt sind und auf " "deine momentane Hostmaske zutreffen. Besonders nützlich um dich selsbt zu " "entbannen, falls du versehentlich im Kanal gebannt wurdest. ist nur " "notwendig, wenn die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:453 msgid "All bans on %s matching %s have been removed." msgstr "Alle Bans von %s die auf %s zutreffen wurden entfernt." #: plugin.py:457 msgid "No bans matching %s were found on %s." msgstr "Keine passenden Bans %s wurden in %s gefunden." #: plugin.py:460 msgid "unban someone" msgstr "jemanden entbannen" #: plugin.py:467 msgid "" "[]\n" "\n" " List all bans on the channel.\n" " If is not given, it defaults to the current channel." msgstr "" #: plugin.py:471 msgid "No bans." msgstr "" #: plugin.py:476 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will invite \n" " to join . is only necessary if the message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dazu eingeladen den " " zu betreten. ist nur notwendig, wenn die Nachricht nicht im " "Kanal selbst gesendet wurde." #: plugin.py:485 msgid "invite someone" msgstr "jemanden einladen" #: plugin.py:504 msgid "%s is already in %s." msgstr "%s ist schon in %s." #: plugin.py:511 msgid "There is no %s on this network." msgstr "%s nicht auf diesem Netzwerk." #: plugin.py:523 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will \"lobotomize\" " "the\n" " bot, making it silent and unanswering to all requests made in " "the\n" " channel. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" "Falls du die #channel,op Fähigkeit besitzt, wird dies den Bot " "\"hirnamputieren\", er wird still und wird nicht auf Anfragen reagieren, die " "im Kanal getätigt werden. ist nur notwendig, wenn die Nachricht " "nicht im Kanal selbst gesendet wurde." #: plugin.py:538 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will unlobotomize " "the\n" " bot, making it respond to requests made in the channel again.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dem Bot sein Hirn " "zurückgegeben, ab diesem Zeitpunkt antwortet er wieder auf Anfragen aus dem " "Kanal. ist nur notwendig, wenn die Nachricht nicht im Kanal selbst " "gesendet wurde." #: plugin.py:553 msgid "" "takes no arguments\n" "\n" " Returns the channels in which this bot is lobotomized.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt die Kanäle aus in denen der Bot hirnamputiert ist." #: plugin.py:568 msgid "I'm currently lobotomized in %L." msgstr "Ich bin momentan hirnamputiert in %L." #: plugin.py:571 msgid "I'm not currently lobotomized in any channels that you're in." msgstr "" "Momentan bin ich nicht hirnamputiert in allen Kanälen in denen du bist." #: plugin.py:585 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will effect a\n" " persistent ban from interacting with the bot on the given\n" " (or the current hostmask associated with ). " "Other\n" " plugins may enforce this ban by actually banning users with\n" " matching hostmasks when they join. is an optional\n" " argument specifying when (in \"seconds from now\") the ban " "should\n" " expire; if none is given, the ban will never automatically " "expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird einem beständigen Ban mit der " " (oder dem Benutzer der mit verbunden wird) verboten mit " "dem Bot zu interagieren. Andere Plugins setzen es womöglich durch das der " "Ban Benutzer beim betreten eines Kanals wirklich bannt. ist " "optional, und legt fest wann (in Sekunden von jetzt an) der Ban abläuft, " "falls nicht angegeben wird der Ban niemals automatisch ablaufen. ist " "nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:605 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ban on . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Falls du die #channel,op Fähigkeit hast, wird der beständige Ban auf " " aufgehoben. ist nur notwendig, wenn die Nachricht nicht " "im Kanal selbst gesendet wurde." #: plugin.py:617 msgid "There are no persistent bans for that hostmask." msgstr "Es gibt keine beständigen Bans für diese Hostmaske." #: plugin.py:622 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will show you the\n" " current persistent bans on the that match the given\n" " mask, if any (returns all of them otherwise).\n" " Note that you can use * as a wildcard on masks and \\* to match\n" " actual * in masks\n" " " msgstr "" "[] \n" "\n" "Falls du die #channel,op Fähigkeit hast, wird der beständige Ban auf " " aufgehoben. ist nur notwendig, wenn die Nachricht nicht " "im Kanal selbst gesendet wurde." #: plugin.py:640 msgid "%q (expires %t)" msgstr "%q (läuft ab:%t)" #: plugin.py:643 msgid "%q (never expires)" msgstr "%q(läuft niemals ab)" #: plugin.py:647 msgid "There are no persistent bans on %s." msgstr "Es gibt keine beständigen Bans für %s." #: plugin.py:654 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will set a " "persistent\n" " ignore on or the hostmask currently\n" " associated with . is an optional argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird eine beständige Ignorierung " "auf gesetzt oder auf die Hostmaske die momentan mit " "verbunden wird. ist optional und gibt an wann (in Sekunden von " "jetzt an) die Ignorierung abläuft, falls dies nicht angegeben wird, wird die " "Ignorierung niemals automatisch ablaufen. ist nur notwendig, wenn " "die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:672 #, fuzzy msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ignore on in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dies den dauerhaften Ignore " "von im Kanal entfernen. ist nur notwendig, wenn die " "Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:684 msgid "There are no ignores for that hostmask." msgstr "Es gibt keine Ignorierungen für diese Hostmaske." #: plugin.py:689 msgid "" "[]\n" "\n" " Lists the hostmasks that the bot is ignoring on the given " "channel.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" "Listet die Hostmasken auf, die der Bot im gegeben Kanal ignoriert. " "ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:698 msgid "I'm not currently ignoring any hostmasks in %q" msgstr "Momentan ignoriere ich keine Hostmasken in %s." #: plugin.py:709 #, fuzzy msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give the\n" " (or the user to whom maps)\n" " the capability in the channel. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird die Kanalfähigkeit " "dem Benutzer (oder dem Benutzer auf den passt) gegeben. " " ist nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet " "wurde." #: plugin.py:725 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will take from the\n" " user currently identified as (or the user to whom " "\n" " maps) the capability in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird die die Fähigkeit " "im Kanal vom Benutzer entfernt, der momentan als angemeldet ist (oder " "auf des Benutzers auf den die zutrifft). ist nur " "notwendig, wenn die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:744 msgid "That user didn't have the %L %s." msgstr "Der Nutzer hatte nicht %L %s." #: plugin.py:753 msgid "" "[] {True|False}\n" "\n" " If you have the #channel,op capability, this will set the " "default\n" " response to non-power-related (that is, not {op, halfop, " "voice})\n" " capabilities to be the value you give. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {True|False}\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dies die Standard Antwort auf " "nicht-macht-bezogene (also nicht {Op, halb-Op, Voice}) Fähigkeiten auf die " "angegeben Antwort setzen. ist nur notwendig, wenn die Nachricht " "nicht im Kanal selbst gesendet wurde." #: plugin.py:771 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will add the " "channel\n" " capability for all users in the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird die Kanalfähigkeit " " für alle Benutzer hinzugefügt. ist nur notwendig, wenn " "die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:786 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will unset the " "channel\n" " capability so each user's specific capability or " "the\n" " channel default capability will take precedence. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird die Kanalfähigkeit " "entfernt, sodass die Benutzerfähigkeiten oder Kanal Standardfähigkeiten " "vorrang haben. ist nur notwendig, wenn die Nachricht nicht im Kanal " "selbst gesendet wurde." #: plugin.py:802 msgid "capability" msgstr "Fähigkeit" #: plugin.py:805 msgid "I do not know about the %L %s." msgstr "Ich weiß nichts von %L %s." #: plugin.py:812 msgid "" "[]\n" "\n" " Returns the capabilities present on the . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Gibt die Fähigkeiten zurück, die es im gibt. ist nur " "notwendig, falls die Nachricht nicht im Kanal selbt gegeben wurde." #: plugin.py:824 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will disable the " "\n" " in . If is provided, will be disabled " "only\n" " for that plugin. If only is provided, all commands in the\n" " given plugin will be disabled. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dies den im " "deaktivieren, falls er deaktiviert wurde. Falls angegeben wurde, " "wird nur für dieses deaktiviert. Falls nur " "angegeben wurde, werden alle Befehle des Plugins deaktiviert. ist " "nur notwendig, wenn die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:840 plugin.py:879 msgid "The %s plugin does not have a command called %s." msgstr "%s Plugin hat keinen Befehl %s." #: plugin.py:847 plugin.py:886 msgid "No plugin or command named %s could be found." msgstr "Kein Plugin oder Befehl mit dem Namen %s konnte gefunden werden." #: plugin.py:863 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will enable the " "\n" " in if it has been disabled. If is provided,\n" " will be enabled only for that plugin. If only " "is\n" " provided, all commands in the given plugin will be enabled. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" "Falls du die #channel,op Fähigkeit hast, wird dies den im " "aktivieren, falls er deaktiviert wurde. Falls angegeben wurde, wird " " nur für dieses aktiviert. Falls nur angegeben " "wurde, werden alle Befehle des Plugins aktiviert. ist nur notwendig, " "wenn die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:900 msgid "%s was not disabled." msgstr "%s wurde nicht abgeschaltet." #: plugin.py:909 msgid "" "[] [--count]\n" "\n" " Returns the nicks in . is only necessary if the\n" " message isn't sent in the channel itself. Returns only the number " "of\n" " nicks if --count option is provided.\n" " " msgstr "" "[] [--count]\n" "\n" "Gibt die Nicks in zurück. ist nur notwendig, wenn die " "Nachricht nicht im Kanal selbst gesendet wurde. Gibt nur die Anzahl der " "Nicks an, falls die --count Option angegeben wurde." #: plugin.py:931 msgid "You don't have access to that information." msgstr "Ich habe keinen Zugriff auf diese Informationen." #: plugin.py:946 msgid "" "Internal message for notifying all the #channel,ops in a channel of\n" " a given situation." msgstr "" "Interne Nachricht die alle #channel,ops eines Kanals über eine Situation " "informiert." #: plugin.py:949 msgid "Alert to all %s ops: %s" msgstr "Alarm an alle %s Operatoren: %s" #: plugin.py:951 msgid " (from %s)" msgstr "(von %s)" #: plugin.py:963 msgid "" "[] \n" "\n" " Sends to all the users in who have the ," "op\n" " capability.\n" " " msgstr "" "[] \n" "\n" "Sendet , die die ,op Fähigkeit haben." #: plugin.py:973 msgid "" "[] []\n" "\n" " Tells the bot to part the list of channels you give it. " "is\n" " only necessary if you want the bot to part a channel other than the\n" " current channel. If is specified, use it as the part\n" " message. Otherwise, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if no default is configured.\n" " " msgstr "" #: plugin.py:994 #, fuzzy msgid "I'm not in %s." msgstr "Ich habe % nicht gesehen." #~ msgid "" #~ "I cowardly refuse to devoice myself. If you really want me devoiced, " #~ "tell me to op you and then devoice me yourself." #~ msgstr "" #~ "Ich bin zu Feige um mir Voice zu entziehen. Falls du mir wirklich Voice " #~ "entziehen willst, befehle mir dir Op zu geben und entziehe mir Voice " #~ "selbst." #, fuzzy #~ msgid "" #~ "[]\n" #~ "\n" #~ " If you have the #channel,op capability, this will show you " #~ "the\n" #~ " current persistent bans on the .\n" #~ " " #~ msgstr "" #~ "[]\n" #~ "\n" #~ "Falls du die #channel,op Fähigkeit hast, werden die momentan beständigen " #~ "Bans im angezeigt." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/locales/fi.po0000644000175000017500000010471514535072470017541 0ustar00valval# # Mikaela Suomalainen , 2012. # msgid "" msgstr "" "Project-Id-Version: Supybot Channel\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:14+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: suomi <>\n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" "X-Generator: Poedit 1.6.10\n" #: config.py:49 msgid "" "Determines whether the bot will always try to\n" " rejoin a channel whenever it's kicked from the channel." msgstr "" "Määrittää yrittääkö botti aina\n" " liittyä kanavalle uudelleen, kun se on potkittu kanavalta." #: config.py:52 msgid "" "Determines whether the output of 'nicks' will\n" " be sent in private. This prevents mass-highlights of a channel's users,\n" " accidental or on purpose." msgstr "" #: config.py:56 msgid "" "Determines how many seconds the bot will wait\n" " before rejoining a channel if kicked and\n" " supybot.plugins.Channel.alwaysRejoin is on." msgstr "" #: config.py:60 msgid "" "Determines what part message should be\n" " used by default. If the part command is called without a part " "message,\n" " this will be used. If this value is empty, then no part message " "will\n" " be used (they are optional in the IRC protocol). The standard\n" " substitutions ($version, $nick, etc.) are all handled appropriately." msgstr "" #: plugin.py:48 msgid "" "This plugin provides various commands for channel management, such\n" " as setting modes and channel-wide bans/ignores/capabilities. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" "Tämä plugin antaa erilaisia kanavanhallinta komentoja, kuten tilojen ja " "kanavanlaajuisten\n" " bannien/ignoreiden/valtuuksien asettamisen. Tämä on ydin Supybot-plugin, " "jota ei pitäisi poistaa." #: plugin.py:94 msgid "" "[] [ ...]\n" "\n" " Sets the mode in to , sending the arguments given.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Asettaa tilaksi , lähettäen annetut parametrit.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalta\n" " jonka tilaa ollaan muuttamassa.\n" " " #: plugin.py:101 msgid "change the mode" msgstr "vaihda tila" #: plugin.py:105 msgid "" "[] []\n" "\n" " Sets the channel limit to . If is 0, or isn't " "given,\n" " removes the channel limit. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Asettaa kanavan . Jos on 0, tai ei annettu,\n" "kanavan rajoitus poistetaan. on vaadittu vain jos\n" "viestiä ei lähetetä kanavalta itseltään.\n" " " #: plugin.py:115 msgid "change the limit" msgstr "Vaihda rajoitusta" #: plugin.py:120 msgid "" "[]\n" "\n" " Sets +m on , making it so only ops and voiced users can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Asettaa tilan +m , sallien vain operaattoreiden ja käyttäjien " "jolla on ääni\n" " lähettää viestejä kanavalle. on vaadittu vain jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:127 msgid "moderate the channel" msgstr "valvo kanavaa" #: plugin.py:131 msgid "" "[]\n" "\n" " Sets -m on , making it so everyone can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Asettaa tilan -m , sallien jokaisen\n" " lähettää viestejä kanavalla. on vaadittu vain jos\n" " viestiä ei lähetetä kanavalta itseltään.\n" " " #: plugin.py:139 msgid "unmoderate the channel" msgstr "lopeta kanavan valvominen" #: plugin.py:143 msgid "" "[] []\n" "\n" " Sets the keyword in to . If is not given, " "removes\n" " the keyword requirement to join . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Asettaa avainsanan . Jos ei ole " "annettu, poistaa\n" " avainsana vaatimuksen liittymisestä. on " "vaadittu vain\n" " jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:155 msgid "change the keyword" msgstr "vaihtaa avainsanan" #: plugin.py:160 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give all the " "s\n" " you provide ops. If you don't provide any s, this will op " "you.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä antaa kaikille " "\n" " sinun tarjoamat opit. Jos et anna yhtään , tämä oppaa " "sinut.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalta\n" " itsellään.\n" " " #: plugin.py:172 msgid "op someone" msgstr "oppaa joku" #: plugin.py:176 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,halfop capability, this will give all the\n" " s you provide halfops. If you don't provide any s, " "this\n" " will give you halfops. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Jos sinulla on #kanava,halfop valtuus, tämä antaa kaikille\n" " halfopit. Jos et anna yhtään , tämä\n" "antaa sinulle halfopit. on vaadittu vain jos viestiä ei\n" "lähetetä kanavalta itseltään.\n" " " #: plugin.py:188 msgid "halfop someone" msgstr "halfoppaa joku" #: plugin.py:209 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,voice capability, this will voice all the\n" " s you provide. If you don't provide any s, this will\n" " voice you. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,voice valtuus, tämä antaa kaikille\n" " äänen. Jos et anna yhtään , tämä\n" " antaa sinulle äänen. on vaadittu vain jos viestiä ei\n" " lähetetä kanavalla itsellään.\n" " " #: plugin.py:217 msgid "voice someone" msgstr "anna jollekulle ääni" #: plugin.py:222 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa opit\n" " kaikilta annetuilta nimimerkeiltä. Jos nimimerkkejä ei ole annettu, " "poistaa\n" " opit henkilöltä joka lähettää viestin.\n" " " #: plugin.py:229 msgid "" "I cowardly refuse to deop myself. If you really want me deopped, tell me to " "op you and then deop me yourself." msgstr "" "Minä pelkurimaisesti kieltäydyn poistamasta oppeja itseltäni. Jos todella " "tahdot poistaa minulta opit, käske minun opata sinut ja sitten poista opit " "minut itse." #: plugin.py:237 msgid "deop someone" msgstr "deoppaa joku" #: plugin.py:242 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove half-" "operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " half-operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa half-op:n\n" " kaikilta annetuilta nimimerkeiltä. Jos nimimerkkejä ei anneta,\n" " viestin lähettäneeltä henkilöltä poistetaan puoli-kanavaoperaattorin " "oikeudet.\n" " " #: plugin.py:249 msgid "" "I cowardly refuse to dehalfop myself. If you really want me dehalfopped, " "tell me to op you and then dehalfop me yourself." msgstr "" "Minä pelkurimaisesti kieltäydyn poistamaan half-oppeja itseltäni. Jos haluat " "minut half-opatuksi, käske minun opata sinut ja sitten poista half-op minut " "itse." #: plugin.py:257 msgid "dehalfop someone" msgstr "poista joltakulta half-op" #: plugin.py:262 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove voice from " "all\n" " the nicks given. If no nicks are given, removes voice from the " "person\n" " sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa äänen kaikilta\n" " annetuilta nimimerkeiltä. Jos nimimerkkejä ei ole annettu, poistaa " "äänen henkilöltä,\n" " joka lähettää viestin.\n" " " #: plugin.py:274 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will cause the bot to\n" " \"cycle\", or PART and then JOIN the channel. is only " "necessary\n" " if the message isn't sent in the channel itself. If is not\n" " specified, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if neither a cycle reason nor a default part message is given.\n" " " msgstr "" "[]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä aiheuttaa botin tekemään\n" " \"cyclen\", tai POISTUMAAN ja LIITTYMÄÄN kanavalle. on " "vaadittu vain\n" " jos viestiä ei lähetetä kanavalta itsestään.\n" " " #: plugin.py:292 msgid "" "[] [, , ...] []\n" "\n" " Kicks (s) from for . If isn't " "given,\n" " uses the nick of the person making the command as the reason.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [, , ...] []\n" "\n" " Potkii . Jos ei ole " "annettu,\n" " käyttää komennon antajan nimimerkkiä syynä.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalta\n" " itseltään.\n" " " #: plugin.py:300 msgid "I cowardly refuse to kick myself." msgstr "Minä pelkurimaisesti kieltäydyn potkimasta itseäni." #: plugin.py:305 msgid "" "The reason you gave is longer than the allowed length for a KICK reason on " "this server." msgstr "" "Syy, jonka annoit on pidempi kuin sallittu pituus POTKIMIS-syyksi tällä " "palvelimella." #: plugin.py:310 msgid "kick someone" msgstr "potki joku" #: plugin.py:316 msgid "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " If you have the #channel,op capability, this will kickban " "for\n" " as many seconds as you specify, or else (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact bans only the exact hostmask; --nick bans just the nick;\n" " --user bans just the user, and --host bans just the host. You can\n" " combine these options as you choose. is a reason to give " "for\n" " the kick.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä potkii ja antaa " "porttikiellon \n" " niin moneksi sekuntiksi kuin määrität, tai muuten (jos määrität 0 " "sekuntia tai\n" " et määritä sekuntien lukumäärää) se antaa henkilölle ikuisen " "porttikiellon.\n" " --exact antaa porttikiellon vain hostmaskille; --nick antaa " "porttikiellon vain nimimerkille;\n" " --user antaa porttikiellon vain käyttäjälle, ja --host antaa " "porttikiellon vain isännälle. Voit\n" " yhdistää näitä vaihtoehtoja mielesi mukaan. on syy, jonka " "vuoksi annat\n" " potkun.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:333 msgid "kick or ban someone" msgstr "potki tai anna jollekulle porttikielto" #: plugin.py:341 msgid "" "[] [--{exact,nick,user,host}] []\n" "\n" " If you have the #channel,op capability, this will ban for\n" " as many seconds as you specify, otherwise (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact can be used to specify an exact hostmask. You can combine " "the\n" " exact, nick, user, and host options as you choose. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] []\n" " Mikäli käyttäjällä on #kanava,op valtuus, tämä antaa " "porttikiellon\n" " niin pitkäksi aikaa, kuin määritetään sekunneissa, muutoin (jos määrität 0 " "sekuntia\n" " tai et määritä sekuntejen määrää ollenkaan) henkilölle annetaan " "määrittämättömän\n" " ajan pituinen porttikielto. --exact lippua voidaan käyttää tarkan " "hostmaskin\n" " määrittämiseen. Asetuksia, exact, nimimerkki, käyttäjä ja isäntä , voiaan " "yhdistää\n" " käyttäjän halun mukaan. on tarpeellinen vain, ellei viestiä " "lähetetä \n" " kanavalla itsellään." #: plugin.py:355 plugin.py:581 msgid "ban someone" msgstr "anna jollekulle porttikielto" #: plugin.py:375 msgid "I haven't seen %s." msgstr "Minä en ole nähnyt käyttäjää %s." #: plugin.py:385 msgid "I cowardly refuse to kickban myself." msgstr "" "Minä pelkurimaisesti kieltäydyn potkimasta itseäni ja antamasta itselleni " "porttikieltoa." #: plugin.py:394 msgid "I cowardly refuse to ban myself." msgstr "Minä pelkurimaisesti kieltäydyn antamasta itselleni porttikieltoa." #: plugin.py:422 msgid "%s has %s too, you can't ban them." msgstr "" "Käyttäjälltä %s on myös valtuus %s, et voi antaa hänelle/sille porttikieltoa." #: plugin.py:433 #, fuzzy msgid "" "[] []\n" "\n" " Unbans on . If is not given, unbans\n" " any hostmask currently banned on that matches your " "current\n" " hostmask. Especially useful for unbanning yourself when you get\n" " unexpectedly (or accidentally) banned from the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Poistaa porttikiellon . Jos " "ei ole annettu, poistaa porttikiellon\n" " kaikilta hostmaskeilta, joilla on porttikielto ja jotka " "täsmäävät sinun\n" " hostmaskiisi. Etenkin hyödyllinen jos saat \n" " odottomattomasti (tai vahingossa) porttikiellon kanavalta. " "on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:453 msgid "All bans on %s matching %s have been removed." msgstr "" "Kaikki porttikiellot, jotka täsmäävät kanavalla %s käyttäjään %s on " "poistettu." #: plugin.py:457 msgid "No bans matching %s were found on %s." msgstr "Banneja, jotka täsmäävät käyttäjään %s ei löydetty kanavalla %s." #: plugin.py:460 msgid "unban someone" msgstr "poista joltakulta porttikielto" #: plugin.py:467 msgid "" "[]\n" "\n" " List all bans on the channel.\n" " If is not given, it defaults to the current channel." msgstr "" "[]\n" "\n" " Luettelee kaikki kanavan porttikiellot.\n" " Jos ei ole annettu, se on oletuksena nykyinen kanava." #: plugin.py:471 msgid "No bans." msgstr "Ei porttikieltoja." #: plugin.py:476 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will invite \n" " to join . is only necessary if the message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Jos sinulla on #kanava,op valtuus, tämä kutsuu \n" " liittymään . on vaadittu vain jos viestiä\n" " ei lähetetä kanavalla itsessään.\n" " " #: plugin.py:485 msgid "invite someone" msgstr "kutsu joku" #: plugin.py:504 msgid "%s is already in %s." msgstr "%s on jo kanavalla %s" #: plugin.py:511 msgid "There is no %s on this network." msgstr "Käyttäjä %s ei ole tässä verkossa." #: plugin.py:523 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will \"lobotomize\" " "the\n" " bot, making it silent and unanswering to all requests made in " "the\n" " channel. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä \"lobotomoi\" \n" " botin tehden sen hiljaisesksi ja vastaamattomaksi kaikkiin " "pyyntöihin,\n" " jotka on tehty kanavalla. on vaadittu vain jos viestiä " "ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:538 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will unlobotomize " "the\n" " bot, making it respond to requests made in the channel again.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa lobotimian\n" " botista, saaden sen jälleen vastaamaan kaikkiin pyyntöihin " "kanavalla.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla " "itsellään.\n" " " #: plugin.py:553 msgid "" "takes no arguments\n" "\n" " Returns the channels in which this bot is lobotomized.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa kanavat, joilla botti on lobotomoitu.\n" " " #: plugin.py:568 msgid "I'm currently lobotomized in %L." msgstr "Minut on tällä hetkellä lobotomoity seuraavilla kanavilla: %L." #: plugin.py:571 msgid "I'm not currently lobotomized in any channels that you're in." msgstr "En tällä hetkellä ole lobotomoitu millään kanavalla, jolla sinä olet." #: plugin.py:585 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will effect a\n" " persistent ban from interacting with the bot on the given\n" " (or the current hostmask associated with ). " "Other\n" " plugins may enforce this ban by actually banning users with\n" " matching hostmasks when they join. is an optional\n" " argument specifying when (in \"seconds from now\") the ban " "should\n" " expire; if none is given, the ban will never automatically " "expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä aiheuttaa\n" " pysyvän porttikiellon estääkseen bottia olemasta " "vuorovaikutuksessa annetun\n" " kanssa (tai nykyisen hostmaskin. " "Toiset\n" " lisäosat saattavat pakottaa tämän porttikiellon antamalla " "porttikiellon käyttäjille, joiden\n" " hostmaskit täsmäävät lisättyyn porttikieltoon. on " "vaihtoehtoinen\n" " parametri , kun määritetään ( \"sekunteja alkaen nyt\") milloin " "porttikielto\n" " vanhentuu; jos mitään ei ole annettu, porttikielto ei ikinä " "vanhene automaattisesti.\n" " on vaadittu vain jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:605 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ban on . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa\n" " pysyvän porttikiellon . on vaadittu vain " "jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:617 msgid "There are no persistent bans for that hostmask." msgstr "Tuolla hostmaskilla ei ole pysyvää porttikieltoa." #: plugin.py:622 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will show you the\n" " current persistent bans on the that match the given\n" " mask, if any (returns all of them otherwise).\n" " Note that you can use * as a wildcard on masks and \\* to match\n" " actual * in masks\n" " " msgstr "" "[] \n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa\n" " pysyvän porttikiellon . on vaadittu vain " "jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:640 msgid "%q (expires %t)" msgstr "%q (venhentuu %t)" #: plugin.py:643 msgid "%q (never expires)" msgstr "%q (ei vanhene ikinä)" #: plugin.py:647 msgid "There are no persistent bans on %s." msgstr "Kanavalla %s ei ole pysyviä porttikieltoja." #: plugin.py:654 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will set a " "persistent\n" " ignore on or the hostmask currently\n" " associated with . is an optional argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä asettaa pysyvän\n" " huomiotta jättämisen tai hostmaskiin\n" " joka on käytössä. on vaihtoehtoinen " "parametri\n" " kun määritetään ( \"sekunteja alkaen nyt\")milloin huomiotta " "jättäminen vanhentuu; jos\n" " sitä ei ole annettu, huomiotta jättäminen ei ikinä vanhene " "automaattisesti.\n" " on vaadittu vain jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:672 #, fuzzy msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ignore on in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa\n" " pysyvän huomiotta jättämisen kanavalla. " "on vaadittu vain jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:684 msgid "There are no ignores for that hostmask." msgstr "Tuolla hostmaskilla ei ole pysyviä huomiotta jättämisiä." #: plugin.py:689 msgid "" "[]\n" "\n" " Lists the hostmasks that the bot is ignoring on the given " "channel.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" " Luettelee hostmaskit, jotka ovat botin huomiotta jättämis " "listalla annetulla kanavalla.\n" " on vaadittu vain, jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:698 msgid "I'm not currently ignoring any hostmasks in %q" msgstr "En tällä hetkellä jätä mitään hostmaskia huomiotta kanavalla %q." #: plugin.py:709 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give the\n" " (or the user to whom maps)\n" " the capability in the channel. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä antaa \n" " (tai käyttäjälle, jonka " "määrittää)\n" " kanavalla. on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:725 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will take from the\n" " user currently identified as (or the user to whom " "\n" " maps) the capability in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa\n" " tällä hetkellä tunnistautuneelta käyttäjältä (tai " "käyttäjältä, jonka \n" " täsmää) kanavalla. on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:744 msgid "That user didn't have the %L %s." msgstr "Tuolla käyttäjällä ei ollut valtuutta %L kanavalla %s." #: plugin.py:753 msgid "" "[] {True|False}\n" "\n" " If you have the #channel,op capability, this will set the " "default\n" " response to non-power-related (that is, not {op, halfop, " "voice})\n" " capabilities to be the value you give. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {True|False}\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä asettaa oletus\n" " ei voimaan-liittyvän (jokin muu kuin {op, halfop, voice}\n" " valtuusarvon, jonka annat. on vaadittu vain\n" " jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:771 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will add the " "channel\n" " capability for all users in the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä lisää\n" " kaikille käyttäjille kanavalla. on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:786 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will unset the " "channel\n" " capability so each user's specific capability or " "the\n" " channel default capability will take precedence. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa kanavan\n" " joten jokaisen käyttäjäkohtainen valtuus tai\n" " kanavan oletusvaltuus otetaan käyttöön. on vaadittu\n" " vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:802 msgid "capability" msgstr "valtuus" #: plugin.py:805 msgid "I do not know about the %L %s." msgstr "En tiedä valtuudesta %L kanavalla %s." #: plugin.py:812 msgid "" "[]\n" "\n" " Returns the capabilities present on the . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa listan valtuuksista, jotka ovat . " "on\n" " vaadittu vain, jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:824 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will disable the " "\n" " in . If is provided, will be disabled " "only\n" " for that plugin. If only is provided, all commands in the\n" " given plugin will be disabled. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä poistaa käytöstä\n" " . Jos on annettu, poistetaan " "käytöstä\n" " vain siitä lisäosasta. Jos vain on annettu, kaikki " "komennot\n" " annetusta lisäosasta poistetaan käytöstä. on vaadittu vain " "jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:840 plugin.py:879 msgid "The %s plugin does not have a command called %s." msgstr "%s lisäosassa ei ole komentoa %s." #: plugin.py:847 plugin.py:886 msgid "No plugin or command named %s could be found." msgstr "Lisäosaa tai komentoa nimeltä %s ei löytynyt." #: plugin.py:863 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will enable the " "\n" " in if it has been disabled. If is provided,\n" " will be enabled only for that plugin. If only " "is\n" " provided, all commands in the given plugin will be enabled. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" " Jos sinulla on #kanava,op valtuus, tämä ottaa käyttöön\n" " jos se on poistettu käytöstä. Jos on " "annettu,\n" " otetaan käyttöön vain siinä lisäosassa. Jos vain " " on\n" " annettu, kaikki komennot annetussa lisäosassa otetaan käyttööön. " "\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:900 msgid "%s was not disabled." msgstr "%s ei ollut poistettu käytöstä." #: plugin.py:909 msgid "" "[] [--count]\n" "\n" " Returns the nicks in . is only necessary if the\n" " message isn't sent in the channel itself. Returns only the number " "of\n" " nicks if --count option is provided.\n" " " msgstr "" "[]\n" "\n" " Palauttaa listan nimimerkit, jotka ovat . on " "vaadittu vain jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:931 msgid "You don't have access to that information." msgstr "Sinulla ei ole pääsyoikeutta tuohon tietoon." #: plugin.py:946 msgid "" "Internal message for notifying all the #channel,ops in a channel of\n" " a given situation." msgstr "" "Sisäinen viesti huomattamaan kaikkia henkilöitä, joilla on #kanava,op " "valtuus kanavalla,\n" " annetusta tilanteesta." #: plugin.py:949 msgid "Alert to all %s ops: %s" msgstr "Hälytys kaikille kanavan %s operaattoreille: %s" #: plugin.py:951 msgid " (from %s)" msgstr "(lähettänyt %s)" #: plugin.py:963 msgid "" "[] \n" "\n" " Sends to all the users in who have the ," "op\n" " capability.\n" " " msgstr "" "[] \n" "\n" " Lähettää kaikille käyttäjille, jotka ovat ja " "joilla on ,op\n" " valtuus.\n" " " #: plugin.py:973 msgid "" "[] []\n" "\n" " Tells the bot to part the list of channels you give it. " "is\n" " only necessary if you want the bot to part a channel other than the\n" " current channel. If is specified, use it as the part\n" " message. Otherwise, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if no default is configured.\n" " " msgstr "" #: plugin.py:994 #, fuzzy msgid "I'm not in %s." msgstr "Minä en ole nähnyt käyttäjää %s." #~ msgid "" #~ "I cowardly refuse to devoice myself. If you really want me devoiced, " #~ "tell me to op you and then devoice me yourself." #~ msgstr "" #~ "Minä pelkurimaisesti kieltäydyn poistamasta ääntä itseltäni. Jos todella " #~ "tahdot poistaa minulta äänen, käske minut oppaamaan sinut ja sitten " #~ "poista ääni minulta itse." #~ msgid "" #~ "[]\n" #~ "\n" #~ " If you have the #channel,op capability, this will show you " #~ "the\n" #~ " current persistent bans on the .\n" #~ " " #~ msgstr "" #~ "[]\n" #~ "\n" #~ " Jos sinulla on #kanava,op valtuus, tämä näyttää\n" #~ " pysyvät bannit #kanavalla.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/locales/fr.po0000644000175000017500000007664614535072470017565 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:49 msgid "" "Determines whether the bot will always try to\n" " rejoin a channel whenever it's kicked from the channel." msgstr "" "Détermine si le bot tentera toujours de rejoindre un canal duquel il a été " "kické." #: config.py:52 msgid "" "Determines whether the output of 'nicks' will\n" " be sent in private. This prevents mass-highlights of a channel's users,\n" " accidental or on purpose." msgstr "" #: config.py:56 msgid "" "Determines how many seconds the bot will wait\n" " before rejoining a channel if kicked and\n" " supybot.plugins.Channel.alwaysRejoin is on." msgstr "" #: config.py:60 msgid "" "Determines what part message should be\n" " used by default. If the part command is called without a part " "message,\n" " this will be used. If this value is empty, then no part message " "will\n" " be used (they are optional in the IRC protocol). The standard\n" " substitutions ($version, $nick, etc.) are all handled appropriately." msgstr "" #: plugin.py:48 msgid "" "This plugin provides various commands for channel management, such\n" " as setting modes and channel-wide bans/ignores/capabilities. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:94 msgid "" "[] [ ...]\n" "\n" " Sets the mode in to , sending the arguments given.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Définit le sur le canal, en envoyant les arguments donnés. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:101 msgid "change the mode" msgstr "changer le mode" #: plugin.py:105 msgid "" "[] []\n" "\n" " Sets the channel limit to . If is 0, or isn't " "given,\n" " removes the channel limit. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Définit la du canal. Si est 0, ou n'est pas donné, " "supprime la limite du canal. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:115 msgid "change the limit" msgstr "changer la limite" #: plugin.py:120 msgid "" "[]\n" "\n" " Sets +m on , making it so only ops and voiced users can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Définit le +m sur le canal, ce qui fait que seuls les ops et les " "utilisateurs voicés peuvent envoyer des message au canal. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:127 msgid "moderate the channel" msgstr "modérer le canal" #: plugin.py:131 msgid "" "[]\n" "\n" " Sets -m on , making it so everyone can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Définit le -m sur le , ce qui fait que tout le monde peut à nouveau " "envoyer des messages au canal. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:139 msgid "unmoderate the channel" msgstr "démodérer le canal" #: plugin.py:143 msgid "" "[] []\n" "\n" " Sets the keyword in to . If is not given, " "removes\n" " the keyword requirement to join . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Définit la du . Si la n'est pas donnée, retire la clef " "du . n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:155 msgid "change the keyword" msgstr "changer la clef" #: plugin.py:160 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give all the " "s\n" " you provide ops. If you don't provide any s, this will op " "you.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,op, ceci vous permet de oper tous les " "s. Si vous ne fournissez aucun , ceci vous opera. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:172 msgid "op someone" msgstr "oper quelqu'un" #: plugin.py:176 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,halfop capability, this will give all the\n" " s you provide halfops. If you don't provide any s, " "this\n" " will give you halfops. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,halfop, ceci vous permet de halfoper tous " "les s. Si vous ne fournissez aucun , ceci vous halfopera. " " n'est nécessaire que si le message n'est pas envoyé sur le canal lui-" "même." #: plugin.py:188 msgid "halfop someone" msgstr "halfoper quelqu'un" #: plugin.py:209 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,voice capability, this will voice all the\n" " s you provide. If you don't provide any s, this will\n" " voice you. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,voice, ceci vous permet de voicer tous les " "s. Si vous ne fournissez aucun , ceci vous voicera. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:217 msgid "voice someone" msgstr "voicer quelqu'un" #: plugin.py:222 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,op, ceci vous permet de déoper tous les " "s. Si vous ne fournissez aucun , ceci vous déopera. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:229 msgid "" "I cowardly refuse to deop myself. If you really want me deopped, tell me to " "op you and then deop me yourself." msgstr "" "Je suis trop couard pour me déoper moi-même. Si vous voulez vraiment me " "déoper, dîtes-moi de vous oper, et déopez-moi vous-même." #: plugin.py:237 msgid "deop someone" msgstr "déoper quelqu'un" #: plugin.py:242 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove half-" "operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " half-operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,op, ceci vous permet de déhalfoper tous les " "s. Si vous ne fournissez aucun , ceci vous déhalfopera. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:249 msgid "" "I cowardly refuse to dehalfop myself. If you really want me dehalfopped, " "tell me to op you and then dehalfop me yourself." msgstr "" "Je suis trop couard pour me déhalfoper moi-même. Si vous voulez vraiment me " "déhalfoper, dîtes-moi de vous oper, et déhalfopez-moi vous-même." #: plugin.py:257 msgid "dehalfop someone" msgstr "déhalfoper quelqu'un" #: plugin.py:262 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove voice from " "all\n" " the nicks given. If no nicks are given, removes voice from the " "person\n" " sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,op, ceci vous permet de dévoicer tous les " "s. Si vous ne fournissez aucun , ceci vous dévoicera. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:274 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will cause the bot to\n" " \"cycle\", or PART and then JOIN the channel. is only " "necessary\n" " if the message isn't sent in the channel itself. If is not\n" " specified, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if neither a cycle reason nor a default part message is given.\n" " " msgstr "" "[]\n" "\n" "Si vous avez la capacité #canal,op,, ceci fera \"cycler\" le bot, ou un PART " "et un JOIN. n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:292 msgid "" "[] [, , ...] []\n" "\n" " Kicks (s) from for . If isn't " "given,\n" " uses the nick of the person making the command as the reason.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [, , ...] []\n" "\n" "Kicke le(s) (s) du pour la . Si la raison n'est pas " "donnée, le nick de la personne envoyant la commande est utilisé. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:300 msgid "I cowardly refuse to kick myself." msgstr "Je suis trop couard pour me kicker moi-même." #: plugin.py:305 msgid "" "The reason you gave is longer than the allowed length for a KICK reason on " "this server." msgstr "" "La raison que vous avez donnée est plus longue que la taille autorisée pour " "une raison de KICK sur ce serveur." #: plugin.py:310 msgid "kick someone" msgstr "kicker quelqu'un" #: plugin.py:316 msgid "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " If you have the #channel,op capability, this will kickban " "for\n" " as many seconds as you specify, or else (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact bans only the exact hostmask; --nick bans just the nick;\n" " --user bans just the user, and --host bans just the host. You can\n" " combine these options as you choose. is a reason to give " "for\n" " the kick.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" "Si vous avez la capacité #canal,op, ceci kickbannira pendant le " "nombre de que vous avez spécifiées, ou, si vous n'avez rien " "indiqué (ou indiqué 0), bannira la personne indéfiniment.--exact ne bannit " "que le masque d'hôte exacte ; --nick bannit le nick ; --user bannit " "l'ident ; --host bannit l'hôte. Vous pouvez combiner ces options à votre gré." " est une raison que vous donnez pour le kick. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:333 msgid "kick or ban someone" msgstr "kicker ou bannir quelqu'un" #: plugin.py:341 #, fuzzy msgid "" "[] [--{exact,nick,user,host}] []\n" "\n" " If you have the #channel,op capability, this will ban for\n" " as many seconds as you specify, otherwise (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact can be used to specify an exact hostmask. You can combine " "the\n" " exact, nick, user, and host options as you choose. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] []\n" "\n" "Si vous avez la capacité #canal,op, ceci bannira pendant le nombre de " " que vous avez spécifiées, ou, si vous n'avez rien indiqué (ou " "indiqué 0), bannira la personne indéfiniment.--exact ne bannit que le masque " "d'hôte exacte ; --nick bannit le nick ; --user bannit l'ident ; --host " "bannit l'hôte. Vous pouvez combiner ces options à votre gré. est une " "raison que vous donnez pour le kick. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:355 plugin.py:581 msgid "ban someone" msgstr "bannir quelqu'un" #: plugin.py:375 msgid "I haven't seen %s." msgstr "Je n'ai jamais vu %s." #: plugin.py:385 msgid "I cowardly refuse to kickban myself." msgstr "Je suis trop couard pour me kickbannir moi-même." #: plugin.py:394 msgid "I cowardly refuse to ban myself." msgstr "Je suis trop couard pour me bannir moi-même." #: plugin.py:422 msgid "%s has %s too, you can't ban them." msgstr "%s est aussi %s, je ne peux le/la bannir." #: plugin.py:433 msgid "" "[] []\n" "\n" " Unbans on . If is not given, unbans\n" " any hostmask currently banned on that matches your " "current\n" " hostmask. Especially useful for unbanning yourself when you get\n" " unexpectedly (or accidentally) banned from the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Débannit le du . Si le n'est pas " "donné, débannit tous les masques d'hôte qui correspondent à votre masque " "d'hôte actuel. Particulièrement utile lorsque vous avez été banni(e) de " "manière inatendue ou accidentelle d'un canal. n'est nécessaire que " "si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:453 msgid "All bans on %s matching %s have been removed." msgstr "Tous les bannissements sur %s correspondant à %s ont été supprimés." #: plugin.py:457 msgid "No bans matching %s were found on %s." msgstr "Aucun bannissement correspondant à %s n'a été trouvé sur %s." #: plugin.py:460 msgid "unban someone" msgstr "débannir quelqu'un" #: plugin.py:467 msgid "" "[]\n" "\n" " List all bans on the channel.\n" " If is not given, it defaults to the current channel." msgstr "" "[]Liste tous les bannissements du salon. Si n'est pas donné, " "il correspond au canal actuel." #: plugin.py:471 msgid "No bans." msgstr "Pas de bannissement." #: plugin.py:476 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will invite \n" " to join . is only necessary if the message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Si vous avez la capacité #canal,op, ceci invitera à rejoindre le " ". n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:485 msgid "invite someone" msgstr "inviter quelqu'un" #: plugin.py:504 msgid "%s is already in %s." msgstr "%s est déjà sur %s." #: plugin.py:511 msgid "There is no %s on this network." msgstr "Il n'y a aucun %s sur ce réseau." #: plugin.py:523 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will \"lobotomize\" " "the\n" " bot, making it silent and unanswering to all requests made in " "the\n" " channel. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" "Si vous avez la capacité #canal,op, ceci \"lobotomisera\" le bot, c'est à " "dire qu'il sera silencieux et ne répondra plus à aucune requête faite sur le " "canal. n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:538 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will unlobotomize " "the\n" " bot, making it respond to requests made in the channel again.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Si vous avez la capacité #canal,op, ceci \"dé-lobotomisera\" le bot, c'est à " "dire qu'il ne sera plus silencieux et répondra à nouveau aux requêtes faites " "sur le canal. n'est nécessaire que si le message n'est pas envoyé " "sur le canal lui-même." #: plugin.py:553 msgid "" "takes no arguments\n" "\n" " Returns the channels in which this bot is lobotomized.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne les canaux sur lesquels le bot est lobotomisé." #: plugin.py:568 msgid "I'm currently lobotomized in %L." msgstr "Je suis actuellement lobotomisé sur %L." #: plugin.py:571 msgid "I'm not currently lobotomized in any channels that you're in." msgstr "Je ne suis actuellement lobotomisé sur aucun canal où vous êtes." #: plugin.py:585 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will effect a\n" " persistent ban from interacting with the bot on the given\n" " (or the current hostmask associated with ). " "Other\n" " plugins may enforce this ban by actually banning users with\n" " matching hostmasks when they join. is an optional\n" " argument specifying when (in \"seconds from now\") the ban " "should\n" " expire; if none is given, the ban will never automatically " "expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Si vous avez la capacité #canal,op, ceci affectera un bannisement persistant " "au spécifié. D'autres plugins pourraient renforcer ce " "bannissement en bannissant vraiment les utilisateurs correspondant à ce " "masque d'hôte lorsqu'ils entrent. est un argument option, " "correspondant à la durée, en secondes, que doit avoir le bannissement." #: plugin.py:605 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ban on . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Si vous avez la capacité #canal,op, ceci supprimera le bannissement " "persistant sur le . n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:617 msgid "There are no persistent bans for that hostmask." msgstr "Il n'y a pas de bannissement persistant pour ce masque d'hôte." #: plugin.py:622 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will show you the\n" " current persistent bans on the that match the given\n" " mask, if any (returns all of them otherwise).\n" " Note that you can use * as a wildcard on masks and \\* to match\n" " actual * in masks\n" " " msgstr "" "[] \n" "\n" "Si vous avez la capacité #canal,op, ceci supprimera le bannissement " "persistant sur le . n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:640 msgid "%q (expires %t)" msgstr "%q (expire dans %t)" #: plugin.py:643 msgid "%q (never expires)" msgstr "%q (n'expire jamais)" #: plugin.py:647 msgid "There are no persistent bans on %s." msgstr "Il n'y a pas de bannissement persistant sur %s." #: plugin.py:654 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will set a " "persistent\n" " ignore on or the hostmask currently\n" " associated with . is an optional argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Vous permet d'ajouter un ignore persistant sur le , ou le " "masque associé au , à condition d'avoir la capacité #canal,op. " " est un argument optionnel, correspondant au nombre de secondes " "durant lesquelles l'ignore sera effectif ; si il n'est pas donné, l'ignore " "n'expirera jamais. n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:672 #, fuzzy msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ignore on in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Si vous avez la capacité #canal,op, ceci supprimera le masque d'ignorance " "persistant du sur le canal. n'est nécessaire que si " "le message n'est pas envoyé sur le canal lui-même." #: plugin.py:684 msgid "There are no ignores for that hostmask." msgstr "Il n'y a pas d'ignorance pour ce masque d'hôte." #: plugin.py:689 msgid "" "[]\n" "\n" " Lists the hostmasks that the bot is ignoring on the given " "channel.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" "Liste les masques d'hôte que le bot ignore sur le canal donné. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:698 msgid "I'm not currently ignoring any hostmasks in %q" msgstr "Je n'ignore actuellement aucun masque d'hôte sur %q." #: plugin.py:709 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give the\n" " (or the user to whom maps)\n" " the capability in the channel. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la permission #canal,op : donne à l'utilisateur (ou celui " "ayant actuellement le ) la sur le canal. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:725 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will take from the\n" " user currently identified as (or the user to whom " "\n" " maps) the capability in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Vous permet de prendre la de l'utilisateur (ou celui à qui " "appartient le , à condition que vous ayez la capacité #canal," "op. n'est nécessaire que si le message n'est pas envoyé sur le canal " "lui-même." #: plugin.py:744 msgid "That user didn't have the %L %s." msgstr "Cet utilisateur n'a pas les %L %s." #: plugin.py:753 msgid "" "[] {True|False}\n" "\n" " If you have the #channel,op capability, this will set the " "default\n" " response to non-power-related (that is, not {op, halfop, " "voice})\n" " capabilities to be the value you give. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {on|off}\n" "\n" "Si vous avez la capacité #canal,op, cela vous permet de définir la réponse " "par défaut relative aux problèmes de pouvoir nécessaire op, halfop, voice). " " n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:771 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will add the " "channel\n" " capability for all users in the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,op ceci ajoutera la de canal à " "tous les utilisateurs du canal. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:786 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will unset the " "channel\n" " capability so each user's specific capability or " "the\n" " channel default capability will take precedence. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Si vous avez la capacité #canal,op ceci désactivera le fait que tous les " "utilisateurs du canal aient la de canal. n'est nécessaire " "que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:802 msgid "capability" msgstr "capacité" #: plugin.py:805 msgid "I do not know about the %L %s." msgstr "Je ne sais rien à propos des %L %s." #: plugin.py:812 msgid "" "[]\n" "\n" " Returns the capabilities present on the . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne les capacité présentes sur le . n'est nécessaire que " "si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:824 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will disable the " "\n" " in . If is provided, will be disabled " "only\n" " for that plugin. If only is provided, all commands in the\n" " given plugin will be disabled. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" " Si vous avez la capacité #canal,op, ceci désactive la sur le " ". Si le est fourni, toutes les commandes de ce plugin seront " "désactivées. n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:840 plugin.py:879 msgid "The %s plugin does not have a command called %s." msgstr "Le plugin %s n'a pas de commande appelée %s." #: plugin.py:847 plugin.py:886 msgid "No plugin or command named %s could be found." msgstr "Aucun plugin ou commande appelé %s n'a pû être trouvé." #: plugin.py:863 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will enable the " "\n" " in if it has been disabled. If is provided,\n" " will be enabled only for that plugin. If only " "is\n" " provided, all commands in the given plugin will be enabled. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" "Si vous avez la capacité #canal,op, ceci activera la sur " "le , si elle a auparavant été désactivée. Si le seule " "la commande de ce plugin sera activée. n'est nécessaire " "n'est pas envoyé sur le canal lui-même." #: plugin.py:900 msgid "%s was not disabled." msgstr "%s n'était pas désactivé." #: plugin.py:909 msgid "" "[] [--count]\n" "\n" " Returns the nicks in . is only necessary if the\n" " message isn't sent in the channel itself. Returns only the number " "of\n" " nicks if --count option is provided.\n" " " msgstr "" "[] [--count]\n" "\n" "Retourne les nick sur le . n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même. Ne retourne que le nombre de nicks " "si --count est donné." #: plugin.py:931 msgid "You don't have access to that information." msgstr "Vous n'avez pas accès à cette information" #: plugin.py:946 msgid "" "Internal message for notifying all the #channel,ops in a channel of\n" " a given situation." msgstr "" "Message interne pour notifier tous les #canal,ops sur un canal d'une " "situation donnée." #: plugin.py:949 msgid "Alert to all %s ops: %s" msgstr "Alerte à tous les ops de %s : %s" #: plugin.py:951 msgid " (from %s)" msgstr "(de %s)" #: plugin.py:963 msgid "" "[] \n" "\n" " Sends to all the users in who have the ," "op\n" " capability.\n" " " msgstr "" "[] \n" "\n" "Envoie le à tous les utilisateurs sur qui ont la capacité " "#canal,op." #: plugin.py:973 msgid "" "[] []\n" "\n" " Tells the bot to part the list of channels you give it. " "is\n" " only necessary if you want the bot to part a channel other than the\n" " current channel. If is specified, use it as the part\n" " message. Otherwise, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if no default is configured.\n" " " msgstr "" #: plugin.py:994 #, fuzzy msgid "I'm not in %s." msgstr "Je n'ai jamais vu %s." #~ msgid "" #~ "I cowardly refuse to devoice myself. If you really want me devoiced, " #~ "tell me to op you and then devoice me yourself." #~ msgstr "" #~ "Je suis trop couard pour me dévoicer moi-même. Si vous voulez vraiment me " #~ "dévoicer, dîtes-moi de vous oper, et dévoicez-moi vous-même." #~ msgid "" #~ "[]\n" #~ "\n" #~ " If you have the #channel,op capability, this will show you " #~ "the\n" #~ " current persistent bans on the .\n" #~ " " #~ msgstr "" #~ "[]\n" #~ "\n" #~ "Si vous avez la capacité #canal,op, ceci vous affichera la liste des " #~ "bannissements persistants sur le canal." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/locales/hu.po0000644000175000017500000010020314535072470017543 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Channel\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 14:49+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:49 msgid "" "Determines whether the bot will always try to\n" " rejoin a channel whenever it's kicked from the channel." msgstr "" "Meghatározza, hogy a bot mindig megpróbáljon-e visszatérni egy csatornára, " "ha kirúgják onnan." #: config.py:52 msgid "" "Determines whether the output of 'nicks' will\n" " be sent in private. This prevents mass-highlights of a channel's users,\n" " accidental or on purpose." msgstr "" #: config.py:56 msgid "" "Determines how many seconds the bot will wait\n" " before rejoining a channel if kicked and\n" " supybot.plugins.Channel.alwaysRejoin is on." msgstr "" #: config.py:60 msgid "" "Determines what part message should be\n" " used by default. If the part command is called without a part " "message,\n" " this will be used. If this value is empty, then no part message " "will\n" " be used (they are optional in the IRC protocol). The standard\n" " substitutions ($version, $nick, etc.) are all handled appropriately." msgstr "" #: plugin.py:48 msgid "" "This plugin provides various commands for channel management, such\n" " as setting modes and channel-wide bans/ignores/capabilities. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:94 msgid "" "[] [ ...]\n" "\n" " Sets the mode in to , sending the arguments given.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " módját -ra állítja, elküldve a kapott paramétereket. " " csak akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:101 msgid "change the mode" msgstr "megváltoztatni a módot" #: plugin.py:105 msgid "" "[] []\n" "\n" " Sets the channel limit to . If is 0, or isn't " "given,\n" " removes the channel limit. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "A csatorna korlátját -ra állítja. Ha 0 vagy nem " "meghatározott, eltávolítja a csatorna korlátját. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:115 msgid "change the limit" msgstr "megváltoztatni a korlátot" #: plugin.py:120 msgid "" "[]\n" "\n" " Sets +m on , making it so only ops and voiced users can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "-t +m-re állítja, hogy csak operátorok és hanggal rendelkező " "felhasználók küldhessenek üzeneteket a csatornára. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:127 msgid "moderate the channel" msgstr "moderálni a csatornát" #: plugin.py:131 msgid "" "[]\n" "\n" " Sets -m on , making it so everyone can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "-t -m-re állítja, hogy mindenki küldhessen üzeneteket a " "csatornára. csak akkor szükséges, ha az üzenet nem a csatornában " "van elküldve." #: plugin.py:139 msgid "unmoderate the channel" msgstr "kikapcsolni a moderálást a csatornán" #: plugin.py:143 msgid "" "[] []\n" "\n" " Sets the keyword in to . If is not given, " "removes\n" " the keyword requirement to join . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] kulcsát -ra állítja. Ha nem " "meghatározott, eltávolíta a kulcsot -ról. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:155 msgid "change the keyword" msgstr "megváltoztatni a kulcsot" #: plugin.py:160 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give all the " "s\n" " you provide ops. If you don't provide any s, this will op " "you.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez operátor státuszt ad minden " "-nek, amit megadtál. Ha nem adsz meg -et, ez neked ad operátor " "státuszt. csak akkor szükséges, ha az üzenet nem a csatornában " "van elküldve." #: plugin.py:172 msgid "op someone" msgstr "operátor státuszt adni valakinek" #: plugin.py:176 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,halfop capability, this will give all the\n" " s you provide halfops. If you don't provide any s, " "this\n" " will give you halfops. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,halfop képességgel, ez fél-operátor státuszt ad " "minden -nek, amit megadtál. Ha nem adsz meg -et, ez neked ad fél-" "operátor státuszt. csak akkor szükséges, ha az üzenet nem a " "csatornában van elküldve." #: plugin.py:188 msgid "halfop someone" msgstr "fél-operátor státuszt adni valakinek" #: plugin.py:209 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,voice capability, this will voice all the\n" " s you provide. If you don't provide any s, this will\n" " voice you. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Ha rendelkezel a #csatorna,voice képességgel, ez hangot ad minden megadott " "-nek. Ha nem adsz meg -et, ez neked ad hangot. csak " "akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:217 msgid "voice someone" msgstr "hangot adni valakinek" #: plugin.py:222 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja az operátor " "státuszt minden megadott névről. Ha nincs név megadva, rólad távolítja el az " "operátor státuszt. csak akkor szükséges, ha az üzenet nem a " "csatornában van elküldve." #: plugin.py:229 msgid "" "I cowardly refuse to deop myself. If you really want me deopped, tell me to " "op you and then deop me yourself." msgstr "" "Gyáván megtagadom, hogy eltávolítsam magamról az operátor státuszt. Ha el " "akarod távolítani rólam az operátor státuszt, mondd, hogy adjak operátor " "státuszt neked, és távolítsd el rólam." #: plugin.py:237 msgid "deop someone" msgstr "eltávolítani az operátor státuszt valakiről" #: plugin.py:242 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove half-" "operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " half-operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,halfop képességgel, ez eltávolítja a fél-operátor " "státuszt minden -ről, amit megadtál. Ha nincs név megadva, ez rólad " "távolítja el a fél-operátor státuszt. csak akkor szükséges, ha az " "üzenet nem a csatornában van elküldve." #: plugin.py:249 msgid "" "I cowardly refuse to dehalfop myself. If you really want me dehalfopped, " "tell me to op you and then dehalfop me yourself." msgstr "" "Gyáván megtagadom, hogy eltávolítsam magamról a fél-operátor státuszt. Ha el " "akarod távolítani rólam az operátor státuszt, mondd, hogy adjak operátor " "státuszt neked, és távolítsd el rólam a fél-operátor státuszt." #: plugin.py:257 msgid "dehalfop someone" msgstr "eltávolítani a fél-operátor státuszt valakiről" #: plugin.py:262 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove voice from " "all\n" " the nicks given. If no nicks are given, removes voice from the " "person\n" " sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja a hangot minden " "-ről, amit megadtál. Ha nincs név megadva, ez rólad távolítja el a " "hangot.. csak akkor szükséges, ha az üzenet nem a csatornában van " "elküldve." #: plugin.py:274 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will cause the bot to\n" " \"cycle\", or PART and then JOIN the channel. is only " "necessary\n" " if the message isn't sent in the channel itself. If is not\n" " specified, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if neither a cycle reason nor a default part message is given.\n" " " msgstr "" "[]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ennek hatására a bot kilép majd " "visszalép a csaotnára. csak akkor szükséges, ha az üzenet nem a " "csatornában van elküldve." #: plugin.py:292 msgid "" "[] [, , ...] []\n" "\n" " Kicks (s) from for . If isn't " "given,\n" " uses the nick of the person making the command as the reason.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [, , ...] []\n" "Kirúgja (ek)-et -ról -ért. Ha nincs megadva, a " "parancsot használó ember nevét használja okként. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:300 msgid "I cowardly refuse to kick myself." msgstr "Gváván megtagadom, hogy kirúgjam magam." #: plugin.py:305 msgid "" "The reason you gave is longer than the allowed length for a KICK reason on " "this server." msgstr "" "A megadott ok hosszabb a KICK parancs megengedett hosszúságánál ezen a " "szerveren." #: plugin.py:310 msgid "kick someone" msgstr "kirúgni valakit" #: plugin.py:316 msgid "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " If you have the #channel,op capability, this will kickban " "for\n" " as many seconds as you specify, or else (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact bans only the exact hostmask; --nick bans just the nick;\n" " --user bans just the user, and --host bans just the host. You can\n" " combine these options as you choose. is a reason to give " "for\n" " the kick.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez kirúgja és kitiltja -et a " "megadott időre, vagy (ha 0 másodpercet adsz meg vagy nem adsz meg " "másodpercet) a végtelenségig tiltja a megadott embert. --exact csak a pontos " "hosztot tiltja ki; --nick csak a nevet tiltja ki; --user csak a felhasználót " "tiltja ki, és --host csak a hosztot tiltja ki. Kombinálhatod az opciókat " "ahogy szeretnéd. egy ok a kirúgáshoz." #: plugin.py:333 msgid "kick or ban someone" msgstr "kirúgni vagy kitiltani valakit" #: plugin.py:341 #, fuzzy msgid "" "[] [--{exact,nick,user,host}] []\n" "\n" " If you have the #channel,op capability, this will ban for\n" " as many seconds as you specify, otherwise (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact can be used to specify an exact hostmask. You can combine " "the\n" " exact, nick, user, and host options as you choose. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez kirúgja és kitiltja -et a " "megadott időre, vagy (ha 0 másodpercet adsz meg vagy nem adsz meg " "másodpercet) a végtelenségig tiltja a megadott embert. --exact csak a pontos " "hosztot tiltja ki; --nick csak a nevet tiltja ki; --user csak a felhasználót " "tiltja ki, és --host csak a hosztot tiltja ki. Kombinálhatod az opciókat " "ahogy szeretnéd. egy ok a kirúgáshoz." #: plugin.py:355 plugin.py:581 #, fuzzy msgid "ban someone" msgstr "eltávolítani a tiltást valakiről" #: plugin.py:375 msgid "I haven't seen %s." msgstr "Nem láttam %s-t." #: plugin.py:385 msgid "I cowardly refuse to kickban myself." msgstr "Gyáván megtagadom, hogy kirúgjam és kitiltsam magam." #: plugin.py:394 msgid "I cowardly refuse to ban myself." msgstr "Gyáván megtagadom, hogy kitiltsam magam." #: plugin.py:422 msgid "%s has %s too, you can't ban them." msgstr "%s-nek is van %s, nem tilthatod ki őt." #: plugin.py:433 #, fuzzy msgid "" "[] []\n" "\n" " Unbans on . If is not given, unbans\n" " any hostmask currently banned on that matches your " "current\n" " hostmask. Especially useful for unbanning yourself when you get\n" " unexpectedly (or accidentally) banned from the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Eltávolítja a tiltást -ról -ban. Ha nincs megadva, " "az összes tiltást eltávolítja -ról, ami illeszkedik rád. Főleg " "akkor hasznos, hogy eltávolítsd a tiltást magadról, ha váratlanul (vagy " "véletlenül) ki lettél tiltva a csatornáról. csak akkor szükséges, " "ha az üzenet nem a csatornában van elküldve." #: plugin.py:453 msgid "All bans on %s matching %s have been removed." msgstr "Minden tiltás %s-ban, ami illeszkedik %s-ra el lett távolítva." #: plugin.py:457 msgid "No bans matching %s were found on %s." msgstr "Nem található %s-ra illeszkedő tiltás %s-ban." #: plugin.py:460 msgid "unban someone" msgstr "eltávolítani a tiltást valakiről" #: plugin.py:467 msgid "" "[]\n" "\n" " List all bans on the channel.\n" " If is not given, it defaults to the current channel." msgstr "" #: plugin.py:471 msgid "No bans." msgstr "" #: plugin.py:476 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will invite \n" " to join . is only necessary if the message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[ \n" "\n" "Ha rendelkezel a #csatorna,op képességgel, meghívja -et -ra. " " csak akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:485 msgid "invite someone" msgstr "meghívni valakit" #: plugin.py:504 msgid "%s is already in %s." msgstr "%s már %s-ban van." #: plugin.py:511 msgid "There is no %s on this network." msgstr "Nincs %s ezen a hálózaton." #: plugin.py:523 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will \"lobotomize\" " "the\n" " bot, making it silent and unanswering to all requests made in " "the\n" " channel. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez \"némítja\" a botot, ezzel " "némáva teszi azt és nem fog válaszolni a csatornában végrehajtott kérésekre. " " csak akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:538 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will unlobotomize " "the\n" " bot, making it respond to requests made in the channel again.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez megszünteti a bot némítását, " "így az újra válaszol a csatornában vérgehajtott kérésekre. csak " "akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:553 msgid "" "takes no arguments\n" "\n" " Returns the channels in which this bot is lobotomized.\n" " " msgstr "" "paraméter nélküli\n" "\n" "Kiírja a csatornákat, ahol a bot némítva van." #: plugin.py:568 msgid "I'm currently lobotomized in %L." msgstr "Jelenleg némítva vagyok %L-ban." #: plugin.py:571 msgid "I'm not currently lobotomized in any channels that you're in." msgstr "Jelenleg nem vagyok némítva egy csatornában sem, ahol vagy." #: plugin.py:585 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will effect a\n" " persistent ban from interacting with the bot on the given\n" " (or the current hostmask associated with ). " "Other\n" " plugins may enforce this ban by actually banning users with\n" " matching hostmasks when they join. is an optional\n" " argument specifying when (in \"seconds from now\") the ban " "should\n" " expire; if none is given, the ban will never automatically " "expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez megtiltja -nak (vagy " " jelenlegi hosztjának), hogy a botot használja a csatornában. Más " "bővítmények kényszeríthetik a tiltást azzal, hogy kitiltják a " "felhasználókat, akikre illeszkedik a hoszt amikor belépnek. egy nem " "kötelező paaméter, meghatározza, hogy mikor (hány \"másodperc múlva\") " "járjon le a tiltás; ha nincs megadva, a tiltás soha nem fog automatikusan " "lejárni. csak akkor szükséges, ha az üzenet nem a csaotnában van " "elküldve." #: plugin.py:605 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ban on . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja a tiltást -" "ról. csak akkor szükséges, ha az üzenet nem a csatornában van " "elküldve." #: plugin.py:617 msgid "There are no persistent bans for that hostmask." msgstr "Nincsenek tiltások erre a hosztra." #: plugin.py:622 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will show you the\n" " current persistent bans on the that match the given\n" " mask, if any (returns all of them otherwise).\n" " Note that you can use * as a wildcard on masks and \\* to match\n" " actual * in masks\n" " " msgstr "" "[] \n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja a tiltást -" "ról. csak akkor szükséges, ha az üzenet nem a csatornában van " "elküldve." #: plugin.py:640 msgid "%q (expires %t)" msgstr "%q (lejár %t)" #: plugin.py:643 msgid "%q (never expires)" msgstr "%q (soha nem jár le)" #: plugin.py:647 msgid "There are no persistent bans on %s." msgstr "Nincsenek tiltások %s-on." #: plugin.py:654 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will set a " "persistent\n" " ignore on or the hostmask currently\n" " associated with . is an optional argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez mellőzi -ot vagy " "jelenlegi hosztját. egy nem kötelező paraméter, meghatározza, hogy " "mikor (hány \"másodperc múlva\" járjon le a mellőzés; ha nincs megadva, a " "mellőzés soha nem fog automatikusan lejárni. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:672 #, fuzzy msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ignore on in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja a mellőzést " "-ról a csatornában. csak akkor szükséges, ha az üzenet nem " "a csatornában van elküldve." #: plugin.py:684 msgid "There are no ignores for that hostmask." msgstr "Nincsenek mellőzések erre a hosztra." #: plugin.py:689 msgid "" "[]\n" "\n" " Lists the hostmasks that the bot is ignoring on the given " "channel.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[\n" "\n" "Kiírja a hosztokat, amelyeket a bot mellőz a megadott csatornában. " " csak akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:698 msgid "I'm not currently ignoring any hostmasks in %q" msgstr "Nem mellőzők egy hosztot sem %q-ban." #: plugin.py:709 #, fuzzy msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give the\n" " (or the user to whom maps)\n" " the capability in the channel. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez " "felhasználónak (vagy felhasználójának) a képességet a " "csatornában. csak akkor szükséges, ha az üzenet nem a csatornában " "van elküldve." #: plugin.py:725 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will take from the\n" " user currently identified as (or the user to whom " "\n" " maps) the capability in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja a jelenleg -" "ként bejelentkezett felhasználóról (vagy a felhasználóról, akire " "illeszkedik) a képességet a csatornában. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:744 msgid "That user didn't have the %L %s." msgstr "A felhasználónak nem volt a(z) %L %s." #: plugin.py:753 msgid "" "[] {True|False}\n" "\n" " If you have the #channel,op capability, this will set the " "default\n" " response to non-power-related (that is, not {op, halfop, " "voice})\n" " capabilities to be the value you give. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {True|False}\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez az alapértelmezett választ a " "nem erővel kapcsolatos (vagyis nem {op, halfop, voice}) képességeket a " "megadott értékre állítja. csak akkor szükséges, ha az üzenet nem " "a csatornában van elküldve." #: plugin.py:771 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will add the " "channel\n" " capability for all users in the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez hozzáadja -et a " "csatorna képességeihez. csak akkor szükséges, ha az üzenet nem, a " "csatornában van elküldve." #: plugin.py:786 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will unset the " "channel\n" " capability so each user's specific capability or " "the\n" " channel default capability will take precedence. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez eltávolítja a csatorna " " képességét, így a felhasználók saját képességei vagy a csatorna " "alapértelmezett képességei lesznek előnyben. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:802 msgid "capability" msgstr "képesség" #: plugin.py:805 msgid "I do not know about the %L %s." msgstr "Nem tudok a %L %s-ról." #: plugin.py:812 msgid "" "[]\n" "\n" " Returns the capabilities present on the . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Kiírja képességeit. csak akkor szükséges, ha az üzenet " "nem a csatornában van elküldve." #: plugin.py:824 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will disable the " "\n" " in . If is provided, will be disabled " "only\n" " for that plugin. If only is provided, all commands in the\n" " given plugin will be disabled. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [] [\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez letiltja -ot " "-ban. Ha meg van adva, csak a megadott " "bővítményben lesz letiltva. Ha csak van megadva, minden parancs " "le lesz tiltva a megadott bővítményben. csak akkor szükséges, ha " "az üzenet nem a csatornában van elküldve." #: plugin.py:840 plugin.py:879 msgid "The %s plugin does not have a command called %s." msgstr "A %s bővítménynek nincs %s nevű parancsa." #: plugin.py:847 plugin.py:886 msgid "No plugin or command named %s could be found." msgstr "Nem található bővítmény vagy parancs %s néven." #: plugin.py:863 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will enable the " "\n" " in if it has been disabled. If is provided,\n" " will be enabled only for that plugin. If only " "is\n" " provided, all commands in the given plugin will be enabled. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" "Ha rendelkezel a #csatorna,op képességgel, ez engedélyezi -ot " "-ban ha le lett tiltva. Ha meg van adva, " "csak a megadott bővítményben lesz engedélyezve." #: plugin.py:900 msgid "%s was not disabled." msgstr "%s nem volt letiltva." #: plugin.py:909 msgid "" "[] [--count]\n" "\n" " Returns the nicks in . is only necessary if the\n" " message isn't sent in the channel itself. Returns only the number " "of\n" " nicks if --count option is provided.\n" " " msgstr "" "[] [--count]\n" "\n" "Kiírja a neveket -ban. csak akkor szükséges, ha az " "üzenet nem a csatornában van elküldve. Ha a --count opció meg van adva, csak " "a nevek számát írja ki." #: plugin.py:931 msgid "You don't have access to that information." msgstr "Nincs hozzáférésed ehhez az információhoz." #: plugin.py:946 msgid "" "Internal message for notifying all the #channel,ops in a channel of\n" " a given situation." msgstr "Belső üzenet #csatorna,op-ok értesítésére egy adott szituációban." #: plugin.py:949 msgid "Alert to all %s ops: %s" msgstr "Riasztás minden %s operátornak: %s" #: plugin.py:951 msgid " (from %s)" msgstr "(%s-tól)" #: plugin.py:963 msgid "" "[] \n" "\n" " Sends to all the users in who have the ," "op\n" " capability.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ha rendelkezel a #csatorna,halfop képességgel, ez fél-operátor státuszt ad " "minden -nek, amit megadsz. Ha nem adsz meg -et, ez neked ad fél-" "operátor státuszt. csak akkor szükséges, ha az üzenet nem a " "csatornában van elküldve." #: plugin.py:973 msgid "" "[] []\n" "\n" " Tells the bot to part the list of channels you give it. " "is\n" " only necessary if you want the bot to part a channel other than the\n" " current channel. If is specified, use it as the part\n" " message. Otherwise, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if no default is configured.\n" " " msgstr "" #: plugin.py:994 #, fuzzy msgid "I'm not in %s." msgstr "Nem láttam %s-t." #~ msgid "" #~ "I cowardly refuse to devoice myself. If you really want me devoiced, " #~ "tell me to op you and then devoice me yourself." #~ msgstr "" #~ "Gyáván megtagadom, hogy eltávolítsam magamról a hangot. Ha el akarod " #~ "távolítani rólam a hangot, mondd, hogy adjak operátor státuszt neked, és " #~ "távolítsd el rólam a hangot." #, fuzzy #~ msgid "" #~ "[]\n" #~ "\n" #~ " If you have the #channel,op capability, this will show you " #~ "the\n" #~ " current persistent bans on the .\n" #~ " " #~ msgstr "" #~ "[]\n" #~ "\n" #~ "Ha rendelkezel a #csatorna,op képességgel, ez megmutatja neked a " #~ "jelenlegi tiltásokat -n." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/locales/it.po0000644000175000017500000010106514535072470017552 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-06-09 09:33+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:49 msgid "" "Determines whether the bot will always try to\n" " rejoin a channel whenever it's kicked from the channel." msgstr "" "Determina se il bot proverà a rientrare in un canale ogni volta che viene " "espulso." #: config.py:52 msgid "" "Determines whether the output of 'nicks' will\n" " be sent in private. This prevents mass-highlights of a channel's users,\n" " accidental or on purpose." msgstr "" #: config.py:56 msgid "" "Determines how many seconds the bot will wait\n" " before rejoining a channel if kicked and\n" " supybot.plugins.Channel.alwaysRejoin is on." msgstr "" #: config.py:60 msgid "" "Determines what part message should be\n" " used by default. If the part command is called without a part " "message,\n" " this will be used. If this value is empty, then no part message " "will\n" " be used (they are optional in the IRC protocol). The standard\n" " substitutions ($version, $nick, etc.) are all handled appropriately." msgstr "" #: plugin.py:48 msgid "" "This plugin provides various commands for channel management, such\n" " as setting modes and channel-wide bans/ignores/capabilities. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:94 msgid "" "[] [ ...]\n" "\n" " Sets the mode in to , sending the arguments given.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Imposta in inviando l'argomento fornito. \n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso. " #: plugin.py:101 msgid "change the mode" msgstr "modificare il mode" #: plugin.py:105 msgid "" "[] []\n" "\n" " Sets the channel limit to . If is 0, or isn't " "given,\n" " removes the channel limit. is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Imposta di canale. Se è uguale 0 o non viene\n" " fornito, questo viene rimosso. è necessario solo se il\n" " messaggio non viene inviato nel canale stesso. " #: plugin.py:115 msgid "change the limit" msgstr "modificare il limite" #: plugin.py:120 msgid "" "[]\n" "\n" " Sets +m on , making it so only ops and voiced users can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Imposta +m su facendo sì che solo gli operatori e chi ha " "il\n" " voice possa inviare messaggi. è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso.\n" " " #: plugin.py:127 msgid "moderate the channel" msgstr "moderare il canale" #: plugin.py:131 msgid "" "[]\n" "\n" " Sets -m on , making it so everyone can\n" " send messages to the channel. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Imposta -m su facendo sì che tutti possano inviare " "messaggi.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:139 msgid "unmoderate the channel" msgstr "de-moderare il canale" #: plugin.py:143 msgid "" "[] []\n" "\n" " Sets the keyword in to . If is not given, " "removes\n" " the keyword requirement to join . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Imposta in . Se non viene specificata, " "rimuove\n" " quella richiesta per entrare. è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso.\n" " " #: plugin.py:155 msgid "change the keyword" msgstr "cambiare la password" #: plugin.py:160 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give all the " "s\n" " you provide ops. If you don't provide any s, this will op " "you.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di dare lo stato di " "operatore\n" " a tutti i specificati; se nessun è fornito lo darà a " "te.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:172 msgid "op someone" msgstr "dare l'op a qualcuno" #: plugin.py:176 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,halfop capability, this will give all the\n" " s you provide halfops. If you don't provide any s, " "this\n" " will give you halfops. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,halfop permette di dare lo stato di " "halfop\n" " a tutti i specificati; se nessun è fornito lo darà a " "te.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:188 msgid "halfop someone" msgstr "dare l'halfop a qualcuno" #: plugin.py:209 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,voice capability, this will voice all the\n" " s you provide. If you don't provide any s, this will\n" " voice you. is only necessary if the message isn't sent in " "the\n" " channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,voice permette di dare il voice a tutti " "i\n" " specificati; se nessun è fornito lo darà a te. " "\n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:217 msgid "voice someone" msgstr "dare il voice a qualcuno" #: plugin.py:222 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di rimuove i privilegi di " "operatore a tutti\n" " i nick specificati; se nessun nick è fornito li rimuove a chi usa il " "comando.\n" " " #: plugin.py:229 msgid "" "I cowardly refuse to deop myself. If you really want me deopped, tell me to " "op you and then deop me yourself." msgstr "" "Codardamente mi rifiuto di rimuovermi lo stato di operatore; se davvero lo " "vuoi, dimmi di darlo a te e poi toglimelo." #: plugin.py:237 msgid "deop someone" msgstr "rimuovere l'op a qualcuno" #: plugin.py:242 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove half-" "operator\n" " privileges from all the nicks given. If no nicks are given, " "removes\n" " half-operator privileges from the person sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di rimuove i privilegi di " "halfop a tutti\n" " i nick specificati; se nessun nick è fornito li rimuove a chi usa il " "comando.\n" " " #: plugin.py:249 msgid "" "I cowardly refuse to dehalfop myself. If you really want me dehalfopped, " "tell me to op you and then dehalfop me yourself." msgstr "" "Codardamente mi rifiuto di rimuovermi lo stato di halfop; se davvero lo " "vuoi, dimmi di dare l'op a te e poi toglimelo." #: plugin.py:257 msgid "dehalfop someone" msgstr "rimuovere l'halfop a qualcuno" #: plugin.py:262 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will remove voice from " "all\n" " the nicks given. If no nicks are given, removes voice from the " "person\n" " sending the message.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di rimuovere il voice a " "tutti\n" " i nick specificati; se nessun nick è fornito lo rimuove a chi usa il " "comando.\n" " " #: plugin.py:274 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will cause the bot to\n" " \"cycle\", or PART and then JOIN the channel. is only " "necessary\n" " if the message isn't sent in the channel itself. If is not\n" " specified, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if neither a cycle reason nor a default part message is given.\n" " " msgstr "" "[]\n" "\n" " Se hai la capacità #canale,op invierà il comando \"cycle\" al bot " "(PART e JOIN).\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:292 msgid "" "[] [, , ...] []\n" "\n" " Kicks (s) from for . If isn't " "given,\n" " uses the nick of the person making the command as the reason.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [, , ...] []\n" "\n" " Caccia (kick) da con . Se non è " "fornito,\n" " utilizza il nick di chi ha usato il comando. è necessario " "solo\n" " se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:300 msgid "I cowardly refuse to kick myself." msgstr "Codardamente mi rifiuto di cacciare me stesso." #: plugin.py:305 msgid "" "The reason you gave is longer than the allowed length for a KICK reason on " "this server." msgstr "" "Il motivo che hai dato è più lungo della lunghezza consentita da questo " "server." #: plugin.py:310 msgid "kick someone" msgstr "cacciare (kick) qualcuno" #: plugin.py:316 msgid "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " If you have the #channel,op capability, this will kickban " "for\n" " as many seconds as you specify, or else (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact bans only the exact hostmask; --nick bans just the nick;\n" " --user bans just the user, and --host bans just the host. You can\n" " combine these options as you choose. is a reason to give " "for\n" " the kick.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " Se hai la capacità #canale,op permette di espellere (kickban) " "per\n" " il numero di secondi specificati, se il valore è uguale a 0 o non " "viene\n" " fornito un numero, il ban sarà a tempo indeterminato. --exact si " "applica\n" " alla hostmask precisa, --nick solo al nick, --user al nome utente e\n" " --host al nome dell'host. È possibile utilizzare più di un'opzione;\n" " è la ragione da indicare per il kick. è necessario " "solo\n" " se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:333 msgid "kick or ban someone" msgstr "cacciare (kick) o bannare qualcuno" #: plugin.py:341 #, fuzzy msgid "" "[] [--{exact,nick,user,host}] []\n" "\n" " If you have the #channel,op capability, this will ban for\n" " as many seconds as you specify, otherwise (if you specify 0 seconds " "or\n" " don't specify a number of seconds) it will ban the person " "indefinitely.\n" " --exact can be used to specify an exact hostmask. You can combine " "the\n" " exact, nick, user, and host options as you choose. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [--{exact,nick,user,host}] [] []\n" "\n" " Se hai la capacità #canale,op permette di espellere (kickban) " "per\n" " il numero di secondi specificati, se il valore è uguale a 0 o non " "viene\n" " fornito un numero, il ban sarà a tempo indeterminato. --exact si " "applica\n" " alla hostmask precisa, --nick solo al nick, --user al nome utente e\n" " --host al nome dell'host. È possibile utilizzare più di un'opzione;\n" " è la ragione da indicare per il kick. è necessario " "solo\n" " se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:355 plugin.py:581 #, fuzzy msgid "ban someone" msgstr "rimuovere il ban a qualcuno" #: plugin.py:375 msgid "I haven't seen %s." msgstr "Non ho mai visto %s." #: plugin.py:385 msgid "I cowardly refuse to kickban myself." msgstr "Codardamente mi rifiuto di espellere (kickban) me stesso." #: plugin.py:394 msgid "I cowardly refuse to ban myself." msgstr "Codardamente mi rifiuto di bannare stesso." #: plugin.py:422 msgid "%s has %s too, you can't ban them." msgstr "anche %s ha %s, non puoi bannarlo/a." #: plugin.py:433 #, fuzzy msgid "" "[] []\n" "\n" " Unbans on . If is not given, unbans\n" " any hostmask currently banned on that matches your " "current\n" " hostmask. Especially useful for unbanning yourself when you get\n" " unexpectedly (or accidentally) banned from the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Rimuove il ban di su . Se non è " "specificata,\n" " rimuove qualsiasi hostmask attualmente bannata che corrisponde alla " "propria.\n" " Particolarmente utile per toglierlo a sé stessi quando si viene " "bannati\n" " inaspettatamente o accidentalmente. è necessario solo se " "il\n" " messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:453 msgid "All bans on %s matching %s have been removed." msgstr "Tutti i ban su %s che corrisopndono a %s sono stati rimossi." #: plugin.py:457 msgid "No bans matching %s were found on %s." msgstr "Non è stato trovato alcun ban corrispondente a %s su %s." #: plugin.py:460 msgid "unban someone" msgstr "rimuovere il ban a qualcuno" #: plugin.py:467 msgid "" "[]\n" "\n" " List all bans on the channel.\n" " If is not given, it defaults to the current channel." msgstr "" #: plugin.py:471 msgid "No bans." msgstr "" #: plugin.py:476 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will invite \n" " to join . is only necessary if the message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Se hai la capacità #canale,op permette di invitare in " ".\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:485 msgid "invite someone" msgstr "invitare qualcuno" #: plugin.py:504 msgid "%s is already in %s." msgstr "%s è già in %s." #: plugin.py:511 msgid "There is no %s on this network." msgstr "Non c'è nessun %s su questa rete." #: plugin.py:523 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will \"lobotomize\" " "the\n" " bot, making it silent and unanswering to all requests made in " "the\n" " channel. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[]\n" "\n" " Se hai la capacità #canale,op permette di \"lobotomizzare\" il " "bot\n" " rendendolo silenzioso (non risponderà alle richieste fatte in " "canale).\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:538 msgid "" "[]\n" "\n" " If you have the #channel,op capability, this will unlobotomize " "the\n" " bot, making it respond to requests made in the channel again.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Se hai la capacità #canale,op permette di \"de-lobotomizzare\" " "il bot\n" " rendendolo attivo (risponderà nuovamente alle richieste fatte in " "canale).\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:553 msgid "" "takes no arguments\n" "\n" " Returns the channels in which this bot is lobotomized.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta l'elenco dei canali nei quali il bot è lobotomizzato.\n" " " #: plugin.py:568 msgid "I'm currently lobotomized in %L." msgstr "Sono attualmente lobotomizzato in %L." #: plugin.py:571 msgid "I'm not currently lobotomized in any channels that you're in." msgstr "Al momento non sono lobotomizzato in nessun canale in cui sei." #: plugin.py:585 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will effect a\n" " persistent ban from interacting with the bot on the given\n" " (or the current hostmask associated with ). " "Other\n" " plugins may enforce this ban by actually banning users with\n" " matching hostmasks when they join. is an optional\n" " argument specifying when (in \"seconds from now\") the ban " "should\n" " expire; if none is given, the ban will never automatically " "expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" " Se hai la capacità #canale,op applica un ban permanente " "all'\n" " specificata (o quella associata a ). Altri plugin possono " "rafforzare\n" " questo ban effettuando un ban su utenti con una hostmask " "corrispondente.\n" " è un argomento opzionale per specificare quando (in " "\"secondi\n" " a partire da subito\") scadrà il ban; se non fornito, questo non " "scadrà mai.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:605 msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ban on . is only necessary if " "the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Se hai la capacità #canale,op permette di rimuovere il ban " "permanente su .\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:617 msgid "There are no persistent bans for that hostmask." msgstr "Non ci sono ban permanenti per questa hostmask." #: plugin.py:622 #, fuzzy msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will show you the\n" " current persistent bans on the that match the given\n" " mask, if any (returns all of them otherwise).\n" " Note that you can use * as a wildcard on masks and \\* to match\n" " actual * in masks\n" " " msgstr "" "[] \n" "\n" " Se hai la capacità #canale,op permette di rimuovere il ban " "permanente su .\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:640 msgid "%q (expires %t)" msgstr "%q (scade il %t)" #: plugin.py:643 msgid "%q (never expires)" msgstr "%q (non scade)" #: plugin.py:647 msgid "There are no persistent bans on %s." msgstr "Non ci sono ban permanenti su %s." #: plugin.py:654 msgid "" "[] []\n" "\n" " If you have the #channel,op capability, this will set a " "persistent\n" " ignore on or the hostmask currently\n" " associated with . is an optional argument\n" " specifying when (in \"seconds from now\") the ignore will " "expire; if\n" " it isn't given, the ignore will never automatically expire.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] []\n" "\n" " Se hai la capacità #canale,op permette di impostare un ignore " "permanente\n" " su (o quella associata a ). è un " "argomento\n" " opzionale per specificare quando (in \"secondi a partire da " "subito\") scadrà\n" " l'ignore; se non fornito, questo non scadrà mai. è " "necessario solo\n" " se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:672 #, fuzzy msgid "" "[] \n" "\n" " If you have the #channel,op capability, this will remove the\n" " persistent ignore on in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Se hai la capacità #canale,op permette di rimuovere l'ignore " "permanente su .\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:684 msgid "There are no ignores for that hostmask." msgstr "Non ci sono ignore per questa hostmask." #: plugin.py:689 msgid "" "[]\n" "\n" " Lists the hostmasks that the bot is ignoring on the given " "channel.\n" " is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[]\n" "\n" " Elenca le hostmask ignorate dal bot sul dato canale. è\n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:698 msgid "I'm not currently ignoring any hostmasks in %q" msgstr "Al momento non sto ignorando nessuna hostmasks in %q" #: plugin.py:709 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will give the\n" " (or the user to whom maps)\n" " the capability in the channel. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di dare a \n" " (o quello a cui corrisponde ) sul canale. " "\n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:725 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will take from the\n" " user currently identified as (or the user to whom " "\n" " maps) the capability in the channel. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di rimuovere " "all'utente\n" " attualmente identificato da (o quello a cui corrisponde " ")\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:744 msgid "That user didn't have the %L %s." msgstr "Questo utente non ha la %L %s." #: plugin.py:753 msgid "" "[] {True|False}\n" "\n" " If you have the #channel,op capability, this will set the " "default\n" " response to non-power-related (that is, not {op, halfop, " "voice})\n" " capabilities to be the value you give. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {True|False}\n" "\n" " Se hai la capacità #canale,op permette di impostare la risposta " "predefinita\n" " relativa alle capacità senza potere (cioè non op, halfop, " "voice). \n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:771 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will add the " "channel\n" " capability for all users in the channel. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di aggiungere la " "\n" " del canale per tutti gli utenti nel canale. è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:786 msgid "" "[] [ ...]\n" "\n" " If you have the #channel,op capability, this will unset the " "channel\n" " capability so each user's specific capability or " "the\n" " channel default capability will take precedence. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Se hai la capacità #canale,op permette di rimuovere la " "\n" " del canale in modo che avranno la precedenza tutte quelle " "specifiche\n" " di ogni utente o quella predefinita del canale. è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:802 msgid "capability" msgstr "capacità" #: plugin.py:805 msgid "I do not know about the %L %s." msgstr "Non so nulla a proposito di %L %s." #: plugin.py:812 msgid "" "[]\n" "\n" " Returns the capabilities present on the . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta le capacità presenti su . è necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:824 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will disable the " "\n" " in . If is provided, will be disabled " "only\n" " for that plugin. If only is provided, all commands in the\n" " given plugin will be disabled. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" " Se hai la capacità #canale,op permette di disabilitare in " ".\n" " Se è specificato, sarà disabilitato solo per quel " "plugin.\n" " Se viene fornito solo , disabilita tutti i comandi del dato " "plugin.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:840 plugin.py:879 msgid "The %s plugin does not have a command called %s." msgstr "Il plugin %s non ha un comando chiamato %s." #: plugin.py:847 plugin.py:886 msgid "No plugin or command named %s could be found." msgstr "Non è stato trovato nessun plugin o comando chiamato %s." #: plugin.py:863 msgid "" "[] [] []\n" "\n" " If you have the #channel,op capability, this will enable the " "\n" " in if it has been disabled. If is provided,\n" " will be enabled only for that plugin. If only " "is\n" " provided, all commands in the given plugin will be enabled. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" " Se hai la capacità #canale,op permette di abilitare (se " "questo\n" " è stato disabilitato) in . Se è specificato, " " sarà\n" " abilitato solo per quel plugin. Se viene fornito solo , " "abilita tutti\n" " i comandi del dato plugin. è necessario solo se il " "messaggio non viene\n" " inviato nel canale stesso.\n" " " #: plugin.py:900 msgid "%s was not disabled." msgstr "%s non è stato disabilitato." #: plugin.py:909 msgid "" "[] [--count]\n" "\n" " Returns the nicks in . is only necessary if the\n" " message isn't sent in the channel itself. Returns only the number " "of\n" " nicks if --count option is provided.\n" " " msgstr "" "[] [--count]\n" "\n" " Riporta l'elenco dei nick in . è necessario solo " "se\n" " il messaggio non viene inviato nel canale stesso. Se viene fornita\n" " l'opzione --count, mostra solo il numero dei nick.\n" " " #: plugin.py:931 msgid "You don't have access to that information." msgstr "Non hai accesso a questa informazione." #: plugin.py:946 msgid "" "Internal message for notifying all the #channel,ops in a channel of\n" " a given situation." msgstr "" "Messaggio interno per notificare una data situazione a tutti gli operatori " "(#canale,op) in un canale." #: plugin.py:949 msgid "Alert to all %s ops: %s" msgstr "Avviso a tutti gli op di %s: %s" #: plugin.py:951 msgid " (from %s)" msgstr " (da %s)" #: plugin.py:963 msgid "" "[] \n" "\n" " Sends to all the users in who have the ," "op\n" " capability.\n" " " msgstr "" "[] \n" "\n" " Invia a tutti gli utenti in che hanno la capacità " ",op.\n" " " #: plugin.py:973 msgid "" "[] []\n" "\n" " Tells the bot to part the list of channels you give it. " "is\n" " only necessary if you want the bot to part a channel other than the\n" " current channel. If is specified, use it as the part\n" " message. Otherwise, the default part message specified in\n" " supybot.plugins.Channel.partMsg will be used. No part message will " "be\n" " used if no default is configured.\n" " " msgstr "" #: plugin.py:994 #, fuzzy msgid "I'm not in %s." msgstr "Non ho mai visto %s." #~ msgid "" #~ "I cowardly refuse to devoice myself. If you really want me devoiced, " #~ "tell me to op you and then devoice me yourself." #~ msgstr "" #~ "Codardamente mi rifiuto di rimuovermi il voice; se davvero lo vuoi, dimmi " #~ "di dare l'op a te e poi toglimelo." #~ msgid "" #~ "[]\n" #~ "\n" #~ " If you have the #channel,op capability, this will show you " #~ "the\n" #~ " current persistent bans on the .\n" #~ " " #~ msgstr "" #~ "[]\n" #~ "\n" #~ " Se hai la capacità #canale,op mostra gli attuali ban " #~ "permanenti su .\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/plugin.py0000644000175000017500000012657414535072470017040 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009-2012, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import fnmatch import time import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.schedule as schedule import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Channel') class Channel(callbacks.Plugin): """This plugin provides various commands for channel management, such as setting modes and channel-wide bans/ignores/capabilities. This is a core Supybot plugin that should not be removed!""" def __init__(self, irc): self.__parent = super(Channel, self) self.__parent.__init__(irc) self.invites = {} def doKick(self, irc, msg): channel = msg.channel network = irc.network if msg.args[1] == irc.nick: if self.registryValue('alwaysRejoin', channel, network): delay = self.registryValue('rejoinDelay', channel, network) networkGroup = conf.supybot.networks.get(irc.network) if delay: def f(): irc.sendMsg(networkGroup.channels.join(channel)) schedule.addEvent(f, time.time() + delay) self.log.info('Kicked from %s @ %s by %s. ' 'Rejoining after %s seconds.', channel, network, msg.prefix, delay) else: self.log.info('Kicked from %s @ %s by %s. Rejoining.', channel, network, msg.prefix) irc.sendMsg(networkGroup.channels.join(channel)) else: self.log.info('Kicked from %s @ %s by %s. Not auto-rejoining.', channel, network, msg.prefix) def _sendMsg(self, irc, msg): irc.queueMsg(msg) irc.noReply() def _sendMsgs(self, irc, nicks, f): numModes = irc.state.supported.get('modes', 1) if numModes is None: # No limit enforced by the server, we're setting one ourselves. numModes = 5 for i in range(0, len(nicks), numModes): irc.queueMsg(f(nicks[i:i + numModes])) irc.noReply() @internationalizeDocstring def mode(self, irc, msg, args, channel, modes): """[] [ ...] Sets the mode in to , sending the arguments given. is only necessary if the message isn't sent in the channel itself. """ self._sendMsg(irc, ircmsgs.mode(channel, modes)) mode = wrap(mode, ['op', ('haveHalfop+', _('change the mode')), many('something')]) @internationalizeDocstring def limit(self, irc, msg, args, channel, limit): """[] [] Sets the channel limit to . If is 0, or isn't given, removes the channel limit. is only necessary if the message isn't sent in the channel itself. """ if limit: self._sendMsg(irc, ircmsgs.mode(channel, ['+l', limit])) else: self._sendMsg(irc, ircmsgs.mode(channel, ['-l'])) limit = wrap(limit, ['op', ('haveOp', _('change the limit')), additional('nonNegativeInt', 0)]) @internationalizeDocstring def moderate(self, irc, msg, args, channel): """[] Sets +m on , making it so only ops and voiced users can send messages to the channel. is only necessary if the message isn't sent in the channel itself. """ self._sendMsg(irc, ircmsgs.mode(channel, ['+m'])) moderate = wrap(moderate, ['op', ('haveHalfop+', _('moderate the channel'))]) @internationalizeDocstring def unmoderate(self, irc, msg, args, channel): """[] Sets -m on , making it so everyone can send messages to the channel. is only necessary if the message isn't sent in the channel itself. """ self._sendMsg(irc, ircmsgs.mode(channel, ['-m'])) unmoderate = wrap(unmoderate, ['op', ('haveHalfop+', _('unmoderate the channel'))]) @internationalizeDocstring def key(self, irc, msg, args, channel, key): """[] [] Sets the keyword in to . If is not given, removes the keyword requirement to join . is only necessary if the message isn't sent in the channel itself. """ networkGroup = conf.supybot.networks.get(irc.network) networkGroup.channels.key.get(channel).setValue(key) if key: self._sendMsg(irc, ircmsgs.mode(channel, ['+k', key])) else: self._sendMsg(irc, ircmsgs.mode(channel, ['-k'])) key = wrap(key, ['op', ('haveHalfop+', _('change the keyword')), additional('somethingWithoutSpaces', '')]) @internationalizeDocstring def op(self, irc, msg, args, channel, nicks): """[] [ ...] If you have the #channel,op capability, this will give all the s you provide ops. If you don't provide any s, this will op you. is only necessary if the message isn't sent in the channel itself. """ if not nicks: nicks = [msg.nick] def f(L): return ircmsgs.ops(channel, L) self._sendMsgs(irc, nicks, f) op = wrap(op, ['op', ('haveOp', _('op someone')), any('nickInChannel')]) @internationalizeDocstring def halfop(self, irc, msg, args, channel, nicks): """[] [ ...] If you have the #channel,halfop capability, this will give all the s you provide halfops. If you don't provide any s, this will give you halfops. is only necessary if the message isn't sent in the channel itself. """ if not nicks: nicks = [msg.nick] def f(L): return ircmsgs.halfops(channel, L) self._sendMsgs(irc, nicks, f) halfop = wrap(halfop, ['halfop', ('haveOp', _('halfop someone')), any('nickInChannel')]) def _voice(self, irc, msg, args, channel, nicks, fn): if nicks: if len(nicks) == 1 and msg.nick in nicks: capability = 'voice' else: capability = 'op' else: nicks = [msg.nick] capability = 'voice' capability = ircdb.makeChannelCapability(channel, capability) if ircdb.checkCapability(msg.prefix, capability): def f(L): return fn(channel, L) self._sendMsgs(irc, nicks, f) else: irc.errorNoCapability(capability) def voice(self, irc, msg, args, channel, nicks): """[] [ ...] If you have the #channel,voice capability, this will voice all the s you provide. If you don't provide any s, this will voice you. is only necessary if the message isn't sent in the channel itself. """ self._voice(irc, msg, args, channel, nicks, ircmsgs.voices) voice = wrap(voice, ['channel', ('haveHalfop+', _('voice someone')), any('nickInChannel')]) @internationalizeDocstring def deop(self, irc, msg, args, channel, nicks): """[] [ ...] If you have the #channel,op capability, this will remove operator privileges from all the nicks given. If no nicks are given, removes operator privileges from the person sending the message. """ if irc.nick in nicks: irc.error(_('I cowardly refuse to deop myself. If you really ' 'want me deopped, tell me to op you and then deop me ' 'yourself.'), Raise=True) if not nicks: nicks = [msg.nick] def f(L): return ircmsgs.deops(channel, L) self._sendMsgs(irc, nicks, f) deop = wrap(deop, ['op', ('haveOp', _('deop someone')), any('nickInChannel')]) @internationalizeDocstring def dehalfop(self, irc, msg, args, channel, nicks): """[] [ ...] If you have the #channel,op capability, this will remove half-operator privileges from all the nicks given. If no nicks are given, removes half-operator privileges from the person sending the message. """ if irc.nick in nicks: irc.error(_('I cowardly refuse to dehalfop myself. If you really ' 'want me dehalfopped, tell me to op you and then ' 'dehalfop me yourself.'), Raise=True) if not nicks: nicks = [msg.nick] def f(L): return ircmsgs.dehalfops(channel, L) self._sendMsgs(irc, nicks, f) dehalfop = wrap(dehalfop, ['halfop', ('haveOp', _('dehalfop someone')), any('nickInChannel')]) @internationalizeDocstring def devoice(self, irc, msg, args, channel, nicks): """[] [ ...] If you have the #channel,op capability, this will remove voice from all the nicks given. If no nicks are given, removes voice from the person sending the message. """ self._voice(irc, msg, args, channel, nicks, ircmsgs.devoices) devoice = wrap(devoice, ['channel', ('haveOp', 'devoice someone'), any('nickInChannel')]) @internationalizeDocstring def cycle(self, irc, msg, args, channel, reason): """[] [] If you have the #channel,op capability, this will cause the bot to "cycle", or PART and then JOIN the channel. is only necessary if the message isn't sent in the channel itself. If is not specified, the default part message specified in supybot.plugins.Channel.partMsg will be used. No part message will be used if neither a cycle reason nor a default part message is given. """ reason = (reason or self.registryValue("partMsg", channel, irc.network)) reason = ircutils.standardSubstitute(irc, msg, reason) self._sendMsg(irc, ircmsgs.part(channel, reason)) networkGroup = conf.supybot.networks.get(irc.network) self._sendMsg(irc, networkGroup.channels.join(channel)) cycle = wrap(cycle, ['op', additional('text')]) @internationalizeDocstring def kick(self, irc, msg, args, channel, nicks, reason): """[] [, , ...] [] Kicks (s) from for . If isn't given, uses the nick of the person making the command as the reason. is only necessary if the message isn't sent in the channel itself. """ if utils.iter.any(lambda n: ircutils.strEqual(n, irc.nick), nicks): irc.error(_('I cowardly refuse to kick myself.'), Raise=True) if not reason: reason = msg.nick kicklen = irc.state.supported.get('kicklen', sys.maxsize) if len(reason) > kicklen: irc.error(_('The reason you gave is longer than the allowed ' 'length for a KICK reason on this server.'), Raise=True) for nick in nicks: self._sendMsg(irc, ircmsgs.kick(channel, nick, reason)) kick = wrap(kick, ['op', ('haveHalfop+', _('kick someone')), commalist('nickInChannel'), additional('text')]) @internationalizeDocstring def kban(self, irc, msg, args, channel, optlist, bannedNick, expiry, reason): """[] [--{exact,nick,user,host,account}] [] [] If you have the #channel,op capability, this will kickban for as many seconds as you specify, or else (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact bans only the exact hostmask; --nick bans just the nick; --user bans just the user, and --host bans just the host You can combine the --nick, --user, and --host options as you choose. is only necessary if the message isn't sent in the channel itself. """ self._ban(irc, msg, args, channel, optlist, bannedNick, expiry, reason, True) kban = wrap(kban, ['op', getopts({'exact':'', 'nick':'', 'user':'', 'host':''}), ('haveHalfop+', _('kick or ban someone')), 'nickInChannel', optional('expiry', 0), additional('text')]) @internationalizeDocstring def iban(self, irc, msg, args, channel, optlist, bannedNick, expiry): """[] [--{exact,nick,user,host}] [] If you have the #channel,op capability, this will ban for as many seconds as you specify, otherwise (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact can be used to specify an exact hostmask. You can combine the --nick, --user, and --host options as you choose. is only necessary if the message isn't sent in the channel itself. """ self._ban(irc, msg, args, channel, optlist, bannedNick, expiry, None, False) iban = wrap(iban, ['op', getopts({'exact':'', 'nick':'', 'user':'', 'host':''}), ('haveHalfop+', _('ban someone')), first('nick', 'hostmask'), optional('expiry', 0)]) def _ban(self, irc, msg, args, channel, optlist, target, expiry, reason, kick): # Check that they're not trying to make us kickban ourself. if irc.isNick(target): bannedNick = target try: bannedHostmask = irc.state.nickToHostmask(target) banmaskstyle = conf.supybot.protocols.irc.banmask banmask = banmaskstyle.makeBanmask(bannedHostmask, [o[0] for o in optlist]) except KeyError: if not conf.supybot.protocols.irc.strictRfc() and \ target.startswith('$'): # Select the last part, or the whole target: bannedNick = target.split(':')[-1] banmask = bannedHostmask = target else: irc.error(format(_('I haven\'t seen %s.'), bannedNick), Raise=True) else: bannedNick = ircutils.nickFromHostmask(target) banmask = bannedHostmask = target if not irc.isNick(bannedNick): self.log.warning('%q tried to kban a non nick: %q', msg.prefix, bannedNick) raise callbacks.ArgumentError elif bannedNick == irc.nick: if kick: self.log.warning('%q tried to make me kban myself.', msg.prefix) irc.error(_('I cowardly refuse to kickban myself.')) else: self.log.warning('%q tried to make me ban myself.', msg.prefix) irc.error(_('I cowardly refuse to ban myself.')) return if not reason: reason = msg.nick capability = ircdb.makeChannelCapability(channel, 'op') # Check (again) that they're not trying to make us kickban ourself. if ircutils.hostmaskPatternEqual(banmask, irc.prefix): if ircutils.hostmaskPatternEqual(bannedHostmask, irc.prefix): self.log.warning('%q tried to make me kban myself.',msg.prefix) irc.error(_('I cowardly refuse to ban myself.')) return else: self.log.warning('Using exact hostmask since banmask would ' 'ban myself.') banmask = bannedHostmask # Now, let's actually get to it. Check to make sure they have # #channel,op and the bannee doesn't have #channel,op; or that the # bannee and the banner are both the same person. def doBan(): if irc.state.channels[channel].isOp(bannedNick): irc.queueMsg(ircmsgs.deop(channel, bannedNick)) irc.queueMsg(ircmsgs.ban(channel, banmask)) if kick: irc.queueMsg(ircmsgs.kick(channel, bannedNick, reason)) if expiry > 0: def f(): if channel in irc.state.channels and \ banmask in irc.state.channels[channel].bans: irc.queueMsg(ircmsgs.unban(channel, banmask)) schedule.addEvent(f, expiry) if bannedNick == msg.nick: doBan() elif ircdb.checkCapability(msg.prefix, capability): if ircdb.checkCapability(bannedHostmask, capability) and \ not ircdb.checkCapability(msg.prefix, 'owner'): self.log.warning('%s tried to ban %q, but both have %s', msg.prefix, bannedHostmask, capability) irc.error(format(_('%s has %s too, you can\'t ban ' 'them.'), bannedNick, capability)) else: doBan() else: self.log.warning('%q attempted kban without %s', msg.prefix, capability) irc.errorNoCapability(capability) @internationalizeDocstring def unban(self, irc, msg, args, channel, hostmask): """[] [] Unbans on . If is not given, unbans any hostmask currently banned on that matches your current hostmask. Especially useful for unbanning yourself when you get unexpectedly (or accidentally) banned from the channel. is only necessary if the message isn't sent in the channel itself. """ if hostmask == '--all': bans = irc.state.channels[channel].bans self._sendMsg(irc, ircmsgs.unbans(channel, bans)) elif hostmask: self._sendMsg(irc, ircmsgs.unban(channel, hostmask)) else: bans = [] for banmask in irc.state.channels[channel].bans: if ircutils.hostmaskPatternEqual(banmask, msg.prefix): bans.append(banmask) if bans: irc.queueMsg(ircmsgs.unbans(channel, bans)) irc.replySuccess(format(_('All bans on %s matching %s ' 'have been removed.'), channel, msg.prefix)) else: irc.error(_('No bans matching %s were found on %s.') % (msg.prefix, channel)) unban = wrap(unban, ['op', ('haveHalfop+', _('unban someone')), additional( first('hostmask', ('literal', '--all')))]) @internationalizeDocstring def listbans(self, irc, msg, args, channel): """[] List all bans on the channel. If is not given, it defaults to the current channel.""" irc.replies(irc.state.channels[channel].bans or [_('No bans.')]) listbans = wrap(listbans, ['channel']) @internationalizeDocstring def invite(self, irc, msg, args, channel, nick): """[] If you have the #channel,op capability, this will invite to join . is only necessary if the message isn't sent in the channel itself. """ nick = nick or msg.nick self._sendMsg(irc, ircmsgs.invite(nick, channel)) self.invites[(irc.getRealIrc(), ircutils.toLower(nick))] = irc invite = wrap(invite, ['op', ('haveHalfop+', _('invite someone')), additional('nick')]) def do341(self, irc, msg): (foo, nick, channel) = msg.args nick = ircutils.toLower(nick) replyIrc = self.invites.pop((irc, nick), None) if replyIrc is not None: self.log.info('Inviting %s to %s by command of %s.', nick, channel, replyIrc.msg.prefix) replyIrc.replySuccess() else: self.log.info('Inviting %s to %s.', nick, channel) def do443(self, irc, msg): (foo, nick, channel, foo) = msg.args nick = ircutils.toLower(nick) replyIrc = self.invites.pop((irc, nick), None) if replyIrc is not None: replyIrc.error(format(_('%s is already in %s.'), nick, channel)) def do401(self, irc, msg): nick = msg.args[1] nick = ircutils.toLower(nick) replyIrc = self.invites.pop((irc, nick), None) if replyIrc is not None: replyIrc.error(format(_('There is no %s on this network.'), nick)) def do504(self, irc, msg): nick = msg.args[1] nick = ircutils.toLower(nick) replyIrc = self.invites.pop((irc, nick), None) if replyIrc is not None: replyIrc.error(format('There is no %s on this server.', nick)) class lobotomy(callbacks.Commands): @internationalizeDocstring def add(self, irc, msg, args, channel): """[] If you have the #channel,op capability, this will "lobotomize" the bot, making it silent and unanswering to all requests made in the channel. is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) c.lobotomized = True ircdb.channels.setChannel(channel, c) irc.replySuccess() add = wrap(add, ['op']) @internationalizeDocstring def remove(self, irc, msg, args, channel): """[] If you have the #channel,op capability, this will unlobotomize the bot, making it respond to requests made in the channel again. is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) c.lobotomized = False ircdb.channels.setChannel(channel, c) irc.replySuccess() remove = wrap(remove, ['op']) @internationalizeDocstring def list(self, irc, msg, args): """takes no arguments Returns the channels in which this bot is lobotomized. """ L = [] for (channel, c) in ircdb.channels.items(): if c.lobotomized: chancap = ircdb.makeChannelCapability(channel, 'op') if ircdb.checkCapability(msg.prefix, 'admin') or \ ircdb.checkCapability(msg.prefix, chancap) or \ (channel in irc.state.channels and \ msg.nick in irc.state.channels[channel].users): L.append(channel) if L: L.sort() s = format(_('I\'m currently lobotomized in %L.'), L) irc.reply(s) else: irc.reply(_('I\'m not currently lobotomized in any channels ' 'that you\'re in.')) list = wrap(list) class ban(callbacks.Commands): def hostmask(self, irc, msg, args, channel, banmask): """[] Bans the from the .""" irc.queueMsg(ircmsgs.ban(channel, banmask)) hostmask = wrap(hostmask, ['op', ('haveHalfop+', _('ban someone')), 'text']) @internationalizeDocstring def add(self, irc, msg, args, channel, banmask, expires): """[] [] If you have the #channel,op capability, this will effect a persistent ban from interacting with the bot on the given (or the current hostmask associated with ). Other plugins may enforce this ban by actually banning users with matching hostmasks when they join. is an optional argument specifying when (in "seconds from now") the ban should expire; if none is given, the ban will never automatically expire. is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) c.addBan(banmask, expires) ircdb.channels.setChannel(channel, c) irc.replySuccess() add = wrap(add, ['op', first('hostmask', 'banmask'), additional('expiry', 0)]) @internationalizeDocstring def remove(self, irc, msg, args, channel, banmask): """[] If you have the #channel,op capability, this will remove the persistent ban on . is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) try: c.removeBan(banmask) ircdb.channels.setChannel(channel, c) irc.replySuccess() except KeyError: irc.error(_('There are no persistent bans for that hostmask.')) remove = wrap(remove, ['op', 'hostmask']) @internationalizeDocstring def list(self, irc, msg, args, channel, mask): """[] [] If you have the #channel,op capability, this will show you the current persistent bans on the that match the given mask, if any (returns all of them otherwise). Note that you can use * as a wildcard on masks and \\* to match actual * in masks """ all_bans = ircdb.channels.getChannel(channel).bans if mask: mask = mask.replace(r'\*', '[*]') filtered_bans = fnmatch.filter(all_bans, mask) else: filtered_bans = all_bans if filtered_bans: bans = [] for ban in filtered_bans: if all_bans[ban]: bans.append(format(_('%q (expires %t)'), ban, all_bans[ban])) else: bans.append(format(_('%q (never expires)'), ban, all_bans[ban])) irc.reply(format('%L', bans)) else: irc.reply(format(_('There are no persistent bans on %s.'), channel)) list = wrap(list, ['op', optional('somethingWithoutSpaces')]) class ignore(callbacks.Commands): @internationalizeDocstring def add(self, irc, msg, args, channel, banmask, expires): """[] [] If you have the #channel,op capability, this will set a persistent ignore on or the hostmask currently associated with . is an optional argument specifying when (in "seconds from now") the ignore will expire; if it isn't given, the ignore will never automatically expire. is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) c.addIgnore(banmask, expires) ircdb.channels.setChannel(channel, c) irc.replySuccess() add = wrap(add, ['op', 'banmask', additional('expiry', 0)]) @internationalizeDocstring def remove(self, irc, msg, args, channel, banmask): """[] If you have the #channel,op capability, this will remove the persistent ignore on in the channel. is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) try: c.removeIgnore(banmask) ircdb.channels.setChannel(channel, c) irc.replySuccess() except KeyError: irc.error(_('There are no ignores for that hostmask.')) remove = wrap(remove, ['op', 'banmask']) @internationalizeDocstring def list(self, irc, msg, args, channel): """[] Lists the hostmasks that the bot is ignoring on the given channel. is only necessary if the message isn't sent in the channel itself. """ # XXX Add the expirations. c = ircdb.channels.getChannel(channel) if len(c.ignores) == 0: s = format(_('I\'m not currently ignoring any hostmasks in ' '%q'), channel) irc.reply(s) else: L = sorted(c.ignores) irc.reply(utils.str.commaAndify(list(map(repr, L)))) list = wrap(list, ['op']) class capability(callbacks.Commands): @internationalizeDocstring def add(self, irc, msg, args, channel, user, capabilities): """[] [ ...] If you have the #channel,op capability, this will give the (or the user to whom maps) the capability in the channel. is only necessary if the message isn't sent in the channel itself. """ for c in capabilities.split(): c = ircdb.makeChannelCapability(channel, c) user.addCapability(c) ircdb.users.setUser(user) irc.replySuccess() add = wrap(add, ['op', 'otherUser', 'capability']) @internationalizeDocstring def remove(self, irc, msg, args, channel, user, capabilities): """[] [ ...] If you have the #channel,op capability, this will take from the user currently identified as (or the user to whom maps) the capability in the channel. is only necessary if the message isn't sent in the channel itself. """ fail = [] for c in capabilities.split(): cap = ircdb.makeChannelCapability(channel, c) try: user.removeCapability(cap) except KeyError: fail.append(c) ircdb.users.setUser(user) if fail: s = 'capability' if len(fail) > 1: s = utils.str.pluralize(s) irc.error(format(_('That user didn\'t have the %L %s.'), fail, s), Raise=True) irc.replySuccess() remove = wrap(remove, ['op', 'otherUser', 'capability']) # XXX This needs to be fix0red to be like Owner.defaultcapability. Or # something else. This is a horrible interface. @internationalizeDocstring def setdefault(self, irc, msg, args, channel, v): """[] {True|False} If you have the #channel,op capability, this will set the default response to non-power-related (that is, not {op, halfop, voice}) capabilities to be the value you give. is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) if v: c.setDefaultCapability(True) else: c.setDefaultCapability(False) ircdb.channels.setChannel(channel, c) irc.replySuccess() setdefault = wrap(setdefault, ['op', 'boolean']) @internationalizeDocstring def set(self, irc, msg, args, channel, capabilities): """[] [ ...] If you have the #channel,op capability, this will add the channel capability for all users in the channel. is only necessary if the message isn't sent in the channel itself. """ chan = ircdb.channels.getChannel(channel) for c in capabilities: chan.addCapability(c) ircdb.channels.setChannel(channel, chan) irc.replySuccess() set = wrap(set, ['op', many('capability')]) @internationalizeDocstring def unset(self, irc, msg, args, channel, capabilities): """[] [ ...] If you have the #channel,op capability, this will unset the channel capability so each user's specific capability or the channel default capability will take precedence. is only necessary if the message isn't sent in the channel itself. """ chan = ircdb.channels.getChannel(channel) fail = [] for c in capabilities: try: chan.removeCapability(c) except KeyError: fail.append(c) ircdb.channels.setChannel(channel, chan) if fail: s = _('capability') if len(fail) > 1: s = utils.str.pluralize(s) irc.error(format(_('I do not know about the %L %s.'), fail, s), Raise=True) irc.replySuccess() unset = wrap(unset, ['op', many('capability')]) @internationalizeDocstring def list(self, irc, msg, args, channel): """[] Returns the capabilities present on the . is only necessary if the message isn't sent in the channel itself. """ c = ircdb.channels.getChannel(channel) L = sorted(c.capabilities) irc.reply(' '.join(L)) list = wrap(list, ['channel']) @internationalizeDocstring def disable(self, irc, msg, args, channel, plugin, command): """[] [] [] If you have the #channel,op capability, this will disable the in . If is provided, will be disabled only for that plugin. If only is provided, all commands in the given plugin will be disabled. is only necessary if the message isn't sent in the channel itself. """ chan = ircdb.channels.getChannel(channel) failMsg = '' if plugin: s = '-%s' % plugin.name() if command: if plugin.isCommand(command): s = '-%s.%s' % (plugin.name(), command) else: failMsg = format(_('The %s plugin does not have a command ' 'called %s.'), plugin.name(), command) elif command: # findCallbackForCommand if list(filter(None, irc.findCallbacksForArgs([command]))): s = '-%s' % command else: failMsg = format(_('No plugin or command named %s could be ' 'found.'), command) else: raise callbacks.ArgumentError if failMsg: irc.error(failMsg) else: chan.addCapability(s) ircdb.channels.setChannel(channel, chan) irc.replySuccess() disable = wrap(disable, ['op', optional(('plugin', False)), additional('commandName')]) @internationalizeDocstring def enable(self, irc, msg, args, channel, plugin, command): """[] [] [] If you have the #channel,op capability, this will enable the in if it has been disabled. If is provided, will be enabled only for that plugin. If only is provided, all commands in the given plugin will be enabled. is only necessary if the message isn't sent in the channel itself. """ chan = ircdb.channels.getChannel(channel) failMsg = '' if plugin: s = '-%s' % plugin.name() if command: if plugin.isCommand(command): s = '-%s.%s' % (plugin.name(), command) else: failMsg = format(_('The %s plugin does not have a command ' 'called %s.'), plugin.name(), command) elif command: # findCallbackForCommand if list(filter(None, irc.findCallbacksForArgs([command]))): s = '-%s' % command else: failMsg = format(_('No plugin or command named %s could be ' 'found.'), command) else: raise callbacks.ArgumentError if failMsg: irc.error(failMsg) else: fail = [] try: chan.removeCapability(s) except KeyError: fail.append(s) ircdb.channels.setChannel(channel, chan) if fail: irc.error(format(_('%s was not disabled.'), s[1:])) else: irc.replySuccess() enable = wrap(enable, ['op', optional(('plugin', False)), additional('commandName')]) @internationalizeDocstring def nicks(self, irc, msg, args, channel, optlist): """[] [--count] Returns the nicks in . is only necessary if the message isn't sent in the channel itself. Returns only the number of nicks if --count option is provided. """ # Make sure we don't elicit information about private channels to # people or channels that shouldn't know. Someone is allowed if # any of these are true: # * the channel is not secret (mode +s), # * the request is sent to the channel itself (FIXME: what about # channels without +n?), # * the requester is op, # * the request is not sent to a channel (or else everyone in the # channel would see the response) and the requester is in the # channel themselves capability = ircdb.makeChannelCapability(channel, 'op') if 's' in irc.state.channels[channel].modes and \ msg.channel != channel and \ not ircdb.checkCapability(msg.prefix, capability) and \ (msg.channel or \ msg.nick not in irc.state.channels[channel].users): irc.error(_('You don\'t have access to that information.'), Raise=True) L = list(irc.state.channels[channel].users) keys = [option for (option, arg) in optlist] if 'count' not in keys: utils.sortBy(str.lower, L) private = self.registryValue("nicksInPrivate", channel, irc.network) irc.reply(utils.str.commaAndify(L), private=private) else: irc.reply(str(len(L))) nicks = wrap(nicks, ['inChannel', getopts({'count':''})]) @internationalizeDocstring def alertOps(self, irc, msg, channel, s, frm=None): """Internal message for notifying all the #channel,ops in a channel of a given situation.""" capability = ircdb.makeChannelCapability(channel, 'op') s = format(_('Alert to all %s ops: %s'), channel, s) if frm is not None: s += format(_(' (from %s)'), frm) for nick in irc.state.channels[channel].users: prefix = irc.state.nicksToHostmasks.get(nick) if not prefix: continue if not ircdb.checkCapability(prefix, capability): continue irc.reply(s, to=nick, private=True) irc.replySuccess() @internationalizeDocstring def alert(self, irc, msg, args, channel, text): """[] Sends to all the users in who have the ,op capability. """ self.alertOps(irc, msg, channel, text, frm=msg.nick) alert = wrap(alert, ['inChannel', 'text']) @internationalizeDocstring def part(self, irc, msg, args, channel, reason): """[] [] Tells the bot to part the list of channels you give it. is only necessary if you want the bot to part a channel other than the current channel. If is specified, use it as the part message. Otherwise, the default part message specified in supybot.plugins.Channel.partMsg will be used. No part message will be used if no default is configured. """ channel = channel or msg.channel if not channel: irc.error(Raise=True) capability = ircdb.makeChannelCapability(channel, 'op') if not ircdb.checkCapabilities(msg.prefix, [capability, 'admin']): irc.errorNoCapability(capability, Raise=True) try: network = conf.supybot.networks.get(irc.network) network.channels().remove(channel) except KeyError: pass if channel not in irc.state.channels: irc.error(_('I\'m not in %s.') % channel, Raise=True) reason = (reason or self.registryValue("partMsg", channel, irc.network)) reason = ircutils.standardSubstitute(irc, msg, reason) irc.queueMsg(ircmsgs.part(channel, reason)) if msg.nick in irc.state.channels[channel].users: irc.noReply() else: irc.replySuccess() part = wrap(part, [optional('validChannel'), additional('text')]) Class = Channel # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Channel/test.py0000644000175000017500000003204014535072470016501 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 * import supybot.conf as conf import supybot.ircdb as ircdb import supybot.ircmsgs as ircmsgs class ChannelTestCase(ChannelPluginTestCase): plugins = ('Channel', 'User') def setUp(self): super(ChannelTestCase, self).setUp() self.irc.state.channels[self.channel].addUser('foo') self.irc.state.channels[self.channel].addUser('bar') def testLobotomies(self): self.assertRegexp('lobotomy list', 'not.*any') def testCapabilities(self): self.prefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar', prefix=self.prefix)) u = ircdb.users.getUser(0) u.addCapability('%s.op' % self.channel) ircdb.users.setUser(u) self.assertNotError(' ') self.assertResponse('user capabilities foo', '[]') self.assertNotError('channel addcapability foo op') self.assertRegexp('channel capabilities foo', 'op') self.assertNotError('channel removecapability foo op') self.assertResponse('user capabilities foo', '[]') def testCapabilities(self): self.assertNotError('channel capability list') self.assertNotError('channel capability set -foo') self.assertNotError('channel capability unset -foo') self.assertError('channel capability unset -foo') self.assertNotError('channel capability set -foo bar baz') self.assertRegexp('channel capability list', 'baz') self.assertNotError('channel capability unset -foo baz') self.assertError('channel capability unset baz') def testEnableDisable(self): self.assertNotRegexp('channel capability list', '-Channel') self.assertError('channel enable channel') self.assertNotError('channel disable channel') self.assertRegexp('channel capability list', '-Channel') self.assertNotError('channel enable channel') self.assertNotRegexp('channel capability list', '-Channel') self.assertNotError('channel disable channel nicks') self.assertRegexp('channel capability list', '-Channel.nicks') self.assertNotError('channel enable channel nicks') self.assertNotRegexp('channel capability list', '-Channel.nicks') self.assertNotRegexp('channel capability list', 'nicks') self.assertNotError('channel disable nicks') self.assertRegexp('channel capability list', 'nicks') self.assertNotError('channel enable nicks') self.assertError('channel disable invalidPlugin') self.assertError('channel disable channel invalidCommand') def testUnban(self): self.assertError('unban foo!bar@baz') self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) m = self.getMsg('unban foo!bar@baz') self.assertEqual(m.command, 'MODE') self.assertEqual(m.args, (self.channel, '-b', 'foo!bar@baz')) self.assertNoResponse(' ', 2) def testErrorsWithoutOps(self): for s in 'op deop halfop dehalfop voice devoice kick invite'.split(): self.assertError('%s foo' % s) self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) self.assertNotError('%s foo' % s) self.irc.feedMsg(ircmsgs.deop(self.channel, self.nick)) def testWontDeItself(self): for s in 'deop dehalfop'.split(): self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) self.assertError('%s %s' % (s, self.nick)) def testCanDevoiceSelf(self): self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) self.assertNotError('devoice %s' % self.nick) def testOp(self): self.assertError('op') self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) self.assertNotError('op') m = self.getMsg('op foo') self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+o', 'foo')) m = self.getMsg('op foo bar') self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+o', 'foo')) m = self.irc.takeMsg() self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+o', 'bar')) self.irc.state.supported['MODES'] = 2 m = self.getMsg('op foo bar') try: self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+oo', 'foo', 'bar')) finally: self.irc.state.supported['MODES'] = 1 def testHalfOp(self): self.assertError('halfop') self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) self.assertNotError('halfop') m = self.getMsg('halfop foo') self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+h', 'foo')) m = self.getMsg('halfop foo bar') self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+h', 'foo')) m = self.irc.takeMsg() self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+h', 'bar')) def testVoice(self): self.assertError('voice') self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) self.assertNotError('voice') m = self.getMsg('voice foo') self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+v', 'foo')) m = self.getMsg('voice foo bar') self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+v', 'foo')) m = self.irc.takeMsg() self.assertTrue(m.command == 'MODE' and m.args == (self.channel, '+v', 'bar')) def assertKban(self, query, hostmask, **kwargs): m = self.getMsg(query, **kwargs) self.assertEqual(m, ircmsgs.ban(self.channel, hostmask)) m = self.getMsg(' ') self.assertEqual(m.command, 'KICK') def assertBan(self, query, hostmask, **kwargs): m = self.getMsg(query, **kwargs) self.assertEqual(m, ircmsgs.ban(self.channel, hostmask)) def testIban(self): self.irc.feedMsg(ircmsgs.join(self.channel, prefix='foobar!user@host.domain.tld')) self.assertError('iban foo!bar@baz') self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick)) self.assertBan('iban foo!bar@baz', 'foo!bar@baz') self.assertBan('iban foobar', 'foobar!user@host.domain.tld') conf.supybot.protocols.irc.strictRfc.setValue(True) self.assertError('iban $a:nyuszika7h') self.assertError('unban $a:nyuszika7h') conf.supybot.protocols.irc.strictRfc.setValue(False) self.assertBan('iban $a:nyuszika7h', '$a:nyuszika7h') self.assertNotError('unban $a:nyuszika7h') def testKban(self): self.irc.prefix = 'something!else@somehwere.else' self.irc.nick = 'something' def join(): self.irc.feedMsg(ircmsgs.join( self.channel, prefix='foobar!user@host.domain.tld')) join() self.assertError('kban foobar') self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick)) #self.assertError('kban foobar -1') #self.assertKban('kban foobar', '*!*@*.domain.tld') #join() self.assertKban('kban --exact foobar', 'foobar!user@host.domain.tld') join() self.assertKban('kban --host foobar', '*!*@host.domain.tld') join() self.assertKban('kban --user foobar', '*!user@*') join() self.assertKban('kban --nick foobar', 'foobar!*@*') join() self.assertKban('kban --nick --user foobar', 'foobar!user@*') join() self.assertKban('kban --nick --host foobar', 'foobar!*@host.domain.tld') join() self.assertKban('kban --user --host foobar', '*!user@host.domain.tld') join() self.assertKban('kban --nick --user --host foobar', 'foobar!user@host.domain.tld') join() self.assertKban('kban foobar', '*!*@host.domain.tld') self.assertRegexp('kban adlkfajsdlfkjsd', 'adlkfajsdlfkjsd is not in') def testBan(self): with conf.supybot.protocols.irc.banmask.context(['exact']): self.assertNotError('ban add foo!bar@baz') self.assertNotError('ban remove foo!bar@baz') orig = conf.supybot.protocols.irc.strictRfc() with conf.supybot.protocols.irc.strictRfc.context(True): # something wonky is going on here. irc.error (src/Channel.py|449) # is being called but the assert is failing self.assertError('ban add not!a.hostmask') self.assertNotRegexp('ban add not!a.hostmask', 'KeyError') self.assertError('ban add $a:nyuszika7h') self.assertError('ban remove $a:nyuszika7h') conf.supybot.protocols.irc.strictRfc.setValue(False) self.assertNotError('ban add $a:nyuszika7h') self.assertNotError('ban remove $a:nyuszika7h') def testBanList(self): self.assertNotError('ban add foo!bar@baz') self.assertNotError('ban add foobar!*@baz') self.assertNotError('ban add foobar!qux@baz') self.assertRegexp('ban list', r'.*foo!bar@baz.*') self.assertRegexp('ban list', r'.*foobar!\*@baz.*') self.assertRegexp('ban list', r'.*foobar!qux@baz.*') self.assertNotRegexp('ban list foobar!*@baz', r'.*foo!bar@baz.*') self.assertRegexp('ban list foobar!*@baz', r'.*foobar!\*@baz.*') self.assertRegexp('ban list foobar!*@baz', r'.*foobar!qux@baz.*') self.assertResponse('ban list foobar!\\*@baz', '"foobar!*@baz" (never expires)') def testIgnore(self): orig = conf.supybot.protocols.irc.banmask() def ignore(given, expect=None): if expect is None: expect = given self.assertNotError('channel ignore add %s' % given) self.assertResponse('channel ignore list', "'%s'" % expect) self.assertNotError('channel ignore remove %s' % expect) self.assertRegexp('channel ignore list', 'not currently') ignore('foo!bar@baz', '*!*@baz') ignore('foo!*@*') with conf.supybot.protocols.irc.banmask.context(['exact']): ignore('foo!bar@baz') ignore('foo!*@*') self.assertError('ban add not!a.hostmask') def testNicks(self): self.assertResponse('channel nicks', 'bar, foo, and test') self.assertResponse('channel nicks --count', '3') def testPart(self): def getAfterJoinMessages(): m = self.irc.takeMsg() self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.assertEqual(m.command, 'WHO') self.assertError('part #foo') self.assertRegexp('part #foo', 'not in') self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix)) getAfterJoinMessages() m = self.getMsg('part #foo') self.assertEqual(m.command, 'PART') self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix)) getAfterJoinMessages() m = self.getMsg('part #foo reason') self.assertEqual(m.command, 'PART') self.assertEqual(m.args[0], '#foo') self.assertEqual(m.args[1], 'reason') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/ChannelLogger/0000755000175000017500000000000014535072535016313 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/__init__.py0000644000175000017500000000474314535072470020432 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Logs each channel to its own individual logfile. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/config.py0000644000175000017500000001355214535072470020136 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('ChannelLogger') 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('ChannelLogger', True) ChannelLogger = conf.registerPlugin('ChannelLogger') conf.registerChannelValue(ChannelLogger, 'enable', registry.Boolean(True, _("""Determines whether logging is enabled."""))) conf.registerGlobalValue(ChannelLogger, 'flushImmediately', registry.Boolean(True, _("""Determines whether channel logfiles will be flushed anytime they're written to, rather than being buffered by the operating system."""))) conf.registerChannelValue(ChannelLogger, 'showJoinParts', registry.Boolean(True, _("""Determines whether joins and parts are logged"""))) conf.registerChannelValue(ChannelLogger, 'showAway', registry.Boolean(True, _("""Determines whether users going away and coming back should be logged. This is only supported on networks implementing the 'away-notify' IRCv3 capability."""))) conf.registerChannelValue(ChannelLogger, 'stripFormatting', registry.Boolean(True, _("""Determines whether formatting characters (such as bolding, color, etc.) are removed when writing the logs to disk."""))) conf.registerChannelValue(ChannelLogger, 'timestamp', registry.Boolean(True, _("""Determines whether the logs for this channel are timestamped with the timestamp in supybot.log.timestampFormat."""))) conf.registerChannelValue(ChannelLogger, 'noLogPrefix', registry.String('[nolog]', _("""Determines what string a message should be prefixed with in order not to be logged. If you don't want any such prefix, just set it to the empty string."""))) conf.registerChannelValue(ChannelLogger, 'rotateLogs', registry.Boolean(False, _("""Determines whether the bot will automatically rotate the logs for this channel. The bot will rotate logs when the timestamp for the log changes. The timestamp is set according to the 'filenameTimestamp' configuration variable."""))) conf.registerChannelValue(ChannelLogger, 'filenameTimestamp', registry.String('%Y-%m-%d', _("""Determines how to represent the timestamp used for the filename in rotated logs. When this timestamp changes, the old logfiles will be closed and a new one started. The format characters for the timestamp are in the time.strftime docs at python.org. In order for your logs to be rotated, you'll also have to enable supybot.plugins.ChannelLogger.rotateLogs."""))) conf.registerChannelValue(ChannelLogger, 'rewriteRelayed', registry.Boolean(False, _("""Determines whether the bot will rewrite outgoing relayed messages (eg. from the Relay plugin) to use the original nick instead of the bot's nick."""))) conf.registerGlobalValue(ChannelLogger, 'directories', registry.Boolean(True, _("""Determines whether the bot will partition its channel logs into separate directories based on different criteria."""))) conf.registerGlobalValue(ChannelLogger.directories, 'network', registry.Boolean(True, _("""Determines whether the bot will use a network directory if using directories."""))) conf.registerGlobalValue(ChannelLogger.directories, 'channel', registry.Boolean(True, _("""Determines whether the bot will use a channel directory if using directories."""))) conf.registerGlobalValue(ChannelLogger.directories, 'timestamp', registry.Boolean(False, _("""Determines whether the bot will use a timestamp (determined by supybot.plugins.ChannelLogger.directories.timestamp.format) if using directories."""))) conf.registerGlobalValue(ChannelLogger.directories.timestamp, 'format', registry.String('%B', _("""Determines what timestamp format will be used in the directory structure for channel logs if supybot.plugins.ChannelLogger.directories.timestamp is True."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/ChannelLogger/locales/0000755000175000017500000000000014535072535017735 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/locales/fi.po0000644000175000017500000001256314535072470020700 0ustar00valval# ChannelLogger plugin in Limnoria. # Copyright (C) 2011, 2012 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Supybot Channellogger\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:13+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: suomi <>\n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" "X-Generator: Poedit 1.6.10\n" #: config.py:47 msgid "Determines whether logging is enabled." msgstr "Määrittää onko lokin pitäminen käytössä." #: config.py:49 msgid "" "Determines whether channel logfiles will be\n" " flushed anytime they're written to, rather than being buffered by the\n" " operating system." msgstr "" "Määrittää pitäisikö kanava lokitiedostot\n" " tallentaa silloin kun ne kirjoitetaan, mielummin kuin käyttöjärjestelmän\n" " puskuroimana." #: config.py:53 #, fuzzy msgid "Determines whether joins and parts are logged" msgstr "Määrittää kirjoitetaanko liittymiset ja poistumiset lokiin" #: config.py:55 msgid "" "Determines whether users going away and coming\n" " back should be logged. This is only supported on networks implementing " "the\n" " 'away-notify' IRCv3 capability." msgstr "" #: config.py:59 msgid "" "Determines whether formatting characters (such\n" " as bolding, color, etc.) are removed when writing the logs to disk." msgstr "" "Määrittää pitääkö muotoilumerkit (kuten\n" "korostukset, väri, jne.) poistaa kun lokeja kirjoitetaan levylle." #: config.py:62 msgid "" "Determines whether the logs for this channel are\n" " timestamped with the timestamp in supybot.log.timestampFormat." msgstr "" "Määrittää laitetaanko tämän kanavan lokitiedostoihin\n" " aikaleimat aikaleimalla, joka on määritetty asetuksella supybot.log." "timestampFormat." #: config.py:65 msgid "" "Determines what string a message should be\n" " prefixed with in order not to be logged. If you don't want any such\n" " prefix, just set it to the empty string." msgstr "" "Määrittää millä merkkiketjulla aloitettuja viestejä\n" " ei tallenneta lokiin. Jos et halua\n" " merkkiketjua, aseta se tyhjäksi merkkiketjuksi." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " rotate the logs for this channel. The bot will rotate logs when the\n" " timestamp for the log changes. The timestamp is set according to\n" " the 'filenameTimestamp' configuration variable." msgstr "" "Määrittää kääntääkö botti automaattisesti\n" " lokit tällä kanavalla. Botti kääntää lokit kun\n" " kun aikaleima lokeille vaihtuu. Aikaleima asetetaan\n" " 'filenameTimestamp' asetusarvon mukaan." #: config.py:74 msgid "" "Determines how to represent the timestamp\n" " used for the filename in rotated logs. When this timestamp changes, " "the\n" " old logfiles will be closed and a new one started. The format " "characters\n" " for the timestamp are in the time.strftime docs at python.org. In " "order\n" " for your logs to be rotated, you'll also have to enable\n" " supybot.plugins.ChannelLogger.rotateLogs." msgstr "" "Määrittää kuinka aikaleima, jota käytetään\n" " tiedostonimenä käännetyille lokeille, esitetään. Kun tämä aikaleima " "muuttuu\n" " vanhat lokitiedostot suljetaan ja uudet aloitetaan. Muotomerkit\n" " aikaleimoille ovat time.strftime documenteissa python.org :issa. " "Saadaksesi\n" " lokisi käännetyksi, sinun täytyy myös ottaa käyttöön\n" " supybot.plugins.ChannelLogger.rotateLogs." #: config.py:81 msgid "" "Determines whether the bot will rewrite\n" " outgoing relayed messages (eg. from the Relay plugin) to use the " "original\n" " nick instead of the bot's nick." msgstr "" #: config.py:86 msgid "" "Determines whether the bot will partition its\n" " channel logs into separate directories based on different criteria." msgstr "" "Määrittää osioiko botti kanavalokinsa\n" " eri hakemistoihin perustuen eri kriteereihin." #: config.py:89 msgid "" "Determines whether the bot will use a network\n" " directory if using directories." msgstr "" "Määrittää käyttääkö botti verkkohakemistoa\n" " jos käytetään hakemistoja." #: config.py:92 msgid "" "Determines whether the bot will use a channel\n" " directory if using directories." msgstr "" "Määrittää käyttääkö botti kanavahakemistoa\n" " jos käytetään hakemistoja." #: config.py:95 msgid "" "Determines whether the bot will use a timestamp\n" " (determined by supybot.plugins.ChannelLogger.directories.timestamp." "format)\n" " if using directories." msgstr "" "Määrittää käyttääkö botti aikaleimaa\n" " (supybot.plugins.ChannelLogger.directories.timestamp.format " "määrittämänä\n" " jos käytetään hakemistoja." #: config.py:99 msgid "" "Determines what timestamp format will be used in\n" " the directory structure for channel logs if\n" " supybot.plugins.ChannelLogger.directories.timestamp is True." msgstr "" "Määrittää mitä aikaleimamuotoa käytetään\n" " hakemistorakenteessa kanavalokeille jos\n" " supybot.plugins.ChannelLogger.directories.timestamp on True." #: plugin.py:61 msgid "This plugin allows the bot to log channel conversations to disk." msgstr "Tämä plugin sallii botin tallentaa kanavan keskustelut levylle." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/locales/fr.po0000644000175000017500000001233414535072470020705 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:47 msgid "Determines whether logging is enabled." msgstr "Détermine si les logs sont activés." #: config.py:49 msgid "" "Determines whether channel logfiles will be\n" " flushed anytime they're written to, rather than being buffered by the\n" " operating system." msgstr "" "Détermine si le fichier de logs sera enregistré à chaque fois que l'on y " "écrit plutôt que d'être mis en buffer par le système d'exploitation." #: config.py:53 #, fuzzy msgid "Determines whether joins and parts are logged" msgstr "Détermine si les arrivées et les départs sont loggués." #: config.py:55 msgid "" "Determines whether users going away and coming\n" " back should be logged. This is only supported on networks implementing " "the\n" " 'away-notify' IRCv3 capability." msgstr "" #: config.py:59 msgid "" "Determines whether formatting characters (such\n" " as bolding, color, etc.) are removed when writing the logs to disk." msgstr "" "Détermine si les caractères de formattage (comme le gras, la couleurs) sont " "supprimés lors de l'écriture des logs sur le disque." #: config.py:62 msgid "" "Determines whether the logs for this channel are\n" " timestamped with the timestamp in supybot.log.timestampFormat." msgstr "" "Détermine si les logs de ce canal sont 'timestampés' avec le format dans " "supybot.log.timestampFormat" #: config.py:65 msgid "" "Determines what string a message should be\n" " prefixed with in order not to be logged. If you don't want any such\n" " prefix, just set it to the empty string." msgstr "" "Détermine par quelle chaîne un message doit être préfixé pour ne pas être " "loggué. Vous ne voulez probablement pas d'un tel préfixe, et simplement " "définir une chaîne vide." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " rotate the logs for this channel. The bot will rotate logs when the\n" " timestamp for the log changes. The timestamp is set according to\n" " the 'filenameTimestamp' configuration variable." msgstr "" "Détermine si le bot fera automatiquement une rotation des logs pour ce " "canal. Le bot fera tourner les logs lorsque leur timestamp changera. Le " "timestamp est défini par la variable de configuration 'filenameTimestamp'." #: config.py:74 msgid "" "Determines how to represent the timestamp\n" " used for the filename in rotated logs. When this timestamp changes, " "the\n" " old logfiles will be closed and a new one started. The format " "characters\n" " for the timestamp are in the time.strftime docs at python.org. In " "order\n" " for your logs to be rotated, you'll also have to enable\n" " supybot.plugins.ChannelLogger.rotateLogs." msgstr "" "Détermine comment représenter le timestamp utilisé pour les noms de fichiers " "de logs 'tournants'. Lorsque ce timestamp change, l'ancien fichier de logs " "sera fermé, et un nouveau sera ouvert. Le format du timestamp est le même " "que celui de time.strftime (documentation disponible sur python.org). Pour " "avoir des logs 'tournant', vous devez activer supybot.plugins.ChannelLogger." "rotateLogs." #: config.py:81 msgid "" "Determines whether the bot will rewrite\n" " outgoing relayed messages (eg. from the Relay plugin) to use the " "original\n" " nick instead of the bot's nick." msgstr "" #: config.py:86 msgid "" "Determines whether the bot will partition its\n" " channel logs into separate directories based on different criteria." msgstr "" "Détermine si le bot partitionnera les logs de canaux dans des répertoires " "séparés, en fonction de différents critères." #: config.py:89 msgid "" "Determines whether the bot will use a network\n" " directory if using directories." msgstr "" "Détermine si le bot utilisera un répertoire par réseau, si il utilise des " "répertoires." #: config.py:92 msgid "" "Determines whether the bot will use a channel\n" " directory if using directories." msgstr "" "Détermine si le bot utilisera un répertoire par canal, si il utilise des " "répertoires." #: config.py:95 msgid "" "Determines whether the bot will use a timestamp\n" " (determined by supybot.plugins.ChannelLogger.directories.timestamp." "format)\n" " if using directories." msgstr "" "Détermine si le bot utilisera un timestamp (déterminé par supybot.plugins." "ChannelLogger.directories.timestamp.format), si il utilise des répertoires." #: config.py:99 msgid "" "Determines what timestamp format will be used in\n" " the directory structure for channel logs if\n" " supybot.plugins.ChannelLogger.directories.timestamp is True." msgstr "" "Détermine quel format de timestamp sera utilisé dans la structure de " "répertoires pour les logs de canaux si supybot.plugins.ChannelLogger." "directories.timestamp est True." #: plugin.py:61 msgid "This plugin allows the bot to log channel conversations to disk." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/locales/hu.po0000644000175000017500000001274714535072470020722 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria ChannelLogger\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-19 16:21+0200\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:47 msgid "Determines whether logging is enabled." msgstr "Meghatározza, hogy a naplózás engedélyezve van-e." #: config.py:49 msgid "" "Determines whether channel logfiles will be\n" " flushed anytime they're written to, rather than being buffered by the\n" " operating system." msgstr "" "Meghatározza, hogy a csatorna naplófájlok mentve legyenek-e, amikor írva " "vannak, ahelyett, hogy az operációs rendszer pufferelje őket." #: config.py:53 #, fuzzy msgid "Determines whether joins and parts are logged" msgstr "Meghatározza, hogy a naplózás engedélyezve van-e." #: config.py:55 msgid "" "Determines whether users going away and coming\n" " back should be logged. This is only supported on networks implementing " "the\n" " 'away-notify' IRCv3 capability." msgstr "" #: config.py:59 msgid "" "Determines whether formatting characters (such\n" " as bolding, color, etc.) are removed when writing the logs to disk." msgstr "" "Meghatározza, hogy milyen formázási karakterek (olyan, mint félkövér, szín " "stb.) vannak eltávolítva a naplófájlok lemezre írásakor." #: config.py:62 msgid "" "Determines whether the logs for this channel are\n" " timestamped with the timestamp in supybot.log.timestampFormat." msgstr "" "Meghatározza, hogy a csatorna naplójai el legyenek-e látva a supybot.log." "timestampFormat-ban lévő időbélyegzővel." #: config.py:65 msgid "" "Determines what string a message should be\n" " prefixed with in order not to be logged. If you don't want any such\n" " prefix, just set it to the empty string." msgstr "" "Meghatározza, hogy milyen karakterlánccal kell kezdődnie egy üzenetnek, hogy " "ne legyen naplózva. Ha nem szeretnél semmi ilyen előtagot, csak állítsd üres " "karakterláncra." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " rotate the logs for this channel. The bot will rotate logs when the\n" " timestamp for the log changes. The timestamp is set according to\n" " the 'filenameTimestamp' configuration variable." msgstr "" "Meghatározza, hogy a bot automatikusan elforgassa-e a csatorna " "naplófájljait. A bot elforgatja a naplófájlokat, amikor a naplófájl " "időbélyegzője megváltozik. Az időbélyegző a 'filenameTimestamp' " "konfigurációs változó szerint van beállítva." #: config.py:74 msgid "" "Determines how to represent the timestamp\n" " used for the filename in rotated logs. When this timestamp changes, " "the\n" " old logfiles will be closed and a new one started. The format " "characters\n" " for the timestamp are in the time.strftime docs at python.org. In " "order\n" " for your logs to be rotated, you'll also have to enable\n" " supybot.plugins.ChannelLogger.rotateLogs." msgstr "" "Meghatározza, hogyan legyen az elforgatott naplófájlok fájlnevéhez használt " "időbélyegző ábrázolva. Amikor ez az időbélyegző megváltozik, a régi " "naplófájlok le lesznek zárva és újak lesznek nyitva. A formázási karakterek " "az időbélyegzőhez a time.strftime-ban vannak a python.org dokumentációiban. " "Ahhoz, hogy a naplófájljaid el legyenek forgatva, engedélyezned kell a " "supybot.plugins.ChannelLogger.rotateLogs-t is." #: config.py:81 msgid "" "Determines whether the bot will rewrite\n" " outgoing relayed messages (eg. from the Relay plugin) to use the " "original\n" " nick instead of the bot's nick." msgstr "" #: config.py:86 msgid "" "Determines whether the bot will partition its\n" " channel logs into separate directories based on different criteria." msgstr "" "Meghatározza, hogy a bot szétválassza-e a csatorna naplóit különálló " "könyvtárakba különböző kritériumok alapján." #: config.py:89 msgid "" "Determines whether the bot will use a network\n" " directory if using directories." msgstr "" "Meghatározza, hogy a bot használjon-e egy könyvtárat a hálózatnak, ha " "használ könyvtárakat." #: config.py:92 msgid "" "Determines whether the bot will use a channel\n" " directory if using directories." msgstr "" "Meghatározza, hogy a bot használjon-e egy könyvtárat a csatornának, ha " "használ könyvtárakat." #: config.py:95 msgid "" "Determines whether the bot will use a timestamp\n" " (determined by supybot.plugins.ChannelLogger.directories.timestamp." "format)\n" " if using directories." msgstr "" "Meghatározza, hogy a bot használjon-e egy időbályegzőt (a supybot.plugins." "ChannelLogger.directories.timestamp.format által meghatározott) ha használ " "könyvtárakat." #: config.py:99 msgid "" "Determines what timestamp format will be used in\n" " the directory structure for channel logs if\n" " supybot.plugins.ChannelLogger.directories.timestamp is True." msgstr "" "Meghatározza, milyen időbélyegző formátum lesz használva a " "könyvtárstruktúrában a csatorna naplófájlokhoz, ha a supybot.plugins." "ChannelLogger.directories.timestamp True." #: plugin.py:61 msgid "This plugin allows the bot to log channel conversations to disk." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/locales/it.po0000644000175000017500000001207714535072470020716 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-19 16:57+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:47 msgid "Determines whether logging is enabled." msgstr "Determina se i log sono abilitati." #: config.py:49 msgid "" "Determines whether channel logfiles will be\n" " flushed anytime they're written to, rather than being buffered by the\n" " operating system." msgstr "" "Determina se i file di log del canale verranno salvati ogni volta che sono\n" " scritti piuttosto che tenuti in memoria dal sistema." #: config.py:53 #, fuzzy msgid "Determines whether joins and parts are logged" msgstr "Determina se i log sono abilitati." #: config.py:55 msgid "" "Determines whether users going away and coming\n" " back should be logged. This is only supported on networks implementing " "the\n" " 'away-notify' IRCv3 capability." msgstr "" #: config.py:59 msgid "" "Determines whether formatting characters (such\n" " as bolding, color, etc.) are removed when writing the logs to disk." msgstr "" "Determina se la formattazione dei caratteri (come grassetto, colori, ecc.)\n" " viene rimossa una volta che i log sono scritti sul disco." #: config.py:62 msgid "" "Determines whether the logs for this channel are\n" " timestamped with the timestamp in supybot.log.timestampFormat." msgstr "" "Determina se i log per questo canale riportano data e ora (timestamp)\n" " con il formato indicato in supybot.log.timestampFormat." #: config.py:65 msgid "" "Determines what string a message should be\n" " prefixed with in order not to be logged. If you don't want any such\n" " prefix, just set it to the empty string." msgstr "" "Determina con quale stringa debba essere prefissato un messaggio per non\n" " essere registrato. Se non si vuole alcun prefisso, impostare una stringa " "vuota." #: config.py:69 msgid "" "Determines whether the bot will automatically\n" " rotate the logs for this channel. The bot will rotate logs when the\n" " timestamp for the log changes. The timestamp is set according to\n" " the 'filenameTimestamp' configuration variable." msgstr "" "Determina se il bot ruoterà automaticamente i log di questo canale.\n" " Verranno ruotati quando cambiano data e ora; il timestamp\n" " è impostato in base alla variabile \"filenameTimestamp\"." #: config.py:74 msgid "" "Determines how to represent the timestamp\n" " used for the filename in rotated logs. When this timestamp changes, " "the\n" " old logfiles will be closed and a new one started. The format " "characters\n" " for the timestamp are in the time.strftime docs at python.org. In " "order\n" " for your logs to be rotated, you'll also have to enable\n" " supybot.plugins.ChannelLogger.rotateLogs." msgstr "" "Determina come rappresentare il timestamp usato per il nome del file da " "ruotare.\n" " Quando questo cambia, il vecchio file di log verrà chiuso e ne inizierà " "uno\n" " nuovo; il formato è lo stesso di time.strftime (documentazione su python." "org).\n" " Affinché i log vengano ruotati, è inoltre necessario abilitare supybot." "plugins.ChannelLogger.rotateLogs." #: config.py:81 msgid "" "Determines whether the bot will rewrite\n" " outgoing relayed messages (eg. from the Relay plugin) to use the " "original\n" " nick instead of the bot's nick." msgstr "" #: config.py:86 msgid "" "Determines whether the bot will partition its\n" " channel logs into separate directories based on different criteria." msgstr "" "Determina se il bot suddividerà i log del canale in directory separate in " "base a criteri differenti." #: config.py:89 msgid "" "Determines whether the bot will use a network\n" " directory if using directories." msgstr "" "Determina se, in caso si utilizzino directory, il bot userà una directory " "per network." #: config.py:92 msgid "" "Determines whether the bot will use a channel\n" " directory if using directories." msgstr "" "Determina se, in caso si utilizzino directory, il bot userà una directory " "per canale." #: config.py:95 msgid "" "Determines whether the bot will use a timestamp\n" " (determined by supybot.plugins.ChannelLogger.directories.timestamp." "format)\n" " if using directories." msgstr "" "Determina se, in caso si utilizzino directory, il bot userà un timestamp\n" " (definito da supybot.plugins.ChannelLogger.directories.timestamp.format)." #: config.py:99 msgid "" "Determines what timestamp format will be used in\n" " the directory structure for channel logs if\n" " supybot.plugins.ChannelLogger.directories.timestamp is True." msgstr "" "Determina quale formato di timestamp sarà utilizzato nella struttura della " "directory\n" " dei log del canale se supybot.plugins.ChannelLogger.directories." "timestamp è impostata a True." #: plugin.py:61 msgid "This plugin allows the bot to log channel conversations to disk." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/plugin.py0000644000175000017500000003266114535072470020171 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, 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 sys import time import supybot.conf as conf import supybot.world as world import supybot.ircdb as ircdb import supybot.utils as utils import supybot.irclib as irclib import supybot.utils.minisix as minisix import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('ChannelLogger') if minisix.PY2: from io import open class FakeLog(object): def flush(self): return def close(self): return def write(self, s): return class ChannelLogger(callbacks.Plugin): """This plugin allows the bot to log channel conversations to disk.""" noIgnore = True echoMessage = True def __init__(self, irc): self.__parent = super(ChannelLogger, self) self.__parent.__init__(irc) self.logs = {} self.flusher = self.flush world.flushers.append(self.flusher) # 10 minutes is long enough to be confident the server won't send # messages after they timeouted. self._emitted_relayed_msgs = utils.structures.ExpiringDict(10*60) def die(self): for log in self._logs(): log.close() world.flushers = [x for x in world.flushers if x is not self.flusher] def reset(self): for log in self._logs(): log.close() self.logs.clear() def _logs(self): for logs in self.logs.values(): for log in logs.values(): yield log def flush(self): self.checkLogNames() for log in self._logs(): try: log.flush() except ValueError as e: if e.args[0] != 'I/O operation on a closed file': self.log.exception('Odd exception:') def logNameTimestamp(self, network, channel): format = self.registryValue('filenameTimestamp', channel, network) return time.strftime(format) def getLogName(self, network, channel): if self.registryValue('rotateLogs', channel, network): name = '%s.%s.log' % (channel, self.logNameTimestamp(network, channel)) else: name = '%s.log' % channel return utils.file.sanitizeName(name) def getLogDir(self, irc, channel): channel = self.normalizeChannel(irc, channel) logDir = conf.supybot.directories.log.dirize(self.name()) if self.registryValue('directories'): if self.registryValue('directories.network'): logDir = os.path.join(logDir, irc.network) if self.registryValue('directories.channel'): logDir = os.path.join(logDir, utils.file.sanitizeName(channel)) if self.registryValue('directories.timestamp'): format = self.registryValue('directories.timestamp.format') timeDir =time.strftime(format) logDir = os.path.join(logDir, timeDir) if not os.path.exists(logDir): os.makedirs(logDir) return logDir def checkLogNames(self): for (irc, logs) in self.logs.items(): for (channel, log) in list(logs.items()): if self.registryValue('rotateLogs', channel, irc.network): name = self.getLogName(irc.network, channel) if name != os.path.basename(log.name): log.close() del logs[channel] def getLog(self, irc, channel): self.checkLogNames() try: logs = self.logs[irc] except KeyError: logs = ircutils.IrcDict() self.logs[irc] = logs if channel in logs: return logs[channel] else: try: name = self.getLogName(irc.network, channel) logDir = self.getLogDir(irc, channel) log = open(os.path.join(logDir, name), encoding='utf-8', mode='a') logs[channel] = log return log except IOError: self.log.exception('Error opening log:') return FakeLog() def timestamp(self, log): format = conf.supybot.log.timestampFormat() if format: string = time.strftime(format) + ' ' if minisix.PY2: string = string.decode('utf8', 'ignore') log.write(string) def normalizeChannel(self, irc, channel): return ircutils.toLower(channel) def doLog(self, irc, channel, s, *args): if not self.registryValue('enable', channel, irc.network): return s = format(s, *args) channel = self.normalizeChannel(irc, channel) log = self.getLog(irc, channel) if self.registryValue('timestamp', channel, irc.network): self.timestamp(log) if self.registryValue('stripFormatting', channel, irc.network): s = ircutils.stripFormatting(s) if minisix.PY2: s = s.decode('utf8', 'ignore') log.write(s) if self.registryValue('flushImmediately'): log.flush() def doPrivmsg(self, irc, msg): (recipients, text) = msg.args for channel in recipients.split(','): if irc.isChannel(channel): noLogPrefix = self.registryValue('noLogPrefix', channel, irc.network) cap = ircdb.makeChannelCapability(channel, 'logChannelMessages') try: logChannelMessages = ircdb.checkCapability(msg.prefix, cap, ignoreOwner=True) except KeyError: logChannelMessages = True nick = msg.nick or irc.nick rewriteRelayed = self.registryValue('rewriteRelayed', channel, irc.network) if msg.tagged('ChannelLogger__relayed'): wasRelayed = True elif 'label' in msg.server_tags: label = msg.server_tags['label'] if label in self._emitted_relayed_msgs: del self._emitted_relayed_msgs[label] wasRelayed = True else: wasRelayed = False else: wasRelayed = False if rewriteRelayed and wasRelayed: (nick, text) = text.split(' ', 1) nick = nick[1:-1] msg.args = (recipients, text) if (noLogPrefix and text.startswith(noLogPrefix)) or \ not logChannelMessages: text = '-= THIS MESSAGE NOT LOGGED =-' if ircmsgs.isAction(msg): self.doLog(irc, channel, '* %s %s\n', nick, ircmsgs.unAction(msg)) else: self.doLog(irc, channel, '<%s> %s\n', nick, text) def doNotice(self, irc, msg): (recipients, text) = msg.args for channel in recipients.split(','): if irc.isChannel(channel): self.doLog(irc, channel, '-%s- %s\n', msg.nick, text) def doAway(self, irc, msg): # https://ircv3.net/specs/extensions/away-notify if msg.args: away_message = msg.args[-1] for channel in msg.tagged('channels'): if self.registryValue('showAway', channel, irc.network): self.doLog(irc, channel, '*** %s is now away: %s\n', msg.nick, away_message) else: for channel in msg.tagged('channels'): if self.registryValue('showAway', channel, irc.network): self.doLog(irc, channel, '*** %s is back\n', msg.nick) def doNick(self, irc, msg): oldNick = msg.nick newNick = msg.args[0] for channel in msg.tagged('channels'): self.doLog(irc, channel, '*** %s is now known as %s\n', oldNick, newNick) def doInvite(self, irc, msg): (target, channel) = msg.args self.doLog(irc, channel, '*** %s <%s> invited %s to %s\n', msg.nick, msg.prefix, target, channel) def doJoin(self, irc, msg): for channel in msg.args[0].split(','): if(self.registryValue('showJoinParts', channel, irc.network)): self.doLog(irc, channel, '*** %s <%s> has joined %s\n', msg.nick, msg.prefix, channel) def doKick(self, irc, msg): if len(msg.args) == 3: (channel, target, kickmsg) = msg.args else: (channel, target) = msg.args kickmsg = '' if kickmsg: self.doLog(irc, channel, '*** %s was kicked by %s (%s)\n', target, msg.nick, kickmsg) else: self.doLog(irc, channel, '*** %s was kicked by %s\n', target, msg.nick) def doPart(self, irc, msg): if len(msg.args) > 1: reason = " (%s)" % msg.args[1] else: reason = "" for channel in msg.args[0].split(','): if(self.registryValue('showJoinParts', channel, irc.network)): self.doLog(irc, channel, '*** %s <%s> has left %s%s\n', msg.nick, msg.prefix, channel, reason) def doMode(self, irc, msg): channel = msg.args[0] if irc.isChannel(channel) and msg.args[1:]: self.doLog(irc, channel, '*** %s sets mode: %s %s\n', msg.nick or msg.prefix, msg.args[1], ' '.join(msg.args[2:])) def doTopic(self, irc, msg): if len(msg.args) == 1: return # It's an empty TOPIC just to get the current topic. channel = msg.args[0] self.doLog(irc, channel, '*** %s changes topic to "%s"\n', msg.nick, msg.args[1]) def doQuit(self, irc, msg): if len(msg.args) == 1: reason = " (%s)" % msg.args[0] else: reason = "" for channel in msg.tagged('channels'): if(self.registryValue('showJoinParts', channel, irc.network)): self.doLog(irc, channel, '*** %s <%s> has quit IRC%s\n', msg.nick, msg.prefix, reason) def outFilter(self, irc, msg): # Mark/remember outgoing relayed messages, so we can rewrite them if # rewriteRelayed is True. if msg.command in ('PRIVMSG', 'NOTICE'): rewriteRelayed = self.registryValue( 'rewriteRelayed', msg.channel, irc.network) if rewriteRelayed and 'echo-message' in irc.state.capabilities_ack: assert 'labeled-response' in irc.state.capabilities_ack, \ 'echo-message was negotiated without labeled-response.' # If we negotiated the echo-message cap, we have to remember # this message was relayed when the server sends it back to us. if 'label' not in msg.server_tags: msg.server_tags['label'] = ircutils.makeLabel() if msg.tagged('relayedMsg'): # Remember this was a relayed message, in case # rewriteRelayed is True. self._emitted_relayed_msgs[msg.server_tags['label']] = True else: # Else, we can simply rely on internal tags, because echos are # simulated. if msg.tagged('relayedMsg'): msg.tag('ChannelLogger__relayed') return msg Class = ChannelLogger # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelLogger/test.py0000644000175000017500000002203214535072470017641 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 io from unittest.mock import patch, Mock from supybot import conf from supybot.test import * from . import plugin timestamp_re = '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ' patch_open = patch.object(plugin, 'open', create=True) class ChannelLoggerTestCase(ChannelPluginTestCase): plugins = ('ChannelLogger',) config = { 'supybot.plugins.ChannelLogger.flushImmediately': True, 'supybot.plugins.ChannelLogger.enable.:test.#foo': True, } def testLogName(self): self.assertEqual( self.irc.getCallback('ChannelLogger').getLogName('test', '#foo'), '#foo.log' ) self.assertEqual( self.irc.getCallback('ChannelLogger').getLogName('test', '#f/../oo'), '#f..oo.log' ) def testLogDir(self): self.assertEqual( self.irc.getCallback('ChannelLogger').getLogDir(self.irc, '#foo'), conf.supybot.directories.log.dirize('ChannelLogger/test/#foo') ) self.assertEqual( self.irc.getCallback('ChannelLogger').getLogDir(self.irc, '#f/../oo'), conf.supybot.directories.log.dirize('ChannelLogger/test/#f..oo') ) @patch_open def testLog(self, mock_open): mock_open.return_value = Mock() self.assertIs( self.irc.getCallback('ChannelLogger').getLog(self.irc, '#foo'), mock_open.return_value ) mock_open.assert_called_once_with( conf.supybot.directories.log.dirize('ChannelLogger/test/#foo/#foo.log'), encoding='utf-8', mode='a' ) # the log file should be cached mock_open.reset_mock() self.assertIs( self.irc.getCallback('ChannelLogger').getLog(self.irc, '#foo'), mock_open.return_value ) mock_open.assert_not_called() @patch_open def testLogPrivmsg(self, mock_open): log_file = io.StringIO() mock_open.return_value = log_file self.irc.feedMsg( ircmsgs.privmsg('#foo', 'test message', prefix='foo!bar@baz') ) self.assertRegex( log_file.getvalue(), timestamp_re + ' test message\n' ) @patch_open def _testLogRewriteRelayedEmulatedEcho(self, mock_open, relayed): with conf.supybot.plugins.ChannelLogger.rewriteRelayed.context(True): log_file = io.StringIO() mock_open.return_value = log_file msg = ircmsgs.privmsg( '#foo', ' test message', prefix='foo!bar@baz' ) if relayed: msg.tag('relayedMsg') self.irc.getCallback('ChannelLogger').outFilter(self.irc, msg) self.irc.feedMsg(msg) if relayed: content = ' test message\n' else: content = ' test message\n' self.assertRegex(log_file.getvalue(), timestamp_re + content) def testLogRewriteRelayedEmulatedEcho(self): self._testLogRewriteRelayedEmulatedEcho(relayed=True) def testLogRewriteRelayedEmulatedEchoNotRelayed(self): self._testLogRewriteRelayedEmulatedEcho(relayed=False) @patch_open def _testLogRewriteRelayedRealEcho(self, mock_open, relayed): with conf.supybot.plugins.ChannelLogger.rewriteRelayed.context(True): log_file = io.StringIO() mock_open.return_value = log_file original_caps = self.irc.state.capabilities_ack self.irc.state.capabilities_ack |= { 'echo-message', 'labeled-response' } try: out_msg = ircmsgs.privmsg('#foo', ' test message') if relayed: out_msg.tag('relayedMsg') self.irc.getCallback('ChannelLogger').outFilter(self.irc, out_msg) finally: self.irc.state.capabilities_ack = original_caps msg = ircmsgs.privmsg( '#foo', ' test message', prefix='foo!bar@baz' ) msg.server_tags['label'] = out_msg.server_tags['label'] self.irc.feedMsg(msg) if relayed: content = ' test message\n' else: content = ' test message\n' self.assertRegex(log_file.getvalue(), timestamp_re + content) def testLogRewriteRelayedRealEcho(self): self._testLogRewriteRelayedRealEcho(relayed=True) def testLogRewriteRelayedRealEchoNotRelayed(self): self._testLogRewriteRelayedRealEcho(relayed=False) @patch_open def testLogNotice(self, mock_open): log_file = io.StringIO() mock_open.return_value = log_file self.irc.feedMsg( ircmsgs.notice('#foo', 'test message', prefix='foo!bar@baz') ) self.assertRegex( log_file.getvalue(), timestamp_re + '-foo- test message\n' ) @patch_open def testLogJoinQuit(self, mock_open): log_file = io.StringIO() mock_open.return_value = log_file self.irc.feedMsg( ircmsgs.join('#foo', prefix='foo!bar@baz') ) self.irc.feedMsg( ircmsgs.quit('bye', prefix='foo!bar@baz') ) self.assertRegex( log_file.getvalue(), timestamp_re + r'\*\*\* foo has joined #foo\n' + timestamp_re + r'\*\*\* foo has quit IRC \(bye\)\n' ) @patch_open def testNoLogJoinQuit(self, mock_open): log_file = io.StringIO() mock_open.return_value = log_file with conf.supybot.plugins.ChannelLogger.showJoinParts.context(False): self.irc.feedMsg( ircmsgs.join('#foo', prefix='foo!bar@baz') ) self.irc.feedMsg( ircmsgs.quit('bye', prefix='foo!bar@baz') ) self.assertRegex( log_file.getvalue(), '^$' ) @patch_open def testLogAway(self, mock_open): log_file = io.StringIO() mock_open.return_value = log_file self.irc.feedMsg( ircmsgs.join('#foo', prefix='foo!bar@baz') ) self.irc.feedMsg( ircmsgs.IrcMsg(command='AWAY', args=('be right back',), prefix='foo!bar@baz') ) self.irc.feedMsg( ircmsgs.IrcMsg(command='AWAY', args=(), prefix='foo!bar@baz') ) self.assertRegex( log_file.getvalue(), timestamp_re + r'\*\*\* foo has joined #foo\n' + timestamp_re + r'\*\*\* foo is now away: be right back\n' + timestamp_re + r'\*\*\* foo is back\n' ) @patch_open def testNoLogAway(self, mock_open): log_file = io.StringIO() mock_open.return_value = log_file self.irc.feedMsg( ircmsgs.join('#foo', prefix='foo!bar@baz') ) with conf.supybot.plugins.ChannelLogger.showAway.context(False): self.irc.feedMsg( ircmsgs.IrcMsg(command='AWAY', args=('be right back',), prefix='foo!bar@baz') ) self.irc.feedMsg( ircmsgs.IrcMsg(command='AWAY', args=(), prefix='foo!bar@baz') ) self.assertRegex( log_file.getvalue(), timestamp_re + r'\*\*\* foo has joined #foo\n$' ) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/ChannelStats/0000755000175000017500000000000014535072535016172 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/__init__.py0000644000175000017500000000517214535072470020306 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Silently listens to every message received on a channel and keeps statistics concerning joins, parts, and various other commands in addition to tracking statistics about smileys, actions, characters, and words. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/config.py0000644000175000017500000000657114535072470020020 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('ChannelStats') 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('ChannelStats', True) class Smileys(registry.Value): def set(self, s): L = s.split() self.setValue(L) def setValue(self, v): self._lastModified = registry.monotonic_time() self.s = ' '.join(v) self.value = re.compile('|'.join(map(re.escape, v))) def __str__(self): return self.s ChannelStats = conf.registerPlugin('ChannelStats') conf.registerChannelValue(ChannelStats, 'selfStats', registry.Boolean(True, _("""Determines whether the bot will keep channel statistics on itself, possibly skewing the channel stats (especially in cases where the bot is relaying between channels on a network)."""))) conf.registerChannelValue(ChannelStats, 'smileys', Smileys(':) ;) ;] :-) :-D :D :P :p (= =)'.split(), _("""Determines what words (i.e., pieces of text with no spaces in them) are considered 'smileys' for the purposes of stats-keeping."""))) conf.registerChannelValue(ChannelStats, 'frowns', Smileys(':| :-/ :-\\ :\\ :/ :( :-( :\'('.split(), _("""Determines what words (i.e., pieces of text with no spaces in them) are considered 'frowns' for the purposes of stats-keeping."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/ChannelStats/locales/0000755000175000017500000000000014535072535017614 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/locales/fi.po0000644000175000017500000001443214535072470020554 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot ChannelStats\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.6.10\n" #: config.py:62 msgid "" "Determines whether the bot will keep channel\n" " statistics on itself, possibly skewing the channel stats (especially in\n" " cases where the bot is relaying between channels on a network)." msgstr "" "Määrittää pitääkö botti kanava\n" "tilastot itsellään, mahdollisesti vinouttaen kanava tilastot (etenkin\n" "tilanteissa, joissa botti on välittämässä kanavien välillä verkossa)." #: config.py:66 msgid "" "Determines what\n" " words (i.e., pieces of text with no spaces in them) are considered\n" " 'smileys' for the purposes of stats-keeping." msgstr "" "Määrittää mitkä\n" "sanat (esim. tekstin paloja ilman välilyöntiä niiden välissä ) lasketaan\n" " 'hymiöiksi' tilastojen pitämisen takia." #: config.py:70 #, fuzzy msgid "" "Determines what words\n" " (i.e., pieces of text with no spaces in them) are considered 'frowns' " "for\n" " the purposes of stats-keeping." msgstr "" "Määrittää mitkä sanat\n" " (esim. paloja tekstiä ilman välilyöntiä välissä ) lasketaan 'surioiksi'\n" "tilastojen pitämistä varten." #: plugin.py:169 msgid "" "This plugin keeps stats of the channel and returns them with\n" " the command 'channelstats'." msgstr "" "Tämä plugini säilyttää tilastoja kanavasta ja palauttaa ne komennolla\n" " 'channelstats'." #: plugin.py:240 msgid "" "[] []\n" "\n" " Returns the statistics for on . is only\n" " necessary if the message isn't sent on the channel itself. If " "\n" " isn't given, it defaults to the user sending the command.\n" " " msgstr "" "[] []\n" "\n" " . on vaadittu\n" "jos viestiä ei lähetetä kanavalla itsessään. Jos \n" " ei ole annettu, se on oletuksena viestin lähettänyt henkilö.\n" " " #: plugin.py:258 msgid "I couldn't find you in my user database." msgstr "Minä en voi löytää sinua käyttäjä tietokannastani." #: plugin.py:271 #, fuzzy msgid "" "%s has sent %n; a total of %n, %n, %n, and %n; %s of those messages %s. %s " "has joined %n, parted %n, quit %n, kicked someone %n, been kicked %n, " "changed the topic %n, and changed the mode %n." msgstr "" "%s on lähettänyt %n; yhteensä %n, %n, %n, ja %n; %s noista viesteistä %s. %s " "on liittynyt %n, poistunut kanavalta %n, sulkenut %n, potkinut jonkun %n, " "ollut potkittu %n, vaihtanut aihetta %n, vaihtanut tilaa %n." #: plugin.py:278 msgid "character" msgstr "merkki" #: plugin.py:279 plugin.py:368 msgid "word" msgstr "sana" #: plugin.py:280 plugin.py:369 msgid "smiley" msgstr "hymiö" #: plugin.py:281 plugin.py:370 msgid "frown" msgstr "surio" #: plugin.py:283 plugin.py:371 msgid "was an ACTION" msgstr "oli TOIMINTO" #: plugin.py:284 plugin.py:372 msgid "were ACTIONs" msgstr "olivat TOIMINTOja" #: plugin.py:286 plugin.py:287 plugin.py:288 plugin.py:289 plugin.py:290 #: plugin.py:291 plugin.py:292 msgid "time" msgstr "aika" #: plugin.py:295 msgid "I have no stats for that %s in %s." msgstr "Minulla ei ole tilastoja %s:stä %s:ään." #: plugin.py:301 msgid "" "[] \n" "\n" " Returns the ranking of users according to the given stat " "expression.\n" " Valid variables in the stat expression include 'msgs', 'chars',\n" " 'words', 'smileys', 'frowns', 'actions', 'joins', 'parts', 'quits',\n" " 'kicks', 'kicked', 'topics', and 'modes'. Any simple mathematical\n" " expression involving those variables is permitted.\n" " " msgstr "" "[] \n" "\n" "Palauttaa käyttäjien arvoasteikon perustuen annettuun tilasto " "lausekkeeseen.\n" "kelvolliset tilasto lausekkeet sisältävät 'msgs', 'chars',\n" "'words', 'smileys', 'frowns', 'actions', 'joins', 'parts', 'quits',\n" "'kicks', 'kicked', 'topics', and 'modes'. Mikä tahansa yksinkertainen " "matemaattinen\n" " lauseke sisältäen nämä arvot on sallittu.\n" " " #: plugin.py:327 msgid "stat variable" msgstr "aloita muuttuja" #: plugin.py:329 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:347 msgid "" "[]\n" "\n" " Returns the statistics for . is only necessary " "if\n" " the message isn't sent on the channel itself.\n" " " msgstr "" "[]\n" "\n" "Palauttaa tilastot . on vaadittu vain jos viestiä ei " "lähetetä\n" "kanavalla itsessään.\n" " " #: plugin.py:353 msgid "I am not in %s." msgstr "En ole kanavalla %s." #: plugin.py:355 msgid "You must be in %s to use this command." msgstr "Sinun on oltava kanavalla %s käyttääksesi tätä komentoa." #: plugin.py:362 #, fuzzy msgid "" "On %s there %h been %i messages, containing %i characters, %n, %n, and %n; " "%i of those messages %s. There have been %n, %n, %n, %n, %n, and %n. There " "%b currently %n and the channel has peaked at %n." msgstr "" "%s:ssä %h on ollut %i viestiä, sisältäen %i merkkiä, %n, %n, ja %n; %i " "noista viesteistä %s. On ollut %n, %n, %n, %n, %n, ja %n. %b tällä " "hetkellä %n kanavalla on huipulla %n." #: plugin.py:373 msgid "join" msgstr "liity" #: plugin.py:374 msgid "part" msgstr "poistu" #: plugin.py:375 msgid "quit" msgstr "poistu" #: plugin.py:376 msgid "kick" msgstr "potki" #: plugin.py:377 msgid "mode" msgstr "tila" #: plugin.py:377 plugin.py:378 msgid "change" msgstr "vaihdos" #: plugin.py:378 msgid "topic" msgstr "aihe" #: plugin.py:380 plugin.py:381 msgid "user" msgstr "käyttäjä" #: plugin.py:384 msgid "I've never been on %s." msgstr "En ole ikinä ollut %s:llä." #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Ei ole mitään syytä miksi haluaisit alaviivoja tai sulkuja " #~ "matemaattisessa lausekkeessa. Ole hyvä ja poista ne." #, fuzzy #~ msgid "You can't use lambda in this command." #~ msgstr "Et voi käyttää lambdaa tässä komennossa." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/locales/fr.po0000644000175000017500000001476014535072470020571 0ustar00valval# French translations for supybot-i18n package # Traductions françaises du paquet supybot-i18n. # Copyright (C) 2010 THE supybot-i18n'S COPYRIGHT HOLDER # This file is distributed under the same license as the supybot-i package. # ProgVal , 2010. # msgid "" msgstr "" "Project-Id-Version: supybot-i18n\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-01-22 13:47+0100\n" "Last-Translator: \n" "Language-Team: French\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 1.5.4\n" #: config.py:62 msgid "" "Determines whether the bot will keep channel\n" " statistics on itself, possibly skewing the channel stats (especially in\n" " cases where the bot is relaying between channels on a network)." msgstr "" "Détermine si le bot se prendre en compte dans les statistiques du canal, ce " "qui peut les fausser 'particulièrement dans le cas où le bot relaye entre " "des canaux)" #: config.py:66 msgid "" "Determines what\n" " words (i.e., pieces of text with no spaces in them) are considered\n" " 'smileys' for the purposes of stats-keeping." msgstr "" "Détermine quels mots (c'est à dire, les morceaux de texte sans espace) sont " "considérés comme smileys par les stats." #: config.py:70 #, fuzzy msgid "" "Determines what words\n" " (i.e., pieces of text with no spaces in them) are considered 'frowns' " "for\n" " the purposes of stats-keeping." msgstr "" "Détermine quels mots (c'est à dire, les morceaux de texte sans espace) sont " "considérés comme sadleys par les stats." #: plugin.py:169 msgid "" "This plugin keeps stats of the channel and returns them with\n" " the command 'channelstats'." msgstr "" #: plugin.py:240 msgid "" "[] []\n" "\n" " Returns the statistics for on . is only\n" " necessary if the message isn't sent on the channel itself. If " "\n" " isn't given, it defaults to the user sending the command.\n" " " msgstr "" "[] [nom]\n" "\n" " Retourne les statistiques pour sur le . n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même. Si " " n'est pas donné, il correspond par défaut à l'utilisateur envoyant la " "commande" #: plugin.py:258 msgid "I couldn't find you in my user database." msgstr "Je ne peux vous trouver dans ma base de données." #: plugin.py:271 msgid "" "%s has sent %n; a total of %n, %n, %n, and %n; %s of those messages %s. %s " "has joined %n, parted %n, quit %n, kicked someone %n, been kicked %n, " "changed the topic %n, and changed the mode %n." msgstr "" "%s a envoyé %n ; un total de %n, %n, %n, et %n ; %s de ces messages %s. %s " "est arrivé %n fois, est parti %n fois, a quitté %n fois, a kické %n fois, a " "été kické %n fois, a changé le topic %n fois, et a changé les modes %n fois." #: plugin.py:278 msgid "character" msgstr "caractère" #: plugin.py:279 plugin.py:368 msgid "word" msgstr "mot" #: plugin.py:280 plugin.py:369 msgid "smiley" msgstr "smiley" #: plugin.py:281 plugin.py:370 msgid "frown" msgstr "sadley" #: plugin.py:283 plugin.py:371 msgid "was an ACTION" msgstr "était une action" #: plugin.py:284 plugin.py:372 msgid "were ACTIONs" msgstr "étaient des ACTIONs" #: plugin.py:286 plugin.py:287 plugin.py:288 plugin.py:289 plugin.py:290 #: plugin.py:291 plugin.py:292 msgid "time" msgstr "" #: plugin.py:295 msgid "I have no stats for that %s in %s." msgstr "Je n'ai pas de statistiques pour %s sur %s." #: plugin.py:301 msgid "" "[] \n" "\n" " Returns the ranking of users according to the given stat " "expression.\n" " Valid variables in the stat expression include 'msgs', 'chars',\n" " 'words', 'smileys', 'frowns', 'actions', 'joins', 'parts', 'quits',\n" " 'kicks', 'kicked', 'topics', and 'modes'. Any simple mathematical\n" " expression involving those variables is permitted.\n" " " msgstr "" "[] \n" "\n" "Retourne le rang des utilisateurs en fonction de l'expression de " "statistiques données. Les variables valides dans l'expression de " "statistiques sont : 'msgs', 'chars', 'words', 'smileys', 'frowns', " "'actions', 'joins', 'parts', 'quits', 'kicks', 'kicked', 'topics', et " "'modes'. Toute expression mathématiques simple utilisant ces variables est " "autorisée." #: plugin.py:327 msgid "stat variable" msgstr "Variable statistique" #: plugin.py:329 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:347 msgid "" "[]\n" "\n" " Returns the statistics for . is only necessary " "if\n" " the message isn't sent on the channel itself.\n" " " msgstr "" "[canal]\n" "\n" "Retourne les statistiques pour le . n'est nécessaire que si " "le message n'est pas envoyé sur le canal lui-même." #: plugin.py:353 msgid "I am not in %s." msgstr "Je ne suis pas dans %s." #: plugin.py:355 msgid "You must be in %s to use this command." msgstr "Vous devez être dans %s pour utiliser cette commande." #: plugin.py:362 msgid "" "On %s there %h been %i messages, containing %i characters, %n, %n, and %n; " "%i of those messages %s. There have been %n, %n, %n, %n, %n, and %n. There " "%b currently %n and the channel has peaked at %n." msgstr "" "Sur %s il y a eu%v %i messages, contenant %i caractères, %n, %n, et %n ; %i " "de ces messages %s. Il y a eu %n, %n, %n, %n, %n, et %n. Il y a%v " "actuellement %n et le record du canal est de %n." #: plugin.py:373 msgid "join" msgstr "arrivée" #: plugin.py:374 msgid "part" msgstr "départ" #: plugin.py:375 msgid "quit" msgstr "quit" #: plugin.py:376 msgid "kick" msgstr "kick" #: plugin.py:377 msgid "mode" msgstr "mode" #: plugin.py:377 plugin.py:378 msgid "change" msgstr "changement" #: plugin.py:378 msgid "topic" msgstr "topic" #: plugin.py:380 plugin.py:381 msgid "user" msgstr "utilisateur" #: plugin.py:384 msgid "I've never been on %s." msgstr "Je n'ai jamais été sur %s." #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Il n'y a aucune raison pour que vous ayez des underscores ou des crochets " #~ "dans vos expressions mathématiques. Veuillez les supprimer." #~ msgid "You can't use lambda in this command." #~ msgstr "Vous ne pouvez utiliser lambda dans cette commande." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/locales/it.po0000644000175000017500000001414214535072470020570 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-05 13:41+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:62 msgid "" "Determines whether the bot will keep channel\n" " statistics on itself, possibly skewing the channel stats (especially in\n" " cases where the bot is relaying between channels on a network)." msgstr "" "Determina se il bot terrà statistiche del canale, probabilmente alterandole\n" " (in particolare in caso faccia l'inoltro dei messaggi attraverso i " "canali)." #: config.py:66 msgid "" "Determines what\n" " words (i.e., pieces of text with no spaces in them) are considered\n" " 'smileys' for the purposes of stats-keeping." msgstr "" "Determina quali parole (ovvero parti di testo senza spazi) sono considerate\n" " faccine sorridenti per le statistiche." #: config.py:70 #, fuzzy msgid "" "Determines what words\n" " (i.e., pieces of text with no spaces in them) are considered 'frowns' " "for\n" " the purposes of stats-keeping." msgstr "" "Determina quali parole (ovvero parti di testo senza spazi) sono considerate\n" " faccine tristi per le statistiche." #: plugin.py:169 msgid "" "This plugin keeps stats of the channel and returns them with\n" " the command 'channelstats'." msgstr "" #: plugin.py:240 msgid "" "[] []\n" "\n" " Returns the statistics for on . is only\n" " necessary if the message isn't sent on the channel itself. If " "\n" " isn't given, it defaults to the user sending the command.\n" " " msgstr "" "[] []\n" "\n" " Riporta le statistiche per su . è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso. Se \n" " non è specificato, passa all'utente che ha dato il comando.\n" " " #: plugin.py:258 msgid "I couldn't find you in my user database." msgstr "Non ti trovo nel mio database utenti." #: plugin.py:271 msgid "" "%s has sent %n; a total of %n, %n, %n, and %n; %s of those messages %s. %s " "has joined %n, parted %n, quit %n, kicked someone %n, been kicked %n, " "changed the topic %n, and changed the mode %n." msgstr "" "%s ha inviato %n; un totale di %n, %n, %n, e %n; %s di quei messaggi %s. %s " "è entrato %n volte, è uscito %n volte, si è disconnesso %n volte, ha espulso " "qualcuno %n volte, è stato espulso %n volte, ha modificato il topic %n volte " "ed ha cambiato il mode %n volte." #: plugin.py:278 msgid "character" msgstr "carattere" #: plugin.py:279 plugin.py:368 msgid "word" msgstr "parola" #: plugin.py:280 plugin.py:369 msgid "smiley" msgstr "faccina sorridente" #: plugin.py:281 plugin.py:370 msgid "frown" msgstr "faccina triste" #: plugin.py:283 plugin.py:371 msgid "was an ACTION" msgstr "è stata un'azione (ACTION)" #: plugin.py:284 plugin.py:372 msgid "were ACTIONs" msgstr "sono state azioni (ACTION)" #: plugin.py:286 plugin.py:287 plugin.py:288 plugin.py:289 plugin.py:290 #: plugin.py:291 plugin.py:292 msgid "time" msgstr "volta" #: plugin.py:295 msgid "I have no stats for that %s in %s." msgstr "Non ho statistiche per %s in %s." #: plugin.py:301 msgid "" "[] \n" "\n" " Returns the ranking of users according to the given stat " "expression.\n" " Valid variables in the stat expression include 'msgs', 'chars',\n" " 'words', 'smileys', 'frowns', 'actions', 'joins', 'parts', 'quits',\n" " 'kicks', 'kicked', 'topics', and 'modes'. Any simple mathematical\n" " expression involving those variables is permitted.\n" " " msgstr "" "[] \n" "\n" " Riporta la classifica degli utenti in base all'espresisone fornita.\n" " Le variabili valide sono: \"msgs\", \"chars\", \"words\", \"smileys" "\",\n" " \"frowns\", \"actions\", \"joins\", \"parts\", \"quits\", \"kicks" "\",\n" " \"kicked\", \"topics\" e \"modes\". È permessa qualsiasi " "espressione\n" " matematica che utilizzi queste variabili.\n" " " #: plugin.py:327 msgid "stat variable" msgstr "variabile di statistica" #: plugin.py:329 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:347 msgid "" "[]\n" "\n" " Returns the statistics for . is only necessary " "if\n" " the message isn't sent on the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta le statistiche di . è necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:353 msgid "I am not in %s." msgstr "" #: plugin.py:355 #, fuzzy msgid "You must be in %s to use this command." msgstr "Non è possibile usare lambda in questo comando." #: plugin.py:362 msgid "" "On %s there %h been %i messages, containing %i characters, %n, %n, and %n; " "%i of those messages %s. There have been %n, %n, %n, %n, %n, and %n. There " "%b currently %n and the channel has peaked at %n." msgstr "" "In %s ci sono%v stati %i messaggi, contenenti %i caratteri, %n, %n, e %n; %i " "di questi messaggi %s. Ci sono stati %n, %n, %n, %n, %n, e %n. Attualmente " "ci sono%v %n e il canale ha raggiunto il picco di %n." #: plugin.py:373 msgid "join" msgstr "join" #: plugin.py:374 msgid "part" msgstr "part" #: plugin.py:375 msgid "quit" msgstr "quit" #: plugin.py:376 msgid "kick" msgstr "kick" #: plugin.py:377 msgid "mode" msgstr "mode" #: plugin.py:377 plugin.py:378 msgid "change" msgstr "modifiche" #: plugin.py:378 msgid "topic" msgstr "topic" #: plugin.py:380 plugin.py:381 msgid "user" msgstr "utente" #: plugin.py:384 msgid "I've never been on %s." msgstr "Non sono mai stato su %s." #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Non v'è alcuna ragione di usare underscore o parentesi nelle espressioni " #~ "matematiche; ti invito a rimuoverli." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/plugin.py0000644000175000017500000003603714535072470020051 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import math import types import supybot.log as log import supybot.conf as conf from supybot.i18n import PluginInternationalization, internationalizeDocstring import supybot.utils as utils import supybot.world as world import supybot.ircdb as ircdb from supybot.commands import * import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.utils.math_evaluator import safe_eval, InvalidNode _ = PluginInternationalization('ChannelStats') class ChannelStat(irclib.IrcCommandDispatcher): _values = ['actions', 'chars', 'frowns', 'joins', 'kicks','modes', 'msgs', 'parts', 'quits', 'smileys', 'topics', 'words', 'users'] def __init__(self, actions=0, chars=0, frowns=0, joins=0, kicks=0, modes=0, msgs=0, parts=0, quits=0, smileys=0, topics=0, words=0, users=0): self.actions = actions self.chars = chars self.frowns = frowns self.joins = joins self.kicks = kicks self.modes = modes self.msgs = msgs self.parts = parts self.quits = quits self.smileys = smileys self.topics = topics self.words = words self.users = users def values(self): return [getattr(self, s) for s in self._values] def addMsg(self, msg): self.msgs += 1 method = self.dispatchCommand(msg.command, msg.args) if method is not None: method(msg) def doPayload(self, channel, payload): channel = plugins.getChannel(channel) self.chars += len(payload) self.words += len(payload.split()) fRe = conf.supybot.plugins.ChannelStats.get('frowns').get(channel)() sRe =conf.supybot.plugins.ChannelStats.get('smileys').get(channel)() self.frowns += len(fRe.findall(payload)) self.smileys += len(sRe.findall(payload)) def doPrivmsg(self, msg): isAction = ircmsgs.isAction(msg) if ircmsgs.isCtcp(msg) and not isAction: return self.doPayload(*msg.args) if isAction: self.actions += 1 def doTopic(self, msg): self.doPayload(*msg.args) self.topics += 1 def doKick(self, msg): self.kicks += 1 def doPart(self, msg): if len(msg.args) == 2: self.doPayload(*msg.args) self.parts += 1 def doJoin(self, msg): if len(msg.args) == 2: self.doPayload(*msg.args) self.joins += 1 # Handle max-users in the plugin since we need an irc object def doMode(self, msg): self.modes += 1 # doQuit is handled by the plugin. class UserStat(ChannelStat): _values = ['kicked'] + ChannelStat._values def __init__(self, kicked=0, *args): ChannelStat.__init__(self, *args) self.kicked = kicked def doKick(self, msg): self.doPayload(msg.args[0], msg.args[2]) self.kicks += 1 class StatsDB(plugins.ChannelUserDB): def __init__(self, *args, **kwargs): plugins.ChannelUserDB.__init__(self, *args, **kwargs) def serialize(self, v): return v.values() def deserialize(self, channel, id, L): L = list(map(int, L)) if id == 'channelStats': return ChannelStat(*L) else: return UserStat(*L) def addMsg(self, irc, msg, id=None): if msg.channel: channel = plugins.getChannel(msg.channel) if (channel, 'channelStats') not in self: self[channel, 'channelStats'] = ChannelStat() self[channel, 'channelStats'].addMsg(msg) try: if id is None: id = ircdb.users.getUserId(msg.prefix) except KeyError: return if (channel, id) not in self: self[channel, id] = UserStat() self[channel, id].addMsg(msg) def getChannelStats(self, channel): return self[channel, 'channelStats'] def getUserStats(self, channel, id): return self[channel, id] filename = conf.supybot.directories.data.dirize('ChannelStats.db') class ChannelStats(callbacks.Plugin): """This plugin keeps stats of the channel and returns them with the command 'channelstats'.""" noIgnore = True def __init__(self, irc): self.__parent = super(ChannelStats, self) self.__parent.__init__(irc) self.outFiltering = False self.db = StatsDB(filename) self._flush = self.db.flush world.flushers.append(self._flush) def die(self): world.flushers.remove(self._flush) self.db.close() self.__parent.die() def __call__(self, irc, msg): self.db.addMsg(irc, msg) super(ChannelStats, self).__call__(irc, msg) def outFilter(self, irc, msg): if msg.command == 'PRIVMSG': if msg.channel: if self.registryValue('selfStats', msg.channel, irc.network): try: self.outFiltering = True self.db.addMsg(irc, msg, 0) finally: self.outFiltering = False return msg def _setUsers(self, irc, channel): if (channel, 'channelStats') not in self.db: self.db[channel, 'channelStats'] = ChannelStat() oldUsers = self.db[channel, 'channelStats'].users newUsers = len(irc.state.channels[channel].users) self.db[channel, 'channelStats'].users = max(oldUsers, newUsers) def doJoin(self, irc, msg): self._setUsers(irc, msg.args[0]) def do366(self, irc, msg): self._setUsers(irc, msg.args[1]) def doQuit(self, irc, msg): try: id = ircdb.users.getUserId(msg.prefix) except KeyError: id = None for channel in msg.tagged('channels'): if (channel, 'channelStats') not in self.db: self.db[channel, 'channelStats'] = ChannelStat() self.db[channel, 'channelStats'].quits += 1 if id is not None: if (channel, id) not in self.db: self.db[channel, id] = UserStat() self.db[channel, id].quits += 1 def doKick(self, irc, msg): (channel, nick, _) = msg.args hostmask = irc.state.nickToHostmask(nick) try: id = ircdb.users.getUserId(hostmask) except KeyError: return if (channel, id) not in self.db: self.db[channel, id] = UserStat() self.db.channels[channel][id].kicked += 1 @internationalizeDocstring def stats(self, irc, msg, args, channel, name): """[] [] Returns the statistics for on . is only necessary if the message isn't sent on the channel itself. If isn't given, it defaults to the user sending the command. """ if channel != '#': # Skip this check if databases.plugins.channelspecific is False. if msg.nick not in irc.state.channels[channel].users: irc.error(format('You must be in %s to use this command.', channel)) return if name and ircutils.strEqual(name, irc.nick): id = 0 elif not name: try: id = ircdb.users.getUserId(msg.prefix) name = ircdb.users.getUser(id).name except KeyError: irc.error(_('I couldn\'t find you in my user database.')) return elif not ircdb.users.hasUser(name): try: hostmask = irc.state.nickToHostmask(name) id = ircdb.users.getUserId(hostmask) except KeyError: irc.errorNoUser() return else: id = ircdb.users.getUserId(name) try: stats = self.db.getUserStats(channel, id) s = format(_('%s has sent %n; a total of %n, %n, ' '%n, and %n; %s of those messages %s. ' '%s has joined %n, parted %n, quit %n, ' 'kicked someone %n, been kicked %n, ' 'changed the topic %n, and changed the ' 'mode %n.'), name, (stats.msgs, 'message'), (stats.chars, _('character')), (stats.words, _('word')), (stats.smileys, _('smiley')), (stats.frowns, _('frown')), stats.actions, stats.actions == 1 and _('was an ACTION') or _('were ACTIONs'), name, (stats.joins, _('time')), (stats.parts, _('time')), (stats.quits, _('time')), (stats.kicks, _('time')), (stats.kicked, _('time')), (stats.topics, _('time')), (stats.modes, _('time'))) irc.reply(s) except KeyError: irc.error(format(_('I have no stats for that %s in %s.'), name, channel)) stats = wrap(stats, ['channeldb', additional('something')]) @internationalizeDocstring def rank(self, irc, msg, args, channel, expr): """[] Returns the ranking of users according to the given stat expression. Valid variables in the stat expression include 'msgs', 'chars', 'words', 'smileys', 'frowns', 'actions', 'joins', 'parts', 'quits', 'kicks', 'kicked', 'topics', and 'modes'. Any simple mathematical expression involving those variables is permitted. """ if channel != '#': # Skip this check if databases.plugins.channelspecific is False. if msg.nick not in irc.state.channels[channel].users: irc.error(format('You must be in %s to use this command.', channel)) return expr = expr.lower() users = [] for ((c, id), stats) in self.db.items(): if ircutils.strEqual(c, channel) and \ (id == 0 or ircdb.users.hasUser(id)): e = {} for attr in stats._values: e[attr] = float(getattr(stats, attr)) try: v = safe_eval(expr, allow_ints=True, variables=e) except ZeroDivisionError: v = float('inf') except NameError as e: irc.errorInvalid(_('stat variable'), str(e)) except InvalidNode as e: irc.error(_('Invalid syntax: %s') % e.args[0], Raise=True) except Exception as e: irc.error(utils.exnToString(e), Raise=True) else: v = float(v) if id == 0: users.append((v, irc.nick)) else: users.append((v, ircdb.users.getUser(id).name)) users.sort() users.reverse() s = utils.str.commaAndify(['#%s %s (%.3g)' % (i+1, u, v) for (i, (v, u)) in enumerate(users)]) irc.reply(s) rank = wrap(rank, ['channeldb', 'text']) @internationalizeDocstring def channelstats(self, irc, msg, args, channel): """[] Returns the statistics for . is only necessary if the message isn't sent on the channel itself. """ if channel not in irc.state.channels: irc.error(_('I am not in %s.') % channel, Raise=True) elif msg.nick not in irc.state.channels[channel].users: irc.error(_('You must be in %s to use this command.') % channel, Raise=True) try: channeldb = conf.supybot.databases.plugins.channelSpecific. \ getChannelLink(channel) stats = self.db.getChannelStats(channeldb) curUsers = len(irc.state.channels[channel].users) s = format(_('On %s there %h been %i messages, containing %i ' 'characters, %n, %n, and %n; ' '%i of those messages %s. There have been ' '%n, %n, %n, %n, %n, and %n. There %b currently %n ' 'and the channel has peaked at %n.'), channel, stats.msgs, stats.msgs, stats.chars, (stats.words, _('word')), (stats.smileys, _('smiley')), (stats.frowns, _('frown')), stats.actions, stats.actions == 1 and _('was an ACTION') or _('were ACTIONs'), (stats.joins, _('join')), (stats.parts, _('part')), (stats.quits, _('quit')), (stats.kicks, _('kick')), (stats.modes, _('mode'), _('change')), (stats.topics, _('topic'), _('change')), curUsers, (curUsers, _('user')), (stats.users, _('user'))) irc.reply(s) except KeyError: irc.error(format(_('I\'ve never been on %s.'), channel)) channelstats = wrap(channelstats, ['channel']) Class = ChannelStats # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ChannelStats/test.py0000644000175000017500000001140614535072470017523 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, 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 * import supybot.ircdb as ircdb class ChannelStatsTestCase(ChannelPluginTestCase): plugins = ('ChannelStats', 'User') def setUp(self): ChannelPluginTestCase.setUp(self) self.prefix = 'foo!bar@baz' self.nick = 'foo' self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar', prefix=self.prefix)) _ = self.irc.takeMsg() chanop = ircdb.makeChannelCapability(self.channel, 'op') ircdb.users.getUser(self.nick).addCapability(chanop) def test(self): self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) self.assertNotError('channelstats') self.assertNotError('channelstats') self.assertNotError('channelstats') def testStats(self): self.assertError('channelstats stats %s' % self.nick) self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) self.assertNotError('channelstats stats %s' % self.nick) self.assertNotError('channelstats stats %s' % self.nick.upper()) self.assertNotError('channelstats stats') self.assertRegexp('channelstats stats', self.nick) def testSelfStats(self): self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) self.assertError('channelstats stats %s' % self.irc.nick) self.assertNotError('channelstats stats %s' % self.irc.nick) self.assertNotError('channelstats stats %s' % self.irc.nick) self.assertNotError('channelstats stats %s' % self.irc.nick.upper()) self.assertRegexp('channelstats rank chars', self.irc.nick) u = ircdb.users.getUser(self.prefix) u.addCapability(ircdb.makeChannelCapability(self.channel, 'op')) ircdb.users.setUser(u) try: conf.supybot.plugins.ChannelStats.selfStats.setValue(False) m1 = self.getMsg('channelstats stats %s' % self.irc.nick) m2 = self.getMsg('channelstats stats %s' % self.irc.nick) self.assertEqual(m1.args[1], m2.args[1]) finally: conf.supybot.plugins.ChannelStats.selfStats.setValue(True) def testNoKeyErrorStats(self): self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) self.assertNotRegexp('stats sweede', 'KeyError') def testRank(self): self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) self.assertError('channelstats stats %s' % self.irc.nick) self.assertNotError('channelstats stats %s' % self.irc.nick) self.assertNotError('channelstats stats %s' % self.irc.nick) self.assertNotError('channelstats stats %s' % self.irc.nick.upper()) self.assertNotError('channelstats stats %s' % self.nick) self.assertNotError('channelstats stats %s' % self.nick.upper()) self.assertNotError('channelstats stats') self.assertNotError('channelstats rank chars / msgs') self.assertNotError('channelstats rank kicks/kicked') # Tests inf self.assertNotError('channelstats rank log(msgs)') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Conditional/0000755000175000017500000000000014535072535016046 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/__init__.py0000644000175000017500000000553314535072470020163 0ustar00valval### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, 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. ### """ Contains numerous conditional commands (such as 'if', 'and', and 'or'), which can be used on their own or with another plugin. Also provides logic operators for writing conditions. Useful for bot scripting / 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__ = "0.1" __author__ = supybot.Author('Daniel Folkinshteyn', 'nanotube', 'nanotube@users.sourceforge.net') __maintainer__ = supybot.authors.limnoria_core # 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/Conditional/download' from . import config from . import plugin from importlib 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 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/config.py0000644000175000017500000000531314535072470017665 0ustar00valval### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization from supybot.i18n import internationalizeDocstring _ = PluginInternationalization('Conditional') except: # This are useless functions that's allow to run the plugin on a bot # without the i18n plugin _ = lambda x:x internationalizeDocstring = 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('Conditional', True) Conditional = conf.registerPlugin('Conditional') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Conditional, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Conditional/locales/0000755000175000017500000000000014535072535017470 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/locales/fi.po0000644000175000017500000001774014535072470020435 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Conditional plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14:32+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:59 #, fuzzy msgid "" "This plugin provides logic operators and other commands that\n" " enable you to run commands only if a condition is true. Useful for " "nested\n" " commands and scripting." msgstr "" "Tämä plugini tarjoaa loogiset operaattorit ja muita komentoja, jotka " "sallivat\n" " sinun suorittaa komentoja vain ehdon ollessa true. Hyödyllinen " "sisäkkäisille\n" " komennoille ja skriptaukselle." #: plugin.py:66 msgid "Run a command from message, as if command was sent over IRC." msgstr "Suorita komento viestistä, kuin komento olisi lähetetty IRC:stä." #: plugin.py:76 msgid "" " \n" "\n" " Runs if evaluates to true, runs " "\n" " if it evaluates to false.\n" "\n" " Use other logical operators defined in this plugin and command " "nesting\n" " to your advantage here.\n" " " msgstr "" " \n" "\n" " Suorittaa if kehittyy todeksi, suorittaa " "\n" " jos se kehittyy epätodeksi.\n" "\n" " Käytä loogisia toimintoja, jotka on määritetty tässä laajennuksessa " "ja komentojen putkittamista\n" " eduksesi tässä.\n" " " #: plugin.py:92 msgid "" " [ ... ]\n" "\n" " Returns true if all conditions supplied evaluate to true.\n" " " msgstr "" " [ ... ]\n" " \n" "Palauttaa kaikki edellytykset jotka tukevat arviotumista todeksi.\n" " " #: plugin.py:104 msgid "" " [ ... ]\n" "\n" " Returns true if any one of conditions supplied evaluates to true.\n" " " msgstr "" " [ ... ]\n" " \n" " Palauttaa toden, jos yksikin edellytys tukee arvioimista todeksi.\n" " " #: plugin.py:116 msgid "" " [ ... ]\n" "\n" " Returns true if only one of conditions supplied evaluates to true.\n" " " msgstr "" " [ ... ]\n" " \n" "Palauttaa toden, jos vain yksi edellytys tukee arvioimista todeksi.\n" " " #: plugin.py:128 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if they are equal.\n" " " msgstr "" " \n" " \n" " Tekee ketju vertailun ja . \n" " Palauttaa toden jos ne ovat yhtäläisia.\n" " " #: plugin.py:141 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if they are not equal.\n" " " msgstr "" " \n" " \n" " Tekee ketju vertailun ja . \n" " Palauttaa toden, jos ne eivät ole yhtäläisia.\n" " " #: plugin.py:154 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is greater than .\n" " " msgstr "" " \n" " \n" " Tekee ketju vertailun ja . \n" " Palauttaa toden jos on suurempi kuin .\n" " " #: plugin.py:167 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is greater than or equal to .\n" " " msgstr "" " \n" " \n" " Tekee ketju vertailun ja . \n" " Palauttaa toden jos on suurempi tai yhtäläinen .\n" " " #: plugin.py:180 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is less than .\n" " " msgstr "" " \n" " \n" "Tekee ketju vertailun ja . \n" "Palauttaa toden jos on vähemmän kuin .\n" " " #: plugin.py:193 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is less than or equal to .\n" " " msgstr "" " \n" " \n" " Tekee ketju vertailun ja . \n" " Palauttaa toden jos on vähemmän tai yhtäläinen .\n" " " #: plugin.py:206 msgid "" "[--case-insensitive] \n" "\n" " Determines if is a substring of .\n" " Returns true if is contained in .\n" "\n" " Will only match case if --case-insensitive is not given.\n" " " msgstr "" "[--case-insensitive] \n" "\n" " Selvittää onko osa .\n" " Palauttaa true, jos sisältyy .\n" "\n" " Kirjainkoko merkitsee, mikäli --case-insensitive ei ole annettu.\n" " " #: plugin.py:226 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if they are equal.\n" " " msgstr "" " \n" " \n" "Tekee numerollisen vertailun ja välillä. \n" "Palauttaa toden, jos ne ovat yhtäläisiä.\n" " " #: plugin.py:239 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if they are not equal.\n" " " msgstr "" " \n" " \n" " Tekee numerollisen vertailun ja . \n" " Palauttaa toden, jos ne eivät ole yhtäläisiä.\n" " " #: plugin.py:252 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is greater than .\n" " " msgstr "" " \n" " \n" " Tekee numerollisen vertailun ja . \n" " Palauttaa toden jos on suurempi kuin .\n" " " #: plugin.py:265 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is greater than or equal to .\n" " " msgstr "" " \n" " \n" " Tekee numerollisen vertailun ja . \n" " Palauttaa toden, jos on suurempi tai yhtäläinen .\n" " " #: plugin.py:278 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is less than .\n" " " msgstr "" " \n" " \n" " Tekee numerollisen vertailun ja . \n" " Palauttaa toden jos on vähemmän kuin .\n" " " #: plugin.py:291 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is less than or equal to .\n" " " msgstr "" " \n" " \n" " Tekee numerollisen vertailun ja . \n" " Palauttaa toden jos on vähemmän tai yhtäläinen .\n" " " #: plugin.py:303 msgid "" "\n" "\n" " Runs and returns true if it raises an error;\n" " false otherwise.\n" " " msgstr "" #~ msgid "" #~ "Add the help for 'plugin help Conditional' here\n" #~ " This should describe *how* to use this plugin." #~ msgstr "" #~ "Lisää ohje 'plugin help Conditional' tähän.\n" #~ " Tämän pitäisi kuvata *kuinka* tätä lisäosaa käytetään." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/locales/fr.po0000644000175000017500000001601414535072470020437 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:59 msgid "" "This plugin provides logic operators and other commands that\n" " enable you to run commands only if a condition is true. Useful for " "nested\n" " commands and scripting." msgstr "" #: plugin.py:66 msgid "Run a command from message, as if command was sent over IRC." msgstr "." #: plugin.py:76 msgid "" " \n" "\n" " Runs if evaluates to true, runs " "\n" " if it evaluates to false.\n" "\n" " Use other logical operators defined in this plugin and command " "nesting\n" " to your advantage here.\n" " " msgstr "" " \n" "\n" "Exécute la si la est évaluée à true, lance la " " si elle est évaluée à false. Utilisez d'autres opérateurs " "logiques définis dans ce plugin et l'imbrication de commandes à votre " "avantage." #: plugin.py:92 msgid "" " [ ... ]\n" "\n" " Returns true if all conditions supplied evaluate to true.\n" " " msgstr "" " [ ... ]\n" "\n" "Retourne True si toutes les conditions sont évaluées à true." #: plugin.py:104 msgid "" " [ ... ]\n" "\n" " Returns true if any one of conditions supplied evaluates to true.\n" " " msgstr "" " [ ... ]\n" "\n" "Retourne True si une au moins des conditions est évaluée à true." #: plugin.py:116 msgid "" " [ ... ]\n" "\n" " Returns true if only one of conditions supplied evaluates to true.\n" " " msgstr "" " [ ... ]\n" "\n" "Retourne True si une seule des conditions est évaluée à true." #: plugin.py:128 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if they are equal.\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison de chaîne entre <élément1> et <élément2>. Retourne true " "si ils sont égaux." #: plugin.py:141 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if they are not equal.\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison de chaîne entre <élément1> et <élément2>. Retourne true " "si ils sont inégaux." #: plugin.py:154 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is greater than .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison de chaîne entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus grand que <élément2>" #: plugin.py:167 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is greater than or equal to .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison de chaîne entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus grand ou égal à <élément2>" #: plugin.py:180 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is less than .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison de chaîne entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus petit que <élément2>" #: plugin.py:193 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is less than or equal to .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison de chaîne entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus petit ou égal à <élément2>" #: plugin.py:206 msgid "" "[--case-insensitive] \n" "\n" " Determines if is a substring of .\n" " Returns true if is contained in .\n" "\n" " Will only match case if --case-insensitive is not given.\n" " " msgstr "" "[--case-insensitive] <élément1> <élément2>\n" "\n" "Détermine si <élément1> est une sous-chaîne de <élément2>. Retourne true si " "<élément1> est contenu dans <élément2>.Ne prendra la casse en compte que si " "--case-insensitive n’est pas donné." #: plugin.py:226 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if they are equal.\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison numérique entre <élément1> et <élément2>. Retourne true " "si ils sont égaux." #: plugin.py:239 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if they are not equal.\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison numérique entre <élément1> et <élément2>. Retourne true " "si ils sont inégaux." #: plugin.py:252 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is greater than .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison numérique entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus grand que <élément2>" #: plugin.py:265 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is greater than or equal to .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison numérique entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus grand ou égal à <élément2>" #: plugin.py:278 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is less than .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison numérique entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus petit que <élément2>" #: plugin.py:291 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is less than or equal to .\n" " " msgstr "" "<élément1> <élément2>\n" "\n" "Fait une comparaison numérique entre <élément1> et <élément2>. Retourne true " "si <élément1> est plus petit ou égal à <élément2>" #: plugin.py:303 msgid "" "\n" "\n" " Runs and returns true if it raises an error;\n" " false otherwise.\n" " " msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/locales/it.po0000644000175000017500000001650514535072470020451 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-08 09:05+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:59 msgid "" "This plugin provides logic operators and other commands that\n" " enable you to run commands only if a condition is true. Useful for " "nested\n" " commands and scripting." msgstr "" #: plugin.py:66 msgid "Run a command from message, as if command was sent over IRC." msgstr "" #: plugin.py:76 msgid "" " \n" "\n" " Runs if evaluates to true, runs " "\n" " if it evaluates to false.\n" "\n" " Use other logical operators defined in this plugin and command " "nesting\n" " to your advantage here.\n" " " msgstr "" " \n" "\n" " Esegue se analizzata è vera, esegue " " se falsa.\n" "\n" " Utilizza gli altri operatori logici presenti in questo plugin e i " "comandi nidificati a tuo vantaggio.\n" " " #: plugin.py:92 msgid "" " [ ... ]\n" "\n" " Returns true if all conditions supplied evaluate to true.\n" " " msgstr "" " [ ... ]\n" "\n" " Restituisce True se tutte le condizioni fornite sono vere.\n" " " #: plugin.py:104 msgid "" " [ ... ]\n" "\n" " Returns true if any one of conditions supplied evaluates to true.\n" " " msgstr "" " [ ... ]\n" "\n" " Restituisce True se almeno una delle condizioni fornite è vera.\n" " " #: plugin.py:116 msgid "" " [ ... ]\n" "\n" " Returns true if only one of conditions supplied evaluates to true.\n" " " msgstr "" " [ ... ]\n" "\n" " Restituisce True se solo una delle condizioni fornite è vera.\n" " " #: plugin.py:128 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if they are equal.\n" " " msgstr "" " \n" "\n" " Effettua una comparazione di stringhe tra ed " ".\n" " Restituisce True se sono uguali.\n" " " #: plugin.py:141 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if they are not equal.\n" " " msgstr "" " \n" "\n" " Effettua una comparazione di stringhe tra ed " ".\n" " Restituisce True se non sono uguali.\n" " " #: plugin.py:154 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is greater than .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione di stringhe tra ed " ".\n" " Restituisce True se è maggiore di .\n" " " #: plugin.py:167 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is greater than or equal to .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione di stringhe tra ed " ".\n" " Restituisce True se è maggiore o uguale a .\n" " " #: plugin.py:180 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is less than .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione di stringhe tra ed " ".\n" " Restituisce True se è minore di .\n" " " #: plugin.py:193 msgid "" " \n" "\n" " Does a string comparison on and .\n" " Returns true if is less than or equal to .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione di stringhe tra ed " ".\n" " Restituisce True se è minore o uguale a .\n" " " #: plugin.py:206 #, fuzzy msgid "" "[--case-insensitive] \n" "\n" " Determines if is a substring of .\n" " Returns true if is contained in .\n" "\n" " Will only match case if --case-insensitive is not given.\n" " " msgstr "" " \n" "\n" " Determina se è una sottostringa di .\n" " Restituisce True se è contenuto in .\n" " " #: plugin.py:226 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if they are equal.\n" " " msgstr "" " \n" "\n" " Effettua una comparazione numerica tra ed .\n" " Restituisce True se sono uguali.\n" " " #: plugin.py:239 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if they are not equal.\n" " " msgstr "" " \n" "\n" " Effettua una comparazione numerica tra ed .\n" " Restituisce True se non sono uguali.\n" " " #: plugin.py:252 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is greater than .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione numerica tra ed .\n" " Restituisce True se è maggiore di .\n" " " #: plugin.py:265 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is greater than or equal to .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione numerica tra ed .\n" " Restituisce True se è maggiore o uguale a .\n" " " #: plugin.py:278 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is less than .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione numerica tra ed .\n" " Restituisce True se è minore di .\n" " " #: plugin.py:291 msgid "" " \n" "\n" " Does a numeric comparison on and .\n" " Returns true if is less than or equal to .\n" " " msgstr "" " \n" "\n" " Effettua una comparazione numerica tra ed .\n" " Restituisce True se è minore o uguale a .\n" " " #: plugin.py:303 msgid "" "\n" "\n" " Runs and returns true if it raises an error;\n" " false otherwise.\n" " " msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/plugin.py0000644000175000017500000002611614535072470017722 0ustar00valval### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.utils as utils import supybot.plugins as plugins import supybot.commands as commands import supybot.ircutils as ircutils import supybot.callbacks as callbacks import re import collections try: from supybot.i18n import PluginInternationalization from supybot.i18n import internationalizeDocstring _ = PluginInternationalization('Conditional') except: # This are useless functions that's allow to run the plugin on a bot # without the i18n plugin _ = lambda x:x internationalizeDocstring = lambda x:x first = commands.first many = commands.many wrap = commands.wrap getopts = commands.getopts boolean_or_int = first('int', 'boolean') class Conditional(callbacks.Plugin): """This plugin provides logic operators and other commands that enable you to run commands only if a condition is true. Useful for nested commands and scripting.""" def __init__(self, irc): callbacks.Plugin.__init__(self, irc) def _runCommandFunction(self, irc, msg, command): """Run a command from message, as if command was sent over IRC.""" tokens = callbacks.tokenize(command, channel=msg.channel, network=irc.network) try: self.Proxy(irc.irc, msg, tokens) except Exception as e: self.log.exception('Uncaught exception in requested function:') @internationalizeDocstring def cif(self, irc, msg, args, condition, ifcommand, elsecommand): """ Runs if evaluates to true, runs if it evaluates to false. Use other logical operators defined in this plugin and command nesting to your advantage here. """ if condition: self._runCommandFunction(irc, msg, ifcommand) else: self._runCommandFunction(irc, msg, elsecommand) cif = wrap(cif, [boolean_or_int, 'something', 'something']) @internationalizeDocstring def cand(self, irc, msg, args, conds): """ [ ... ] Returns true if all conditions supplied evaluate to true. """ if all(conds): irc.reply("true") else: irc.reply("false") cand = wrap(cand, [many(boolean_or_int),]) @internationalizeDocstring def cor(self, irc, msg, args, conds): """ [ ... ] Returns true if any one of conditions supplied evaluates to true. """ if any(conds): irc.reply("true") else: irc.reply("false") cor = wrap(cor, [many(boolean_or_int),]) @internationalizeDocstring def cxor(self, irc, msg, args, conds): """ [ ... ] Returns true if only one of conditions supplied evaluates to true. """ if sum(conds) == 1: irc.reply("true") else: irc.reply("false") cxor = wrap(cxor, [many(boolean_or_int),]) @internationalizeDocstring def ceq(self, irc, msg, args, item1, item2): """ Does a string comparison on and . Returns true if they are equal. """ if item1 == item2: irc.reply('true') else: irc.reply('false') ceq = wrap(ceq, ['anything', 'anything']) @internationalizeDocstring def ne(self, irc, msg, args, item1, item2): """ Does a string comparison on and . Returns true if they are not equal. """ if item1 != item2: irc.reply('true') else: irc.reply('false') ne = wrap(ne, ['anything', 'anything']) @internationalizeDocstring def gt(self, irc, msg, args, item1, item2): """ Does a string comparison on and . Returns true if is greater than . """ if item1 > item2: irc.reply('true') else: irc.reply('false') gt = wrap(gt, ['anything', 'anything']) @internationalizeDocstring def ge(self, irc, msg, args, item1, item2): """ Does a string comparison on and . Returns true if is greater than or equal to . """ if item1 >= item2: irc.reply('true') else: irc.reply('false') ge = wrap(ge, ['anything', 'anything']) @internationalizeDocstring def lt(self, irc, msg, args, item1, item2): """ Does a string comparison on and . Returns true if is less than . """ if item1 < item2: irc.reply('true') else: irc.reply('false') lt = wrap(lt, ['anything', 'anything']) @internationalizeDocstring def le(self, irc, msg, args, item1, item2): """ Does a string comparison on and . Returns true if is less than or equal to . """ if item1 <= item2: irc.reply('true') else: irc.reply('false') le = wrap(le, ['anything', 'anything']) @internationalizeDocstring def match(self, irc, msg, args, optlist, item1, item2): """[--case-insensitive] Determines if is a substring of . Returns true if is contained in . Will only match case if --case-insensitive is not given. """ optlist = dict(optlist) if 'case-insensitive' in optlist: item1 = item1.lower() item2 = item2.lower() if item2.find(item1) != -1: irc.reply('true') else: irc.reply('false') match = wrap(match, [getopts({'case-insensitive': ''}), 'something', 'something']) @internationalizeDocstring def nceq(self, irc, msg, args, item1, item2): """ Does a numeric comparison on and . Returns true if they are equal. """ if item1 == item2: irc.reply('true') else: irc.reply('false') nceq = wrap(nceq, ['float', 'float']) @internationalizeDocstring def nne(self, irc, msg, args, item1, item2): """ Does a numeric comparison on and . Returns true if they are not equal. """ if item1 != item2: irc.reply('true') else: irc.reply('false') nne = wrap(nne, ['float', 'float']) @internationalizeDocstring def ngt(self, irc, msg, args, item1, item2): """ Does a numeric comparison on and . Returns true if is greater than . """ if item1 > item2: irc.reply('true') else: irc.reply('false') ngt = wrap(ngt, ['float', 'float']) @internationalizeDocstring def nge(self, irc, msg, args, item1, item2): """ Does a numeric comparison on and . Returns true if is greater than or equal to . """ if item1 >= item2: irc.reply('true') else: irc.reply('false') nge = wrap(nge, ['float', 'float']) @internationalizeDocstring def nlt(self, irc, msg, args, item1, item2): """ Does a numeric comparison on and . Returns true if is less than . """ if item1 < item2: irc.reply('true') else: irc.reply('false') nlt = wrap(nlt, ['float', 'float']) @internationalizeDocstring def nle(self, irc, msg, args, item1, item2): """ Does a numeric comparison on and . Returns true if is less than or equal to . """ if item1 <= item2: irc.reply('true') else: irc.reply('false') nle = wrap(nle, ['float', 'float']) def cerror(self, irc, msg, args, testcommand): """ Runs and returns true if it raises an error; false otherwise. """ tokens = callbacks.tokenize(testcommand, channel=msg.channel, network=irc.network) InvalidCommand = collections.namedtuple('InvalidCommand', 'command') replies = [] errors = [] class ErrorReportingProxy(self.Proxy): def reply(self2, s, *args, **kwargs): replies.append(s) def error(self2, s, Raise=False, *args, **kwargs): errors.append(s) if Raise: raise ArgumentError def _callInvalidCommands(self2): errors.append(InvalidCommand(self2.args)) def evalArgs(self2): # We don't want the replies in the nested command to # be stored here. super(ErrorReportingProxy, self2).evalArgs(withClass=self.Proxy) try: ErrorReportingProxy(irc.irc, msg, tokens) except callbacks.ArgumentError as e: pass # TODO: do something with the results if errors: irc.reply('true') else: irc.reply('false') cerror = wrap(cerror, ['something']) Condition = internationalizeDocstring(Conditional) Class = Conditional # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Conditional/test.py0000644000175000017500000001454114535072470017402 0ustar00valval### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, 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 * class ConditionalTestCase(PluginTestCase): plugins = ('Conditional','Utilities',) def testCif(self): self.assertError('cif stuff') self.assertRegexp('cif [ceq bla bla] "echo moo" "echo foo"', 'moo') self.assertRegexp('cif [ceq bla bar] "echo moo" "echo foo"', 'foo') self.assertRegexp('cif [cand [ceq bla bla] [ne soo boo]] "echo moo" "echo foo"', 'moo') self.assertRegexp('cif [ceq [echo $nick] "test"] "echo yay" "echo nay"', 'yay') self.assertRegexp('cif 0 "echo nay" "echo yay"', 'yay') self.assertRegexp('cif 1 "echo yay" "echo nay"', 'yay') self.assertRegexp('cif 4 "echo yay" "echo nay"', 'yay') self.assertRegexp('cif -1 "echo yay" "echo nay"', 'yay') def testCand(self): self.assertRegexp('cand true true', 'true') self.assertRegexp('cand false true', 'false') self.assertRegexp('cand true false', 'false') self.assertRegexp('cand false false', 'false') self.assertRegexp('cand true true true', 'true') def testCor(self): self.assertRegexp('cor true true', 'true') self.assertRegexp('cor false true', 'true') self.assertRegexp('cor true false', 'true') self.assertRegexp('cor false false', 'false') self.assertRegexp('cor true true true', 'true') def testCxor(self): self.assertRegexp('cxor true true', 'false') self.assertRegexp('cxor false true', 'true') self.assertRegexp('cxor true false', 'true') self.assertRegexp('cxor false false', 'false') self.assertRegexp('cxor true true true', 'false') def testCeq(self): self.assertRegexp('ceq bla bla', 'true') self.assertRegexp('ceq bla moo', 'false') self.assertError('ceq bla bla bla') def testNe(self): self.assertRegexp('ne bla bla', 'false') self.assertRegexp('ne bla moo', 'true') self.assertError('ne bla bla bla') def testGt(self): self.assertRegexp('gt bla bla', 'false') self.assertRegexp('gt bla moo', 'false') self.assertRegexp('gt moo bla', 'true') self.assertError('gt bla bla bla') def testGe(self): self.assertRegexp('ge bla bla', 'true') self.assertRegexp('ge bla moo', 'false') self.assertRegexp('ge moo bla', 'true') self.assertError('ge bla bla bla') def testLt(self): self.assertRegexp('lt bla bla', 'false') self.assertRegexp('lt bla moo', 'true') self.assertRegexp('lt moo bla', 'false') self.assertError('lt bla bla bla') def testLe(self): self.assertRegexp('le bla bla', 'true') self.assertRegexp('le bla moo', 'true') self.assertRegexp('le moo bla', 'false') self.assertError('le bla bla bla') def testMatch(self): self.assertRegexp('match bla mooblafoo', 'true') self.assertRegexp('match bla mooblfoo', 'false') self.assertRegexp('match Bla moobLafoo', 'false') self.assertRegexp('match --case-insensitive Bla moobLafoo', 'true') self.assertError('match bla bla stuff') def testNceq(self): self.assertRegexp('nceq 10.0 10', 'true') self.assertRegexp('nceq 4 5', 'false') self.assertError('nceq 1 2 3') self.assertError('nceq bla 1') def testNne(self): self.assertRegexp('nne 1 1', 'false') self.assertRegexp('nne 2.2 3', 'true') self.assertError('nne 1 2 3') self.assertError('nne bla 3') def testNgt(self): self.assertRegexp('ngt 3 3', 'false') self.assertRegexp('ngt 2 3', 'false') self.assertRegexp('ngt 4 3', 'true') self.assertError('ngt 1 2 3') self.assertError('ngt 3 bla') def testNge(self): self.assertRegexp('nge 3 3', 'true') self.assertRegexp('nge 3 4', 'false') self.assertRegexp('nge 5 4.3', 'true') self.assertError('nge 3 4.5 4') self.assertError('nge 45 bla') def testNlt(self): self.assertRegexp('nlt 3 3', 'false') self.assertRegexp('nlt 3 4.5', 'true') self.assertRegexp('nlt 5 3', 'false') self.assertError('nlt 2 3 4') self.assertError('nlt bla bla') def testNle(self): self.assertRegexp('nle 2 2', 'true') self.assertRegexp('nle 2 3.5', 'true') self.assertRegexp('nle 4 3', 'false') self.assertError('nle 3 4 5') self.assertError('nle 1 bla') def testIferror(self): self.assertResponse('cerror "echo hi"', 'false') self.assertResponse('cerror "foobarbaz"', 'true') self.assertResponse('cerror "help foobarbaz"', 'true') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Config/0000755000175000017500000000000014535072535015010 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/__init__.py0000644000175000017500000000442514535072470017124 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Handles configuration of the bot while it is running. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/config.py0000644000175000017500000000474014535072470016632 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Config') 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('Config', True) Config = conf.registerPlugin('Config') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Config, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Config/locales/0000755000175000017500000000000014535072535016432 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/locales/de.po0000644000175000017500000002177114535072470017370 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-27 00:20+0100\n" "Last-Translator: Florian Besser \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: German\n" #: plugin.py:86 msgid "" "Prevents changing certain config variables to gain shell access via\n" " a vulnerable IRC network." msgstr "" #: plugin.py:110 msgid "" "This configuration variable is not writeable via IRC. To change it you have " "to: 1) use the 'flush' command 2) edit the config file 3) use the 'config " "reload' command." msgstr "" #: plugin.py:141 msgid "configuration variable" msgstr "Konfigurationsvariable" #: plugin.py:147 msgid "settable configuration variable" msgstr "setzbare Konfigurationsvariable" #: plugin.py:152 msgid "" "Provides access to the Supybot configuration. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:185 #, fuzzy msgid "" "\n" "\n" " Returns the configuration variables available under the given\n" " configuration . If a variable has values under it, it is\n" " preceded by an '@' sign. If a variable is channel-specific, that " "is,\n" " it can be separately configured for each channel using the " "'channel'\n" " command in this plugin, it is preceded by an '#' sign.\n" " And if a variable is a network-specific, it is preceded by a ':' " "sign.\n" " " msgstr "" "\n" "\n" "Gibt die Konfigurationsvariablen aus die unter der gegeben Konfiguraions " " verfügbar sind. Falls eine Variable mehrere Werte unter ihr wird " "'@' Zeichen vorangestellt. Falls eine Variable ein 'Kanalwert' wird ein '#' " "Zeichen vorangestellt, es ist dann möglich diese Variable für jeden Kanal, " "mit dem 'channel' Befehl, separat zu setzen." #: plugin.py:198 msgid "There don't seem to be any values in %s." msgstr "Es scheint so als würde es keine Werte in %s geben." #: plugin.py:204 msgid "" "\n" "\n" " Searches for in the current configuration variables.\n" " " msgstr "" "\n" "\n" "Sucht nach in den momentanen Konfigurationsvariablen." #: plugin.py:218 plugin.py:237 plugin.py:255 msgid "There were no matching configuration variables." msgstr "Keine passende Konfigurationsvariable gefunden." #: plugin.py:223 #, fuzzy msgid "" "\n" "\n" " Searches for in the help of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "Sucht nach in den momentanen Konfigurationsvariablen." #: plugin.py:242 #, fuzzy msgid "" "\n" "\n" " Searches for in the values of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "Sucht nach in den momentanen Konfigurationsvariablen." #: plugin.py:267 msgid "" "Global: %(global_value)s; %(channel_name)s @ %(network_name)s: " "%(channel_value)s" msgstr "" #: plugin.py:276 msgid "Global: %(global_value)s; %(network_name)s: %(network_value)s" msgstr "" #: plugin.py:293 msgid "" "That registry variable has no value. Use the list command in this plugin to " "see what variables are available in this group." msgstr "" "Diese Registierungsvariable hat keinen Wert. Benutze den list Befehl um zu " "sehen welche Variablen in dieser Gruppe verfügbar sind." #: plugin.py:304 #, fuzzy msgid "" "[] [] []\n" "\n" " If is given, sets the channel configuration variable for " "\n" " to for on the .\n" " Otherwise, returns the current channel\n" " configuration value of . is only necessary if the\n" " message isn't sent in the channel itself. More than one channel may\n" " be given at once by separating them with commas.\n" " defaults to the current network." msgstr "" "[] []\n" "\n" "Falls angegeben ist wird die Kanal Konfigurationsvariable für " "auf gesetzt für . Andererseits wird der momentane Wert der " "Kanal Konfigurationsvariable ausgegben. wird nur benötigt " "wenn die Nachricht nicht im Kanal selbst gesendet wird." #: plugin.py:314 msgid "" "That configuration variable is not a channel-specific configuration variable." msgstr "" "Diese Konfigurationsvariable ist keine kanalspezifische " "Konfigurationsvariable." #: plugin.py:355 #, fuzzy msgid "" "[] []\n" "\n" " If is given, sets the network configuration variable for " "\n" " to for .\n" " Otherwise, returns the current network configuration value of " ".\n" " defaults to the current network." msgstr "" "[] []\n" "\n" "Falls angegeben ist wird die Kanal Konfigurationsvariable für " "auf gesetzt für . Andererseits wird der momentane Wert der " "Kanal Konfigurationsvariable ausgegben. wird nur benötigt " "wenn die Nachricht nicht im Kanal selbst gesendet wird." #: plugin.py:362 #, fuzzy msgid "" "That configuration variable is not a network-specific configuration variable." msgstr "" "Diese Konfigurationsvariable ist keine kanalspezifische " "Konfigurationsvariable." #: plugin.py:380 msgid "" " []\n" "\n" " If is given, sets the value of to . " "Otherwise,\n" " returns the current value of . You may omit the leading\n" " \"supybot.\" in the name if you so choose.\n" " " msgstr "" " []\n" "\n" "Falls angegeben wird, wird der Wert von auf gesetzt. " "Wenn nicht, wird der momentane Wert von ausgegben> Du kannst " "möglicherweise das vornstehende \"supybot.\" im Namen weglassen, falls du " "das möchtest." #: plugin.py:399 msgid "" "\n" "\n" " Returns the description of the configuration variable .\n" " " msgstr "" "\n" "\n" "Gibt die Beschreibung der Konfigurationsvariable aus." #: plugin.py:412 msgid " (Current global value: %s; current channel value: %s)" msgstr "" #: plugin.py:416 plugin.py:418 msgid " (Current value: %s)" msgstr " (Momentaner Wert: %s)" #: plugin.py:421 msgid "" "That configuration group exists, but seems to have no help. Try \"config " "list %s\" to see if it has any children values." msgstr "" "Diese Konfigurationsgruppe extistiert, es scheint aber so als wäre keine " "Hilfe verfügbar. Probiere \"config list\" um zu sehen ob es Werte darunter " "gibt." #: plugin.py:425 msgid "%s has no help." msgstr "%s hat keine Hilfe." #: plugin.py:430 msgid "" "\n" "\n" " Returns the default value of the configuration variable .\n" " " msgstr "" "\n" "\n" "Gibt den Standartwert der Konfigurationsvariable aus." #: plugin.py:440 msgid "" "takes no arguments\n" "\n" " Reloads the various configuration files (user database, channel\n" " database, registry, etc.).\n" " " msgstr "" "hat kein Argument\n" "\n" "Läd verschiedene Konfigurationsdateien neu (Benuter Datenbank, Kanal " "Datenbank, Registrierung, etc)." #: plugin.py:451 msgid "" "\n" "\n" " Exports the public variables of your configuration to .\n" " If you want to show someone your configuration file, but you don't\n" " want that person to be able to see things like passwords, etc., " "this\n" " command will export a \"sanitized\" configuration file suitable for\n" " showing publicly.\n" " " msgstr "" "\n" "\n" "Exportier die öffentlichen Variablen deiner Konfiguration nach . " "Falls du deine Konfiguration jemandem zeigen möchtest, er aber keine Dinge " "wie Passwörter, etc. sehen soll, wird dieser Befehl eine \"bereinigte\" " "Konfiguration exportieren die für die Öffentlichkeit geeignet ist." #: plugin.py:469 msgid "" "\n" "\n" " Resets the configuration variable to its default value.\n" " Use commands 'reset channel' and 'reset network' instead to make\n" " a channel- or network- specific value inherit from the global one.\n" " " msgstr "" #: plugin.py:488 msgid "" "[] [] \n" "\n" " Resets the channel-specific value of variable , so that\n" " it will match the network-specific value (or the global one\n" " if the latter isn't set).\n" " and default to the current network and\n" " channel.\n" " " msgstr "" #: plugin.py:516 msgid "" "[] [] \n" "\n" " Resets the network-specific value of variable , so that\n" " it will match the global.\n" " defaults to the current network and\n" " channel.\n" " " msgstr "" #~ msgid "Global: %s; %s: %s" #~ msgstr "Global: %s; %s: %s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/locales/fi.po0000644000175000017500000002216114535072470017370 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Finnish translation of Config plugin in Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.6.10\n" #: plugin.py:86 msgid "" "Prevents changing certain config variables to gain shell access via\n" " a vulnerable IRC network." msgstr "" #: plugin.py:110 msgid "" "This configuration variable is not writeable via IRC. To change it you have " "to: 1) use the 'flush' command 2) edit the config file 3) use the 'config " "reload' command." msgstr "" #: plugin.py:141 msgid "configuration variable" msgstr "asetusarvo" #: plugin.py:147 msgid "settable configuration variable" msgstr "asetettava asetusarvo" #: plugin.py:152 msgid "" "Provides access to the Supybot configuration. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" "Tarjoaa pääsyn Supybotin asetuksiin. Tämä on Supybotin ydin plugini, jota ei " "pitäisi poistaa!" #: plugin.py:185 #, fuzzy msgid "" "\n" "\n" " Returns the configuration variables available under the given\n" " configuration . If a variable has values under it, it is\n" " preceded by an '@' sign. If a variable is channel-specific, that " "is,\n" " it can be separately configured for each channel using the " "'channel'\n" " command in this plugin, it is preceded by an '#' sign.\n" " And if a variable is a network-specific, it is preceded by a ':' " "sign.\n" " " msgstr "" "\n" "\n" "Palauttaa asetusarvot, jotka ovat annetun\n" "asetus alla. Jos arvolla on toisia arvoja allaan, se on\n" " merkitty '@' merkillä. Jos arvo on 'ChannelValue', se voi olla,\n" "erikseen määritelty jokaiselle kanavalle käyttämällä 'channel'\n" "komentoa tässä lisäosassa, se on merkitty '#' merkillä.\n" " " #: plugin.py:198 msgid "There don't seem to be any values in %s." msgstr "%s:ssä ei näytä olevan yhtään asetusarvoja." #: plugin.py:204 msgid "" "\n" "\n" " Searches for in the current configuration variables.\n" " " msgstr "" "\n" "\n" "Etsii nykyisistä asetus arvoista.\n" " " #: plugin.py:218 plugin.py:237 plugin.py:255 msgid "There were no matching configuration variables." msgstr "Täsmääviä asetusarvoja ei löytynyt." #: plugin.py:223 #, fuzzy msgid "" "\n" "\n" " Searches for in the help of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "Etsii nykyisistä asetus arvoista.\n" " " #: plugin.py:242 #, fuzzy msgid "" "\n" "\n" " Searches for in the values of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "Etsii nykyisistä asetus arvoista.\n" " " #: plugin.py:267 msgid "" "Global: %(global_value)s; %(channel_name)s @ %(network_name)s: " "%(channel_value)s" msgstr "" #: plugin.py:276 msgid "Global: %(global_value)s; %(network_name)s: %(network_value)s" msgstr "" #: plugin.py:293 msgid "" "That registry variable has no value. Use the list command in this plugin to " "see what variables are available in this group." msgstr "" "Sillä rekisteriarvolla ei ole arvoa. Käytä list komentoa tässä lisäosassa " "nähdäksesi mitä arvoja on saatavilla tässä ryhmässä." #: plugin.py:304 #, fuzzy msgid "" "[] [] []\n" "\n" " If is given, sets the channel configuration variable for " "\n" " to for on the .\n" " Otherwise, returns the current channel\n" " configuration value of . is only necessary if the\n" " message isn't sent in the channel itself. More than one channel may\n" " be given at once by separating them with commas.\n" " defaults to the current network." msgstr "" "[] []\n" "\n" "Jos on annettu, asettaa kanavan asetusarvon\n" " . Muutoin, palauttaa nykyisen \n" " nykyisen kanava asetusarvon. on vaadittu vain\n" "jos viestiä ei lähetetä kanavalla itsellään." #: plugin.py:314 msgid "" "That configuration variable is not a channel-specific configuration variable." msgstr "Tällä asetusarvolla ei ole kanava kohtaista asetusarvoa." #: plugin.py:355 #, fuzzy msgid "" "[] []\n" "\n" " If is given, sets the network configuration variable for " "\n" " to for .\n" " Otherwise, returns the current network configuration value of " ".\n" " defaults to the current network." msgstr "" "[] []\n" "\n" "Jos on annettu, asettaa kanavan asetusarvon\n" " . Muutoin, palauttaa nykyisen \n" " nykyisen kanava asetusarvon. on vaadittu vain\n" "jos viestiä ei lähetetä kanavalla itsellään." #: plugin.py:362 #, fuzzy msgid "" "That configuration variable is not a network-specific configuration variable." msgstr "Tällä asetusarvolla ei ole kanava kohtaista asetusarvoa." #: plugin.py:380 msgid "" " []\n" "\n" " If is given, sets the value of to . " "Otherwise,\n" " returns the current value of . You may omit the leading\n" " \"supybot.\" in the name if you so choose.\n" " " msgstr "" " []\n" "\n" "Jos on annettu, asettaa arvon . Muutoin palauttaa,\n" " nykyisen arvon. Voit jättää pois seuraavan rivin pois \n" " \"supybot.\" .\n" " " #: plugin.py:399 msgid "" "\n" "\n" " Returns the description of the configuration variable .\n" " " msgstr "" "\n" "\n" " Palauttaa asetusarvon kuvauksen .\n" " " #: plugin.py:412 msgid " (Current global value: %s; current channel value: %s)" msgstr "" "(Nykyinen globaali asetusarvo: %s; nykyinen kanavakohtainen asetusarvo: %s)" #: plugin.py:416 plugin.py:418 msgid " (Current value: %s)" msgstr " (Nykyinen arvo: %s)" #: plugin.py:421 msgid "" "That configuration group exists, but seems to have no help. Try \"config " "list %s\" to see if it has any children values." msgstr "" "Tuo asetusryhmä on olemassa, mutta sillä ei näytä olevan ohjetta. Käytä " "komentoa \"config list %s\" nähdäksesi onko sillä yhtään alempia arvoja." #: plugin.py:425 msgid "%s has no help." msgstr "%s:llä ei ole ohjetta." #: plugin.py:430 msgid "" "\n" "\n" " Returns the default value of the configuration variable .\n" " " msgstr "" "\n" "\n" "Palauttaa asetusarvon oletusarvon .\n" " " #: plugin.py:440 msgid "" "takes no arguments\n" "\n" " Reloads the various configuration files (user database, channel\n" " database, registry, etc.).\n" " " msgstr "" "ei ota parametrejä\n" "\n" "Lataa uudelleen joitain asetustiedostoja(käyttäjä tietokanta, kanava\n" " tietokanta, rekisteri, jne.).\n" " " #: plugin.py:451 msgid "" "\n" "\n" " Exports the public variables of your configuration to .\n" " If you want to show someone your configuration file, but you don't\n" " want that person to be able to see things like passwords, etc., " "this\n" " command will export a \"sanitized\" configuration file suitable for\n" " showing publicly.\n" " " msgstr "" "\n" "\n" "Vie julkiset asetusarvot asetustiedostostasi .\n" " Jos haluat näyttää jollekulle asetustiedostosi, mutta et\n" "halua tuon henkilön näkevän salasanojasi, jne., tämä\n" "komento vie \"järjellistetyn\" asetustiedoston, joka sopii\n" "julkisesti näyttämiseen.\n" " " #: plugin.py:469 msgid "" "\n" "\n" " Resets the configuration variable to its default value.\n" " Use commands 'reset channel' and 'reset network' instead to make\n" " a channel- or network- specific value inherit from the global one.\n" " " msgstr "" #: plugin.py:488 msgid "" "[] [] \n" "\n" " Resets the channel-specific value of variable , so that\n" " it will match the network-specific value (or the global one\n" " if the latter isn't set).\n" " and default to the current network and\n" " channel.\n" " " msgstr "" #: plugin.py:516 msgid "" "[] [] \n" "\n" " Resets the network-specific value of variable , so that\n" " it will match the global.\n" " defaults to the current network and\n" " channel.\n" " " msgstr "" #~ msgid "Global: %s; %s: %s" #~ msgstr "Globaali: %s; %s: %s" #~ msgid "" #~ "\n" #~ "\n" #~ " Resets the configuration variable to its default value.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa asetusarvon oletukseksi.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/locales/fr.po0000644000175000017500000002216514535072470017405 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:86 msgid "" "Prevents changing certain config variables to gain shell access via\n" " a vulnerable IRC network." msgstr "" #: plugin.py:110 msgid "" "This configuration variable is not writeable via IRC. To change it you have " "to: 1) use the 'flush' command 2) edit the config file 3) use the 'config " "reload' command." msgstr "" #: plugin.py:141 msgid "configuration variable" msgstr "variable de configuration" #: plugin.py:147 msgid "settable configuration variable" msgstr "variable de configuration modifiable" #: plugin.py:152 msgid "" "Provides access to the Supybot configuration. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:185 #, fuzzy msgid "" "\n" "\n" " Returns the configuration variables available under the given\n" " configuration . If a variable has values under it, it is\n" " preceded by an '@' sign. If a variable is channel-specific, that " "is,\n" " it can be separately configured for each channel using the " "'channel'\n" " command in this plugin, it is preceded by an '#' sign.\n" " And if a variable is a network-specific, it is preceded by a ':' " "sign.\n" " " msgstr "" "\n" "\n" "Retourne les variables de configuration qui sont dans le de " "configuration. Si la variable a des sous-variables, elle sera précédée par " "le signe '@'. Si une variable est une 'ChannelValue', elle sera précédée par " "le signe '#'. Plus d'informations : http://supybot.fr.cr/Configuration" #: plugin.py:198 msgid "There don't seem to be any values in %s." msgstr "Il semble n'y avoir aucune valeur dans %s." #: plugin.py:204 msgid "" "\n" "\n" " Searches for in the current configuration variables.\n" " " msgstr "" "\n" "\n" "Recherche le dans les variables de configuration." #: plugin.py:218 plugin.py:237 plugin.py:255 msgid "There were no matching configuration variables." msgstr "Il n'y a aucune variable de configuration correspondante." #: plugin.py:223 #, fuzzy msgid "" "\n" "\n" " Searches for in the help of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "Recherche le dans les variables de configuration." #: plugin.py:242 #, fuzzy msgid "" "\n" "\n" " Searches for in the values of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "Recherche le dans les variables de configuration." #: plugin.py:267 msgid "" "Global: %(global_value)s; %(channel_name)s @ %(network_name)s: " "%(channel_value)s" msgstr "" #: plugin.py:276 msgid "Global: %(global_value)s; %(network_name)s: %(network_value)s" msgstr "" #: plugin.py:293 msgid "" "That registry variable has no value. Use the list command in this plugin to " "see what variables are available in this group." msgstr "" "Cette variable de registre n'a aucune valeur. Utilisez la commande 'list' " "sur ce plugin pour voir quelles variables sont disponibles pour ce groupe." #: plugin.py:304 #, fuzzy msgid "" "[] [] []\n" "\n" " If is given, sets the channel configuration variable for " "\n" " to for on the .\n" " Otherwise, returns the current channel\n" " configuration value of . is only necessary if the\n" " message isn't sent in the channel itself. More than one channel may\n" " be given at once by separating them with commas.\n" " defaults to the current network." msgstr "" "[] []\n" "\n" "Si la est donnée, défini la variable de configuration à " " sur le . Sinon, retourne la configuration actuelle du " " pour . n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:314 msgid "" "That configuration variable is not a channel-specific configuration variable." msgstr "" "Cette variable de configuration n'est pas une variable spécifique à un canal." #: plugin.py:355 #, fuzzy msgid "" "[] []\n" "\n" " If is given, sets the network configuration variable for " "\n" " to for .\n" " Otherwise, returns the current network configuration value of " ".\n" " defaults to the current network." msgstr "" "[] []\n" "\n" "Si la est donnée, défini la variable de configuration à " " sur le . Sinon, retourne la configuration actuelle du " " pour . n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:362 #, fuzzy msgid "" "That configuration variable is not a network-specific configuration variable." msgstr "" "Cette variable de configuration n'est pas une variable spécifique à un canal." #: plugin.py:380 msgid "" " []\n" "\n" " If is given, sets the value of to . " "Otherwise,\n" " returns the current value of . You may omit the leading\n" " \"supybot.\" in the name if you so choose.\n" " " msgstr "" " []\n" "\n" "Si la est donnée, défini la variable de configuration à " ". Sinon, retourne la configuration actuelle de . Vous pouvez " "omettre le 'supybot.' au début du ." #: plugin.py:399 msgid "" "\n" "\n" " Returns the description of the configuration variable .\n" " " msgstr "" "\n" "\n" "Retourne la description de la variable de configuration ." #: plugin.py:412 msgid " (Current global value: %s; current channel value: %s)" msgstr "" #: plugin.py:416 plugin.py:418 msgid " (Current value: %s)" msgstr " (Valeur courante : %s)" #: plugin.py:421 msgid "" "That configuration group exists, but seems to have no help. Try \"config " "list %s\" to see if it has any children values." msgstr "" "Ce groupe de configuration existe mais semble ne pas avoir d'aide. Essayez " "\"config list %s\" pour voir si il a des valeurs filles." #: plugin.py:425 msgid "%s has no help." msgstr "%s n'a pas d'aide." #: plugin.py:430 msgid "" "\n" "\n" " Returns the default value of the configuration variable .\n" " " msgstr "" "\n" "\n" "Retourne la valeur par défaut de la variable de configuration ." #: plugin.py:440 msgid "" "takes no arguments\n" "\n" " Reloads the various configuration files (user database, channel\n" " database, registry, etc.).\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Recharge les différents fichiers de configuration (base d'utilisateurs, base " "de canaux, registre, etc.)." #: plugin.py:451 msgid "" "\n" "\n" " Exports the public variables of your configuration to .\n" " If you want to show someone your configuration file, but you don't\n" " want that person to be able to see things like passwords, etc., " "this\n" " command will export a \"sanitized\" configuration file suitable for\n" " showing publicly.\n" " " msgstr "" "\n" "\n" "Exporte les variables de configuration publiques dans le fichier\n" "\n" " Resets the configuration variable to its default value.\n" " Use commands 'reset channel' and 'reset network' instead to make\n" " a channel- or network- specific value inherit from the global one.\n" " " msgstr "" #: plugin.py:488 msgid "" "[] [] \n" "\n" " Resets the channel-specific value of variable , so that\n" " it will match the network-specific value (or the global one\n" " if the latter isn't set).\n" " and default to the current network and\n" " channel.\n" " " msgstr "" #: plugin.py:516 msgid "" "[] [] \n" "\n" " Resets the network-specific value of variable , so that\n" " it will match the global.\n" " defaults to the current network and\n" " channel.\n" " " msgstr "" #~ msgid "Global: %s; %s: %s" #~ msgstr "Globale : %s ; %s : %s" #~ msgid "" #~ "\n" #~ "\n" #~ " Resets the configuration variable to its default value.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Réinitialise la variable à sa valeur par défaut." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/locales/hu.po0000644000175000017500000002216514535072470017412 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Config\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-30 22:16+0100\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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:86 msgid "" "Prevents changing certain config variables to gain shell access via\n" " a vulnerable IRC network." msgstr "" #: plugin.py:110 msgid "" "This configuration variable is not writeable via IRC. To change it you have " "to: 1) use the 'flush' command 2) edit the config file 3) use the 'config " "reload' command." msgstr "" #: plugin.py:141 msgid "configuration variable" msgstr "konfigurációs változó" #: plugin.py:147 msgid "settable configuration variable" msgstr "beállítható konfigurációs változó" #: plugin.py:152 msgid "" "Provides access to the Supybot configuration. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:185 #, fuzzy msgid "" "\n" "\n" " Returns the configuration variables available under the given\n" " configuration . If a variable has values under it, it is\n" " preceded by an '@' sign. If a variable is channel-specific, that " "is,\n" " it can be separately configured for each channel using the " "'channel'\n" " command in this plugin, it is preceded by an '#' sign.\n" " And if a variable is a network-specific, it is preceded by a ':' " "sign.\n" " " msgstr "" "\n" "\n" "Kiírja a megadott konfigurációs alatt elérhető konfigurációs " "változókat. Ha egy változó alatt vannak értékek, megelőzi egy '@' jel. Ha " "egy változó egy 'ChannelValue', tehát külön konfigurálható mindegyik " "csatornához a 'channel' parancs használatával ebben a bővítményben, megelőzi " "egy '#' jel." #: plugin.py:198 msgid "There don't seem to be any values in %s." msgstr "Nincs semmilyen érték %s-ban." #: plugin.py:204 msgid "" "\n" "\n" " Searches for in the current configuration variables.\n" " " msgstr "" "\n" "\n" "-ra keres az aktuális konfigurációs változók között." #: plugin.py:218 plugin.py:237 plugin.py:255 msgid "There were no matching configuration variables." msgstr "Nincsenek illeszkedő konfigurációs változók." #: plugin.py:223 #, fuzzy msgid "" "\n" "\n" " Searches for in the help of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "-ra keres az aktuális konfigurációs változók között." #: plugin.py:242 #, fuzzy msgid "" "\n" "\n" " Searches for in the values of current configuration " "variables.\n" " " msgstr "" "\n" "\n" "-ra keres az aktuális konfigurációs változók között." #: plugin.py:267 msgid "" "Global: %(global_value)s; %(channel_name)s @ %(network_name)s: " "%(channel_value)s" msgstr "" #: plugin.py:276 msgid "Global: %(global_value)s; %(network_name)s: %(network_value)s" msgstr "" #: plugin.py:293 msgid "" "That registry variable has no value. Use the list command in this plugin to " "see what variables are available in this group." msgstr "" "Ennek az adatbázis-bejegyzésnek nincs értéke. Használd a list parancsot " "ebben a bővítményben, hogy lásd, milyen változók érhetők el ebben a " "csoportban." #: plugin.py:304 #, fuzzy msgid "" "[] [] []\n" "\n" " If is given, sets the channel configuration variable for " "\n" " to for on the .\n" " Otherwise, returns the current channel\n" " configuration value of . is only necessary if the\n" " message isn't sent in the channel itself. More than one channel may\n" " be given at once by separating them with commas.\n" " defaults to the current network." msgstr "" "[] [<érték>]\n" "\n" "Ha <érték> meg van adva, a csatorna konfigurációs változóját <érték>-" "re állítja -ban. Egyébként kiírja a jelenlegi csatorna " "konfigurációs változáját. csak akkor szükséges, ha az üzenet nem " "a csatornában van elküldve." #: plugin.py:314 msgid "" "That configuration variable is not a channel-specific configuration variable." msgstr "" "Ez a konfigurációs változó nem egy csatorna-függő konfigurációs változó." #: plugin.py:355 #, fuzzy msgid "" "[] []\n" "\n" " If is given, sets the network configuration variable for " "\n" " to for .\n" " Otherwise, returns the current network configuration value of " ".\n" " defaults to the current network." msgstr "" "[] [<érték>]\n" "\n" "Ha <érték> meg van adva, a csatorna konfigurációs változóját <érték>-" "re állítja -ban. Egyébként kiírja a jelenlegi csatorna " "konfigurációs változáját. csak akkor szükséges, ha az üzenet nem " "a csatornában van elküldve." #: plugin.py:362 #, fuzzy msgid "" "That configuration variable is not a network-specific configuration variable." msgstr "" "Ez a konfigurációs változó nem egy csatorna-függő konfigurációs változó." #: plugin.py:380 msgid "" " []\n" "\n" " If is given, sets the value of to . " "Otherwise,\n" " returns the current value of . You may omit the leading\n" " \"supybot.\" in the name if you so choose.\n" " " msgstr "" " [<érték>]\n" "\n" "Ha <érték> meg van adva, értékét <érték>-re állítja. Egyébként kiírja " " jelenlegi értékét. Kihagyhatod a megelőző \"supybot.\"-ot a névből, ha " "úgy szeretnéd." #: plugin.py:399 msgid "" "\n" "\n" " Returns the description of the configuration variable .\n" " " msgstr "" "\n" "\n" "Kiírja a konfigurációs változó leírását." #: plugin.py:412 msgid " (Current global value: %s; current channel value: %s)" msgstr "" #: plugin.py:416 plugin.py:418 msgid " (Current value: %s)" msgstr " (Jelenlegi érték: %s)" #: plugin.py:421 msgid "" "That configuration group exists, but seems to have no help. Try \"config " "list %s\" to see if it has any children values." msgstr "" "Ez a konfigurációs csoport létezik, de úgy tűnik, nincs segítsége. Próbáld " "meg a \"config list %s\"-t, hogy lásd, vannak-e gyerek értékei." #: plugin.py:425 msgid "%s has no help." msgstr "%s-nak nincs segítsége." #: plugin.py:430 msgid "" "\n" "\n" " Returns the default value of the configuration variable .\n" " " msgstr "" "\n" "\n" "Kiírja a konfigurációs változó alapértelmezett értékét." #: plugin.py:440 msgid "" "takes no arguments\n" "\n" " Reloads the various configuration files (user database, channel\n" " database, registry, etc.).\n" " " msgstr "" "paraméter nélküli\n" "\n" "Újratölti a különféle konfigurációs fájlokat (felhasználó-adatbázis, " "csatorna-adatbázis, beállításjegyzék stb.)." #: plugin.py:451 msgid "" "\n" "\n" " Exports the public variables of your configuration to .\n" " If you want to show someone your configuration file, but you don't\n" " want that person to be able to see things like passwords, etc., " "this\n" " command will export a \"sanitized\" configuration file suitable for\n" " showing publicly.\n" " " msgstr "" "\n" "\n" "Exportálja a konfigurációd publikus változóit -be. Ha meg szeretnéd " "mutatni valakinek a konfigurációs fájlodat, de nem szeretnéd, hogy az az " "ember láthassa az olyan dolgokat mint jelszavak stb., ez a parancs egy " "\"higiénikus\" konfigurációs fájlt exportál, ami megfelelő nyilvános " "megmutatásra." #: plugin.py:469 msgid "" "\n" "\n" " Resets the configuration variable to its default value.\n" " Use commands 'reset channel' and 'reset network' instead to make\n" " a channel- or network- specific value inherit from the global one.\n" " " msgstr "" #: plugin.py:488 msgid "" "[] [] \n" "\n" " Resets the channel-specific value of variable , so that\n" " it will match the network-specific value (or the global one\n" " if the latter isn't set).\n" " and default to the current network and\n" " channel.\n" " " msgstr "" #: plugin.py:516 msgid "" "[] [] \n" "\n" " Resets the network-specific value of variable , so that\n" " it will match the global.\n" " defaults to the current network and\n" " channel.\n" " " msgstr "" #~ msgid "Global: %s; %s: %s" #~ msgstr "Globális: %s; %s: %s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/locales/it.po0000644000175000017500000002206114535072470017405 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-28 12:33+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:86 msgid "" "Prevents changing certain config variables to gain shell access via\n" " a vulnerable IRC network." msgstr "" #: plugin.py:110 msgid "" "This configuration variable is not writeable via IRC. To change it you have " "to: 1) use the 'flush' command 2) edit the config file 3) use the 'config " "reload' command." msgstr "" #: plugin.py:141 msgid "configuration variable" msgstr "variabile di configurazione" #: plugin.py:147 msgid "settable configuration variable" msgstr "variabile di configurazione impostabile" #: plugin.py:152 msgid "" "Provides access to the Supybot configuration. This is\n" " a core Supybot plugin that should not be removed!" msgstr "" #: plugin.py:185 #, fuzzy msgid "" "\n" "\n" " Returns the configuration variables available under the given\n" " configuration . If a variable has values under it, it is\n" " preceded by an '@' sign. If a variable is channel-specific, that " "is,\n" " it can be separately configured for each channel using the " "'channel'\n" " command in this plugin, it is preceded by an '#' sign.\n" " And if a variable is a network-specific, it is preceded by a ':' " "sign.\n" " " msgstr "" "\n" "\n" " Riporta le variabili di configurazione disponibili per il dato " "\n" " di configurazione. Se una variabile ha delle sottovariabili, sarà " "preceduta\n" " dal simbolo \"@\". Se una variabile è 'ChannelValue', ovvero può " "essere\n" " configurata separatamente per ciascun canale tramite il comando " "\"channel\"\n" " di questo plugin, sarà precedutà dal simbolo \"#\".\n" " " #: plugin.py:198 msgid "There don't seem to be any values in %s." msgstr "Non sembra esserci alcun valore in %s." #: plugin.py:204 msgid "" "\n" "\n" " Searches for in the current configuration variables.\n" " " msgstr "" "\n" "\n" " Cerca nelle variabili di configurazione.\n" " " #: plugin.py:218 plugin.py:237 plugin.py:255 msgid "There were no matching configuration variables." msgstr "Non c'è nessuna variabile di configurazione corrispondente." #: plugin.py:223 #, fuzzy msgid "" "\n" "\n" " Searches for in the help of current configuration " "variables.\n" " " msgstr "" "\n" "\n" " Cerca nelle variabili di configurazione.\n" " " #: plugin.py:242 #, fuzzy msgid "" "\n" "\n" " Searches for in the values of current configuration " "variables.\n" " " msgstr "" "\n" "\n" " Cerca nelle variabili di configurazione.\n" " " #: plugin.py:267 msgid "" "Global: %(global_value)s; %(channel_name)s @ %(network_name)s: " "%(channel_value)s" msgstr "" #: plugin.py:276 msgid "Global: %(global_value)s; %(network_name)s: %(network_value)s" msgstr "" #: plugin.py:293 msgid "" "That registry variable has no value. Use the list command in this plugin to " "see what variables are available in this group." msgstr "" "Questa variabile di registro non ha un valore. Utilizzare il comando \"list" "\" in questo plugin per vedere quali variabili sono disponibili per questo " "gruppo." #: plugin.py:304 #, fuzzy msgid "" "[] [] []\n" "\n" " If is given, sets the channel configuration variable for " "\n" " to for on the .\n" " Otherwise, returns the current channel\n" " configuration value of . is only necessary if the\n" " message isn't sent in the channel itself. More than one channel may\n" " be given at once by separating them with commas.\n" " defaults to the current network." msgstr "" "[] []\n" "\n" " Se è fornito, imposta la variabile di configurazione " "\n" " a per . Altrimenti restituisce l'attuale valore di\n" " configurazione di . è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso." #: plugin.py:314 msgid "" "That configuration variable is not a channel-specific configuration variable." msgstr "Questa variabile di configurazione non è specifica di un canale." #: plugin.py:355 #, fuzzy msgid "" "[] []\n" "\n" " If is given, sets the network configuration variable for " "\n" " to for .\n" " Otherwise, returns the current network configuration value of " ".\n" " defaults to the current network." msgstr "" "[] []\n" "\n" " Se è fornito, imposta la variabile di configurazione " "\n" " a per . Altrimenti restituisce l'attuale valore di\n" " configurazione di . è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso." #: plugin.py:362 #, fuzzy msgid "" "That configuration variable is not a network-specific configuration variable." msgstr "Questa variabile di configurazione non è specifica di un canale." #: plugin.py:380 msgid "" " []\n" "\n" " If is given, sets the value of to . " "Otherwise,\n" " returns the current value of . You may omit the leading\n" " \"supybot.\" in the name if you so choose.\n" " " msgstr "" " []\n" "\n" " Se è fornito, imposta il valore di a . " "Altrimenti riporta\n" " l'attuale valore di . È possibile omettere \"supybot.\" prima " "del nome.\n" " " #: plugin.py:399 msgid "" "\n" "\n" " Returns the description of the configuration variable .\n" " " msgstr "" "\n" "\n" " Riporta la descrizione della variabile di configurazione .\n" " " #: plugin.py:412 msgid " (Current global value: %s; current channel value: %s)" msgstr "" #: plugin.py:416 plugin.py:418 msgid " (Current value: %s)" msgstr " (Valore attuale: %s)" #: plugin.py:421 msgid "" "That configuration group exists, but seems to have no help. Try \"config " "list %s\" to see if it has any children values." msgstr "" "Questo gruppo di configurazione esiste ma sembra non avere un help. Prova " "\"config list %s\" per vedere se ha dei sottovalori." #: plugin.py:425 msgid "%s has no help." msgstr "%s non ha un help." #: plugin.py:430 msgid "" "\n" "\n" " Returns the default value of the configuration variable .\n" " " msgstr "" "\n" "\n" " Riporta il valore predefinito della variabile di configurazione " ".\n" " " #: plugin.py:440 msgid "" "takes no arguments\n" "\n" " Reloads the various configuration files (user database, channel\n" " database, registry, etc.).\n" " " msgstr "" "non necessita argomenti\n" "\n" " Ricarica i file di configurazione (database utenti, database canale, " "registro, ecc.).\n" " " #: plugin.py:451 msgid "" "\n" "\n" " Exports the public variables of your configuration to .\n" " If you want to show someone your configuration file, but you don't\n" " want that person to be able to see things like passwords, etc., " "this\n" " command will export a \"sanitized\" configuration file suitable for\n" " showing publicly.\n" " " msgstr "" "\n" "\n" " Esporta le variabili di configurazione in . Se vuoi\n" " mostrare a qualcuno il tuo file di configurazione evitando di\n" " rivelare dati sensibili come le password, questo comando creerà\n" " un file \"pulito\" adatto ad essere mostrato pubblicamente.\n" " " #: plugin.py:469 msgid "" "\n" "\n" " Resets the configuration variable to its default value.\n" " Use commands 'reset channel' and 'reset network' instead to make\n" " a channel- or network- specific value inherit from the global one.\n" " " msgstr "" #: plugin.py:488 msgid "" "[] [] \n" "\n" " Resets the channel-specific value of variable , so that\n" " it will match the network-specific value (or the global one\n" " if the latter isn't set).\n" " and default to the current network and\n" " channel.\n" " " msgstr "" #: plugin.py:516 msgid "" "[] [] \n" "\n" " Resets the network-specific value of variable , so that\n" " it will match the global.\n" " defaults to the current network and\n" " channel.\n" " " msgstr "" #~ msgid "Global: %s; %s: %s" #~ msgstr "Globale: %s; %s: %s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/plugin.py0000644000175000017500000005236314535072470016667 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 signal import supybot.log as log import supybot.conf as conf import supybot.utils as utils import supybot.world as world import supybot.ircdb as ircdb from supybot.commands import * import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Config') ### # Now, to setup the registry. ### def getWrapper(name): parts = registry.split(name) if not parts or parts[0] not in ('supybot', 'users'): raise registry.InvalidRegistryName(name) group = getattr(conf, parts.pop(0)) while parts: part = parts.pop(0) if group.__hasattr__(part): group = group.get(part) else: # We'll raise registry.InvalidRegistryName here so # that we have a useful error message for the user. raise registry.InvalidRegistryName(name) return group def getCapability(irc, name): capability = 'owner' # Default to requiring the owner capability. if not name.startswith('supybot') and not name.startswith('users'): name = 'supybot.' + name parts = registry.split(name) group = getattr(conf, parts.pop(0)) while parts: part = parts.pop(0) group = group.get(part) if not getattr(group, '_opSettable', True): return 'owner' if irc.isChannel(part): # If a registry value has a channel in it, it requires a # 'channel,op' capability, or so we assume. We'll see if we're # proven wrong. capability = ircdb.makeChannelCapability(part, 'op') ### Do more later, for specific capabilities/sections. return capability def isReadOnly(name): """Prevents changing certain config variables to gain shell access via a vulnerable IRC network.""" parts = registry.split(name.lower()) if parts[0] != 'supybot': parts.insert(0, 'supybot') if parts == ['supybot', 'commands', 'allowshell'] and \ not conf.supybot.commands.allowShell(): # allow setting supybot.commands.allowShell from True to False, # but not from False to True. # Otherwise an IRC network could overwrite it. return True elif parts[0:2] == ['supybot', 'directories'] and \ not conf.supybot.commands.allowShell(): # Setting plugins directory allows for arbitrary code execution if # an attacker can both use the IRC network to MITM and upload files # on the server (eg. with a web CMS). # Setting other directories allows writing data at arbitrary # locations. return True else: return False def checkCanSetValue(irc, msg, group): if isReadOnly(group._name): irc.error(_("This configuration variable is not writeable " "via IRC. To change it you have to: 1) use the 'flush' command 2) edit " "the config file 3) use the 'config reload' command."), Raise=True) capability = getCapability(irc, group._name) if not ircdb.checkCapability(msg.prefix, capability): irc.errorNoCapability(capability, Raise=True) def _reload(): ircdb.users.reload() ircdb.ignores.reload() ircdb.channels.reload() registry.open_registry(world.registryFilename) def _hupHandler(sig, frame): log.info('Received SIGHUP, reloading configuration.') _reload() if os.name == 'posix': signal.signal(signal.SIGHUP, _hupHandler) def getConfigVar(irc, msg, args, state): name = args[0] if name.startswith('conf.'): name = name[5:] if not name.startswith('supybot') and not name.startswith('users'): name = 'supybot.' + name try: group = getWrapper(name) state.args.append(group) del args[0] except registry.InvalidRegistryName as e: state.errorInvalid(_('configuration variable'), str(e)) addConverter('configVar', getConfigVar) def getSettableConfigVar(irc, msg, args, state): getConfigVar(irc, msg, args, state) if not hasattr(state.args[-1], 'set'): state.errorInvalid(_('settable configuration variable'), state.args[-1]._name) addConverter('settableConfigVar', getSettableConfigVar) class Config(callbacks.Plugin): """Provides access to the Supybot configuration. This is a core Supybot plugin that should not be removed!""" def callCommand(self, command, irc, msg, *args, **kwargs): try: super(Config, self).callCommand(command, irc, msg, *args, **kwargs) except registry.InvalidRegistryValue as e: irc.error(str(e)) def _list(self, irc, group): L = [] for (vname, v) in group._children.items(): if getattr(group, '_networkValue', False) and \ vname.startswith(':'): # Skip pseudo-children that are network names continue if getattr(group, '_channelValue', False) and \ irc.isChannel(vname): # Skip pseudo-children that are channel names continue if getattr(v, '_networkValue', False): vname = ':' + vname if getattr(v, '_channelValue', False): vname = '#' + vname if v._added and not all( irc.isChannel(child) or child.startswith(':') for child in v._added): vname = '@' + vname L.append(vname) utils.sortBy(str.lower, L) return L @internationalizeDocstring def list(self, irc, msg, args, group): """ Returns the configuration variables available under the given configuration . If a variable has values under it, it is preceded by an '@' sign. If a variable is channel-specific, that is, it can be separately configured for each channel using the 'channel' command in this plugin, it is preceded by an '#' sign. And if a variable is a network-specific, it is preceded by a ':' sign. """ L = self._list(irc, group) if L: irc.reply(format('%L', sorted(L))) else: irc.error(_('There don\'t seem to be any values in %s.') % group._name) list = wrap(list, ['configVar']) @internationalizeDocstring def search(self, irc, msg, args, word): """ Searches for in the current configuration variables. """ L = [] for (name, x) in conf.supybot.getValues(getChildren=True): if word in name.lower(): last_name_part = registry.split(name)[-1] if not irc.isChannel(last_name_part) \ and not last_name_part.startswith(':'): # network L.append(name) if L: irc.reply(format('%L', L)) else: irc.reply(_('There were no matching configuration variables.')) search = wrap(search, ['lowered']) # XXX compose with withoutSpaces? @internationalizeDocstring def searchhelp(self, irc, msg, args, phrase): """ Searches for in the help of current configuration variables. """ L = [] for (name, x) in conf.supybot.getValues(getChildren=True): if phrase in x.help().lower(): last_name_part = registry.split(name)[-1] if not irc.isChannel(last_name_part) \ and not last_name_part.startswith(':'): # network L.append(name) if L: irc.reply(format('%L', L)) else: irc.reply(_('There were no matching configuration variables.')) searchhelp = wrap(searchhelp, ['lowered']) @internationalizeDocstring def searchvalues(self, irc, msg, args, word): """ Searches for in the values of current configuration variables. """ L = [] for (name, x) in conf.supybot.getValues(getChildren=True): if (hasattr(x, 'value') # not a group and word in str(x()).lower()): last_name_part = registry.split(name)[-1] L.append(name) if L: irc.reply(format('%L', L)) else: irc.reply(_('There were no matching configuration variables.')) searchvalues = wrap(searchvalues, ['lowered']) def _getValue(self, irc, msg, group, network=None, channel=None, addGlobal=False): global_group = group global_value = str(group) or ' ' group = group.getSpecific( network=network.network, channel=channel, check=False) value = str(group) or ' ' if addGlobal and not irc.nested: if global_group._channelValue and channel: # TODO: also show the network value when relevant value = _( 'Global: %(global_value)s; ' '%(channel_name)s @ %(network_name)s: %(channel_value)s') % { 'global_value': global_value, 'channel_name': msg.channel, 'network_name': irc.network, 'channel_value': value, } elif global_group._networkValue and network: value = _( 'Global: %(global_value)s; ' '%(network_name)s: %(network_value)s') % { 'global_value': global_value, 'network_name': irc.network, 'network_value': value, } if hasattr(global_group, 'value'): if not global_group._private: return (value, None) else: capability = getCapability(irc, group._name) if ircdb.checkCapability(msg.prefix, capability): return (value, True) else: irc.errorNoCapability(capability, Raise=True) else: irc.error(_('That registry variable has no value. Use the list ' 'command in this plugin to see what variables are ' 'available in this group.'), Raise=True) def _setValue(self, irc, msg, group, value): checkCanSetValue(irc, msg, group) # I think callCommand catches exceptions here. Should it? group.set(value) @internationalizeDocstring def channel(self, irc, msg, args, network, channels, group, value): """[] [] [] If is given, sets the channel configuration variable for to for on the . Otherwise, returns the current channel configuration value of . is only necessary if the message isn't sent in the channel itself. More than one channel may be given at once by separating them with commas. defaults to the current network.""" if not group._channelValue: irc.error(_('That configuration variable is not a channel-specific ' 'configuration variable.')) return if value is not None: for channel in channels: assert irc.isChannel(channel) # Sets the non-network-specific value, for forward # compatibility, ie. this will work even if the owner rolls # back Limnoria to an older version. # It's also an easy way to support plugins which are not # network-aware. self._setValue(irc, msg, group.get(channel), value) if network != '*': # Set the network-specific value self._setValue(irc, msg, group.get(':' + network.network).get(channel), value) irc.replySuccess() else: if network == '*': network = None values = [] private = None for channel in channels: (value, private_value) = \ self._getValue(irc, msg, group, network, channel) values.append((channel, value)) if private_value: private = True if len(channels) > 1: irc.reply('; '.join(['%s: %s' % (channel, value) for (channel, value) in values]), private=private) else: irc.reply(values[0][1], private=private) channel = wrap(channel, [optional(first(('literal', '*'), 'networkIrc')), 'channels', 'settableConfigVar', additional('text')]) def network(self, irc, msg, args, network, group, value): """[] [] If is given, sets the network configuration variable for to for . Otherwise, returns the current network configuration value of . defaults to the current network.""" if not group._networkValue: irc.error(_('That configuration variable is not a network-specific ' 'configuration variable.')) return if value is not None: self._setValue(irc, msg, group.get(':' + network.network), value) irc.replySuccess() else: values = [] private = None (value, private) = \ self._getValue(irc, msg, group, network) irc.reply(value, private=private) network = wrap(network, ['networkIrc', 'settableConfigVar', additional('text')]) @internationalizeDocstring def config(self, irc, msg, args, group, value): """ [] If is given, sets the value of to . Otherwise, returns the current value of . You may omit the leading "supybot." in the name if you so choose. """ if value is not None: self._setValue(irc, msg, group, value) irc.replySuccess() else: (value, private) = self._getValue( irc, msg, group, network=irc, channel=msg.channel, addGlobal=group._channelValue or group._networkValue) irc.reply(value, private=private) config = wrap(config, ['settableConfigVar', additional('text')]) @internationalizeDocstring def help(self, irc, msg, args, group): """ Returns the description of the configuration variable . """ if hasattr(group, '_help'): s = group.help() if s: if hasattr(group, 'value') and not group._private: if msg.channel and \ msg.channel in group._children: globvalue = str(group) chanvalue = str(group.get(msg.channel)) if chanvalue != globvalue: s += _(' (Current global value: %s; ' 'current channel value: %s)') % \ (globvalue, chanvalue) else: s += _(' (Current value: %s)') % group else: s += _(' (Current value: %s)') % group irc.reply(s) else: irc.reply(_('That configuration group exists, but seems to ' 'have no help. Try "config list %s" to see if it ' 'has any children values.') % group._name) else: irc.error(_('%s has no help.') % group._name) help = wrap(help, ['configVar']) @internationalizeDocstring def default(self, irc, msg, args, group): """ Returns the default value of the configuration variable . """ v = group.__class__(group._default, '') irc.reply(str(v)) default = wrap(default, ['settableConfigVar']) @internationalizeDocstring def reload(self, irc, msg, args): """takes no arguments Reloads the various configuration files (user database, channel database, registry, etc.). """ _reload() # This was factored out for SIGHUP handling. irc.replySuccess() reload = wrap(reload, [('checkCapability', 'owner')]) @internationalizeDocstring def export(self, irc, msg, args, filename): """ Exports the public variables of your configuration to . If you want to show someone your configuration file, but you don't want that person to be able to see things like passwords, etc., this command will export a "sanitized" configuration file suitable for showing publicly. """ if not conf.supybot.commands.allowShell(): # Disallow writing arbitrary files irc.error('This command is not available, because ' 'supybot.commands.allowShell is False.', Raise=True) registry.close(conf.supybot, filename, private=False) irc.replySuccess() export = wrap(export, [('checkCapability', 'owner'), 'filename']) @internationalizeDocstring def setdefault(self, irc, msg, args, group): """ Resets the configuration variable to its default value. Use commands 'reset channel' and 'reset network' instead to make a channel- or network- specific value inherit from the global one. """ v = str(group.__class__(group._default, '')) self._setValue(irc, msg, group, v) irc.replySuccess() setdefault = wrap(setdefault, ['settableConfigVar']) class reset_(callbacks.Commands): # to prevent conflict with the reset() command of callbacks, this class # is renamed 'reset_' def name(self): return 'reset' @internationalizeDocstring def channel(self, irc, msg, args, network, channel, group): """[] [] Resets the channel-specific value of variable , so that it will match the network-specific value (or the global one if the latter isn't set). and default to the current network and channel. """ if network != '*': # reset group.:network.#channel netgroup = group.get(':' + network.network) changroup = netgroup.get(channel) checkCanSetValue(irc, msg, changroup) changroup._setValue(netgroup.value, inherited=True) # reset group.#channel changroup = group.get(channel) checkCanSetValue(irc, msg, changroup) changroup._setValue(group.value, inherited=True) irc.replySuccess() channel = wrap(channel, [ optional(first(('literal', '*'), 'networkIrc')), 'channel', 'settableConfigVar']) @internationalizeDocstring def network(self, irc, msg, args, network, group): """[] [] Resets the network-specific value of variable , so that it will match the global. defaults to the current network and channel. """ # reset group.#channel changroup = group.get(':' + network.network) checkCanSetValue(irc, msg, changroup) changroup._setValue(group.value, inherited=True) irc.replySuccess() network = wrap(network, ['networkIrc', 'settableConfigVar']) Class = Config # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Config/test.py0000644000175000017500000006167314535072470016354 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2022, 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 random from supybot.test import * import supybot.conf as conf import supybot.registry as registry _letters = 'abcdefghijklmnopqrstuvwxyz' def random_string(): return ''.join(random.choice(_letters) for _ in range(16)) class Fruit(registry.OnlySomeStrings): validStrings = ('Apple', 'Orange') group = conf.registerGroup(conf.supybot.plugins.Config, 'test') conf.registerGlobalValue(group, 'fruit', Fruit('Orange', '''Must be a fruit''')) class ConfigTestCase(ChannelPluginTestCase): # We add utilities so there's something in supybot.plugins. plugins = ('Config', 'User', 'Utilities', 'Web') prefix1 = 'somethingElse!user@host1.tld' prefix2 = 'EvensomethingElse!user@host2.tld' prefix3 = 'Completely!Different@host3.tld__no_testcap__' def testGet(self): self.assertNotRegexp('config get supybot.reply', r'registry\.Group') self.assertResponse('config supybot.protocols.irc.throttleTime', '0.0') def testSetOnlysomestrings(self): self.assertResponse('config supybot.plugins.Config.test.fruit Apple', 'The operation succeeded.') self.assertResponse('config supybot.plugins.Config.test.fruit orange', 'The operation succeeded.') self.assertResponse('config supybot.plugins.Config.test.fruit Tomatoe', "Error: Valid values include 'Apple' and " "'Orange', not 'Tomatoe'.") def testList(self): self.assertError('config list asldfkj') self.assertError('config list supybot.asdfkjsldf') self.assertNotError('config list supybot') self.assertRegexp('config list supybot.replies', ', #:errorOwner, ') self.assertRegexp('config list supybot', r'@plugins.*@replies.*@reply') def testListExcludes(self): """Checks that 'config list' excludes pseudo-children of network-specific and channel-specific variables.""" self.assertNotError( 'config channel #zpojfejf supybot.replies.error foo') self.assertRegexp('config list supybot.replies.error', "There don't seem to be any values") def testHelp(self): self.assertError('config help alsdkfj') self.assertError('config help supybot.alsdkfj') self.assertNotError('config help supybot') # We tell the user to list. self.assertNotError('config help supybot.plugins') self.assertNotError('config help supybot.replies.success') self.assertNotError('config help replies.success') def testHelpDoesNotAssertionError(self): self.assertNotRegexp('config help ' # Cont'd. 'supybot.commands.defaultPlugins.help', 'AssertionError') def testHelpExhaustively(self): L = conf.supybot.getValues(getChildren=True) for (name, v) in L: self.assertNotError('config help %s' % name) def testSearch(self): self.assertRegexp( 'config search chars', 'supybot.reply.whenAddressedBy.chars') self.assertNotError('config channel reply.whenAddressedBy.chars @') self.assertNotRegexp('config search chars', self.channel) def testSearchHelp(self): self.assertRegexp( 'config searchhelp "what prefix characters"', 'supybot.reply.whenAddressedBy.chars') self.assertNotError('config channel reply.whenAddressedBy.chars @') self.assertNotRegexp( 'config searchhelp "what prefix characters"', self.channel) def testSearchValues(self): self.assertResponse( 'config searchvalues @@@', 'There were no matching configuration variables.') self.assertNotError('config channel reply.whenAddressedBy.strings @@@') self.assertResponse( 'config searchvalues @@@', r'supybot.reply.whenAddressedBy.strings.#test and ' r'supybot.reply.whenAddressedBy.strings.\:test.#test') def testReload(self): old_password = 'pjfoizjoifjfoii_old' new_password = 'pjfoizjoifjfoii_new' with conf.supybot.networks.test.password.context(old_password): self.assertResponse('config conf.supybot.networks.test.password', old_password) filename = conf.supybot.directories.conf.dirize('Config_testReload.conf') registry.close(conf.supybot, filename) content = open(filename).read() assert old_password in content open(filename, 'wt').write(content.replace(old_password, new_password)) registry.open_registry(filename) self.assertResponse('config conf.supybot.networks.test.password', new_password) def testDefault(self): self.assertNotError('config default ' 'supybot.replies.genericNoCapability') def testConfigErrors(self): self.assertRegexp('config supybot.replies.', 'not a valid') self.assertRegexp('config supybot.repl', 'not a valid') self.assertRegexp('config supybot.reply.withNickPrefix 123', 'True or False.*, not \'123\'.') self.assertRegexp('config supybot.replies foo', 'settable') def testReadOnly(self): old_plugins_dirs = conf.supybot.directories.plugins() try: self.assertResponse('config supybot.commands.allowShell', 'True') self.assertNotError('config supybot.directories.plugins dir1') self.assertNotError('config supybot.commands.allowShell True') self.assertResponse('config supybot.commands.allowShell', 'True') self.assertResponse('config supybot.directories.plugins', 'dir1') self.assertNotError('config supybot.commands.allowShell False') self.assertResponse('config supybot.commands.allowShell', 'False') self.assertRegexp('config supybot.directories.plugins dir2', 'Error.*not writeable') self.assertResponse('config supybot.directories.plugins', 'dir1') self.assertRegexp('config supybot.commands.allowShell True', 'Error.*not writeable') self.assertResponse('config supybot.commands.allowShell', 'False') self.assertRegexp('config commands.allowShell True', 'Error.*not writeable') self.assertResponse('config supybot.commands.allowShell', 'False') self.assertRegexp('config COMMANDS.ALLOWSHELL True', 'Error.*not writeable') self.assertResponse('config supybot.commands.allowShell', 'False') finally: conf.supybot.commands.allowShell.setValue(True) conf.supybot.directories.plugins.setValue(old_plugins_dirs) def testOpEditable(self): var_name = 'testOpEditable' + random_string() conf.registerChannelValue(conf.supybot.plugins.Config, var_name, registry.Integer(0, 'help')) self.assertNotError('register bar passwd', frm=self.prefix3, private=True) self.assertRegexp('whoami', 'bar', frm=self.prefix3) ircdb.users.getUser('bar').addCapability(self.channel + ',op') self.assertRegexp('config plugins.Config.%s 1' % var_name, '^Completely: Error: ', frm=self.prefix3) self.assertResponse('config plugins.Config.%s' % var_name, 'Global: 0; #test @ test: 0') self.assertNotRegexp('config channel plugins.Config.%s 1' % var_name, '^Completely: Error: ', frm=self.prefix3) self.assertResponse('config plugins.Config.%s' % var_name, 'Global: 0; #test @ test: 1') def testOpNonEditable(self): var_name = 'testOpNonEditable' + random_string() conf.registerChannelValue(conf.supybot.plugins.Config, var_name, registry.Integer(0, 'help'), opSettable=False) self.assertNotError('register bar passwd', frm=self.prefix3, private=True) self.assertRegexp('whoami', 'bar', frm=self.prefix3) ircdb.users.getUser('bar').addCapability(self.channel + ',op') self.assertRegexp('config plugins.Config.%s 1' % var_name, '^Completely: Error: ', frm=self.prefix3) self.assertResponse('config plugins.Config.%s' % var_name, 'Global: 0; #test @ test: 0') self.assertRegexp('config channel plugins.Config.%s 1' % var_name, '^Completely: Error: ', frm=self.prefix3) self.assertResponse('config plugins.Config.%s' % var_name, 'Global: 0; #test @ test: 0') self.assertNotRegexp('config channel plugins.Config.%s 1' % var_name, '^Completely: Error: ') self.assertResponse('config plugins.Config.%s' % var_name, 'Global: 0; #test @ test: 1') def testChannel(self): try: conf.supybot.reply.whenAddressedBy.strings.get(':test').unregister(self.channel) conf.supybot.reply.whenAddressedBy.strings.unregister(':test') conf.supybot.reply.whenAddressedBy.strings.unregister(self.channel) except: pass self.assertResponse('config reply.whenAddressedBy.strings ^', 'The operation succeeded.') self.assertResponse('config channel reply.whenAddressedBy.strings @', 'The operation succeeded.') self.assertResponse('config channel reply.whenAddressedBy.strings', '@') self.assertNotError('config channel reply.whenAddressedBy.strings $') self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '^') self.assertResponse('config channel #testchan2 reply.whenAddressedBy.strings', '^') self.assertNotError('config channel #testchan1,#testchan2 reply.whenAddressedBy.strings .') self.assertResponse('config channel reply.whenAddressedBy.strings', '$') self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '.') self.assertResponse('config channel #testchan2 reply.whenAddressedBy.strings', '.') def testNetwork(self): getTestIrc('testnet1') getTestIrc('testnet2') self.assertResponse('config reply.whenAddressedBy.strings ^', 'The operation succeeded.') self.assertResponse('config network reply.whenAddressedBy.strings @', 'The operation succeeded.') self.assertResponse('config network reply.whenAddressedBy.strings', '@') self.assertNotError('config network reply.whenAddressedBy.strings $') self.assertResponse('config network testnet1 reply.whenAddressedBy.strings', '^') self.assertResponse('config network testnet2 reply.whenAddressedBy.strings', '^') self.assertResponse('config network reply.whenAddressedBy.strings', '$') self.assertResponse('config network testnet1 reply.whenAddressedBy.strings', '^') self.assertResponse('config network testnet2 reply.whenAddressedBy.strings', '^') self.assertNotError('config network testnet1 reply.whenAddressedBy.strings =') self.assertResponse('config network testnet1 reply.whenAddressedBy.strings', '=') self.assertResponse('config network testnet2 reply.whenAddressedBy.strings', '^') def testChannelNetwork(self): irc = self.irc irc1 = getTestIrc('testnet1') irc2 = getTestIrc('testnet2') irc3 = getTestIrc('testnet3') conf.supybot.reply.whenAddressedBy.strings.get('#test')._wasSet = False # 1. Set global self.assertResponse('config reply.whenAddressedBy.strings ^', 'The operation succeeded.') # 2. Set for current net + #testchan1 self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings @', 'The operation succeeded.') # Exact match for #2: self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '@') # 3: Set for #testchan1 for all nets: self.assertNotError('config channel * #testchan1 reply.whenAddressedBy.strings $') # Still exact match for #2: self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '@') # Inherit from *: self.assertResponse('config channel testnet1 #testchan1 reply.whenAddressedBy.strings', '$') self.assertResponse('config channel testnet2 #testchan1 reply.whenAddressedBy.strings', '$') # 4: Set for testnet1 for #testchan1 and #testchan2: self.assertNotError('config channel testnet1 #testchan1,#testchan2 reply.whenAddressedBy.strings .') # 5: Set for testnet2 for #testchan1: self.assertNotError('config channel testnet2 #testchan1 reply.whenAddressedBy.strings :') # Inherit from global value (nothing was set of current net or current # chan): (old_channel, self.channel) = (self.channel, '#iejofjfozifk') try: self.assertResponse('config channel reply.whenAddressedBy.strings', '^') finally: self.channel = old_channel # Still exact match for #2: self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '@') self.assertResponse('config channel %s #testchan1 reply.whenAddressedBy.strings' % irc.network, '@') # Exact match for #4: self.assertResponse('config channel testnet1 #testchan1 reply.whenAddressedBy.strings', '.') self.assertResponse('config channel testnet1 #testchan2 reply.whenAddressedBy.strings', '.') # Inherit from #5, which set for #testchan1 on all nets self.assertResponse('config channel testnet3 #testchan1 reply.whenAddressedBy.strings', ':') def testChannelInheritance(self): try: conf.supybot.reply.whenAddressedBy.strings.get(':test').unregister(self.channel) conf.supybot.reply.whenAddressedBy.strings.unregister(':test') conf.supybot.reply.whenAddressedBy.strings.unregister(self.channel) except: pass self.assertResponse('config reply.whenAddressedBy.strings ^', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: ^') self.assertResponse('config channel reply.whenAddressedBy.strings', '^') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) # Parent changes, child follows self.assertResponse('config reply.whenAddressedBy.strings @', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: @; #test @ test: @') self.assertResponse('config channel reply.whenAddressedBy.strings', '@') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) # Child changes, parent keeps its value self.assertResponse('config channel reply.whenAddressedBy.strings $', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: @; #test @ test: $') self.assertResponse('config channel reply.whenAddressedBy.strings', '$') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertTrue( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) # Parent changes, child keeps its value self.assertResponse('config reply.whenAddressedBy.strings .', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: .; #test @ test: $') self.assertResponse('config channel reply.whenAddressedBy.strings', '$') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertTrue( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) def testResetChannel(self): try: conf.supybot.reply.whenAddressedBy.strings.get(':test').unregister(self.channel) conf.supybot.reply.whenAddressedBy.strings.unregister(':test') conf.supybot.reply.whenAddressedBy.strings.unregister(self.channel) except: pass self.assertResponse('config reply.whenAddressedBy.strings ^', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: ^') self.assertResponse('config channel reply.whenAddressedBy.strings', '^') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) # Child changes, parent keeps its value self.assertResponse('config channel reply.whenAddressedBy.strings $', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: $') self.assertResponse('config channel reply.whenAddressedBy.strings', '$') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertTrue( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) # Reset child self.assertResponse('config reset channel reply.whenAddressedBy.strings', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: ^') self.assertResponse('config channel reply.whenAddressedBy.strings', '^') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) # Parent changes, child follows self.assertResponse('config reply.whenAddressedBy.strings .', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: .; #test @ test: .') self.assertResponse('config channel reply.whenAddressedBy.strings', '.') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(self.channel)._wasSet) def testResetNetwork(self): try: conf.supybot.reply.whenAddressedBy.strings.unregister(':test') except: pass self.assertResponse('config reply.whenAddressedBy.strings ^', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: ^') self.assertResponse('config network reply.whenAddressedBy.strings', '^') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(':test')._wasSet) # Child changes, parent keeps its value self.assertResponse('config network reply.whenAddressedBy.strings $', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: $') self.assertResponse('config network reply.whenAddressedBy.strings', '$') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertTrue( conf.supybot.reply.whenAddressedBy.strings.get(':test')._wasSet) # Reset child self.assertResponse('config reset network reply.whenAddressedBy.strings', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: ^; #test @ test: ^') self.assertResponse('config network reply.whenAddressedBy.strings', '^') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(':test')._wasSet) # Parent changes, child follows self.assertResponse('config reply.whenAddressedBy.strings .', 'The operation succeeded.') self.assertResponse('config reply.whenAddressedBy.strings', 'Global: .; #test @ test: .') self.assertResponse('config network reply.whenAddressedBy.strings', '.') self.assertTrue( conf.supybot.reply.whenAddressedBy.strings._wasSet) self.assertFalse( conf.supybot.reply.whenAddressedBy.strings.get(':test')._wasSet) def testResetRegexpChannel(self): """Specifically tests resetting a Regexp value, as they have an extra internal state that needs to be reset; see the comment in plugin.py""" self.assertResponse( 'config plugins.Web.nonSnarfingRegexp', 'Global: ; #test @ test: ' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp m/foo/', 'The operation succeeded.' ) self.assertResponse( 'config channel plugins.Web.nonSnarfingRegexp m/bar/', 'The operation succeeded.' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp', 'Global: m/foo/; #test @ test: m/bar/' ) self.assertResponse( 'config reset channel plugins.Web.nonSnarfingRegexp', 'The operation succeeded.' ) self.assertResponse('config plugins.Web.nonSnarfingRegexp', 'Global: m/foo/; #test @ test: m/foo/' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp ""', 'The operation succeeded.' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp', 'Global: ; #test @ test: ' ) def testResetRegexpNetwork(self): """Specifically tests resetting a Regexp value, as they have an extra internal state that needs to be reset; see the comment in plugin.py""" self.assertResponse( 'config plugins.Web.nonSnarfingRegexp', 'Global: ; #test @ test: ' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp m/foo/', 'The operation succeeded.' ) self.assertResponse( 'config network plugins.Web.nonSnarfingRegexp m/bar/', 'The operation succeeded.' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp', 'Global: m/foo/; #test @ test: m/bar/' ) self.assertResponse( 'config reset network plugins.Web.nonSnarfingRegexp', 'The operation succeeded.' ) self.assertResponse('config plugins.Web.nonSnarfingRegexp', 'Global: m/foo/; #test @ test: m/foo/' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp ""', 'The operation succeeded.' ) self.assertResponse( 'config plugins.Web.nonSnarfingRegexp', 'Global: ; #test @ test: ' ) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Ctcp/0000755000175000017500000000000014535072535014474 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/__init__.py0000644000175000017500000000456414535072470016614 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Handles standard CTCP responses to PING, TIME, SOURCE, VERSION, USERINFO, and FINGER. """ import supybot import supybot.world as world __version__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/config.py0000644000175000017500000000710414535072470016313 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Ctcp') 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('Ctcp', True) ### # Ctcp plugin configuration variables. ### Ctcp = conf.registerPlugin('Ctcp') conf.registerGlobalValue(Ctcp, 'versionWait', registry.PositiveInteger(10, """Determines how many seconds the bot will wait after getting a version command (not a CTCP VERSION, but an actual call of the command in this plugin named "version") before replying with the results it has collected.""")) conf.registerGlobalValue(Ctcp, 'userinfo', registry.String('', """Determines what will be sent when a USERINFO query is received.""")) ### # supybot.abuse configuration variables. ### conf.registerGlobalValue(conf.supybot.abuse.flood, 'ctcp', registry.Boolean(True, """Determines whether the bot will defend itself against CTCP flooding.""")) conf.registerGlobalValue(conf.supybot.abuse.flood.ctcp, 'maximum', registry.PositiveInteger(5, """Determines how many CTCP messages (not including actions) the bot will reply to from a given user in a minute. If a user sends more than this many CTCP messages in a 60 second period, the bot will ignore CTCP messages from this user for supybot.abuse.flood.ctcp.punishment seconds.""")) conf.registerGlobalValue(conf.supybot.abuse.flood.ctcp, 'punishment', registry.PositiveInteger(300, """Determines how many seconds the bot will ignore CTCP messages from users who flood it with CTCP messages.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/Ctcp/locales/0000755000175000017500000000000014535072535016116 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/locales/de.po0000644000175000017500000000404414535072470017046 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-29 19:22+0100\n" "Last-Translator: Florian Besser \n" "Language-Team: Germen \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:44 msgid "" "\n" " Provides replies to common CTCPs (version, time, etc.), and a command\n" " to fetch version responses from channels.\n" "\n" " Please note that the command `ctcp version` cannot receive any responses " "if the channel is\n" " mode +C or similar which prevents CTCP requests to channel.\n" " " msgstr "" #: plugin.py:89 #, fuzzy msgid "^PING(?: (.+))?$" msgstr "PING ?(.*)" #: plugin.py:98 #, fuzzy msgid "^VERSION$" msgstr "VERSION" #: plugin.py:103 #, fuzzy msgid "^USERINFO$" msgstr "USERINFO" #: plugin.py:108 #, fuzzy msgid "^TIME$" msgstr "TIME" #: plugin.py:113 #, fuzzy msgid "^FINGER$" msgstr "FINGER" #: plugin.py:116 msgid "Supybot, the best Python IRC bot in existence!" msgstr "Supybot ist der beste Python IRC Bot den es gibt!" #: plugin.py:119 #, fuzzy msgid "^SOURCE$" msgstr "SOURCE" #: plugin.py:135 msgid "" "[] [--nicks]\n" "\n" " Sends a CTCP VERSION to , returning the various\n" " version strings returned. It waits for 10 seconds before returning\n" " the versions received at that point. If --nicks is given, nicks " "are\n" " associated with the version strings; otherwise, only the version\n" " strings are given.\n" " " msgstr "" "[] [--nicks] \n" "\n" "Sendet CTCP VERSION an , gibt die verschiedenen Versions " "Zeichenketten zurück. Es wartet 10 Sekunden bevor die Versionen die zu " "diesem Zeitpunkt emfpangen wurden zurückgegeben werden. Falls --nicks " "angegeben wird, werden die Versions Zeichenketten an die Nicks geknüpft;wenn " "nicht werden nur die Versions Zeichenketten zurückgegeben." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/locales/fi.po0000644000175000017500000000422414535072470017054 0ustar00valval# Ctcp plugin in Limnoria. # Copyright (C) 2011-2014 Limnoria # Mikaela Suomalainen , 2011-2014 # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-03-22 16:16+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:44 msgid "" "\n" " Provides replies to common CTCPs (version, time, etc.), and a command\n" " to fetch version responses from channels.\n" "\n" " Please note that the command `ctcp version` cannot receive any responses " "if the channel is\n" " mode +C or similar which prevents CTCP requests to channel.\n" " " msgstr "" #: plugin.py:89 msgid "^PING(?: (.+))?$" msgstr "^PING(?: (.+))?$" #: plugin.py:98 msgid "^VERSION$" msgstr "^VERSION$" #: plugin.py:103 msgid "^USERINFO$" msgstr "^USERINFO$" #: plugin.py:108 msgid "^TIME$" msgstr "^TIME$" #: plugin.py:113 msgid "^FINGER$" msgstr "^FINGER$" #: plugin.py:116 msgid "Supybot, the best Python IRC bot in existence!" msgstr "Supybot, paras Pythonilla toteutettu IRC-botti, joka on olemassa!" #: plugin.py:119 msgid "^SOURCE$" msgstr "^SOURCE$" #: plugin.py:135 #, fuzzy msgid "" "[] [--nicks]\n" "\n" " Sends a CTCP VERSION to , returning the various\n" " version strings returned. It waits for 10 seconds before returning\n" " the versions received at that point. If --nicks is given, nicks " "are\n" " associated with the version strings; otherwise, only the version\n" " strings are given.\n" " " msgstr "" "[] [--nicks]\n" "\n" " Lähettää CTCP VERSION , palauttaen \n" " palaavat versioketjut. Se odottaa 10 sekuntia ennen kuin palauttaa\n" " versiot, jotka vastaanotettiin tuolloin. Jos --nicks on annettu, " "nimimerkit\n" " liitetään versioketjuihin; muutoin vain versioketjut\n" " annetaan.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/locales/fr.po0000644000175000017500000000403014535072470017060 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:44 msgid "" "\n" " Provides replies to common CTCPs (version, time, etc.), and a command\n" " to fetch version responses from channels.\n" "\n" " Please note that the command `ctcp version` cannot receive any responses " "if the channel is\n" " mode +C or similar which prevents CTCP requests to channel.\n" " " msgstr "" #: plugin.py:89 #, fuzzy msgid "^PING(?: (.+))?$" msgstr "PING ?(.*)" #: plugin.py:98 #, fuzzy msgid "^VERSION$" msgstr "VERSION" #: plugin.py:103 #, fuzzy msgid "^USERINFO$" msgstr "USERINFO" #: plugin.py:108 #, fuzzy msgid "^TIME$" msgstr "TIME" #: plugin.py:113 #, fuzzy msgid "^FINGER$" msgstr "FINGER" #: plugin.py:116 msgid "Supybot, the best Python IRC bot in existence!" msgstr "Supybot, le meilleur bot IRC en Python au monde !" #: plugin.py:119 #, fuzzy msgid "^SOURCE$" msgstr "SOURCE" #: plugin.py:135 msgid "" "[] [--nicks]\n" "\n" " Sends a CTCP VERSION to , returning the various\n" " version strings returned. It waits for 10 seconds before returning\n" " the versions received at that point. If --nicks is given, nicks " "are\n" " associated with the version strings; otherwise, only the version\n" " strings are given.\n" " " msgstr "" "[] [--nicks]\n" "\n" "Envoie un CTCP VERSION au canal, et renvoie les différentes réponses reçues. " "Il attend 10 secondes avant de renvoyer les réponses reçues jusqu'alors. Si " "--nicks est donné, les nicks sont associés à la chaîne de version ; sinon, " "seules les chaînes sont données." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/locales/hu.po0000644000175000017500000000403114535072470017066 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Ctcp\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-31 12:12+CEST\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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:44 msgid "" "\n" " Provides replies to common CTCPs (version, time, etc.), and a command\n" " to fetch version responses from channels.\n" "\n" " Please note that the command `ctcp version` cannot receive any responses " "if the channel is\n" " mode +C or similar which prevents CTCP requests to channel.\n" " " msgstr "" #: plugin.py:89 #, fuzzy msgid "^PING(?: (.+))?$" msgstr "PING ?(.*)" #: plugin.py:98 #, fuzzy msgid "^VERSION$" msgstr "VERSON" #: plugin.py:103 #, fuzzy msgid "^USERINFO$" msgstr "USERINFO" #: plugin.py:108 #, fuzzy msgid "^TIME$" msgstr "TIME" #: plugin.py:113 #, fuzzy msgid "^FINGER$" msgstr "FINGER" #: plugin.py:116 msgid "Supybot, the best Python IRC bot in existence!" msgstr "Supybot, a legjobb létező Python IRC bot!" #: plugin.py:119 #, fuzzy msgid "^SOURCE$" msgstr "SOURCE" #: plugin.py:135 msgid "" "[] [--nicks]\n" "\n" " Sends a CTCP VERSION to , returning the various\n" " version strings returned. It waits for 10 seconds before returning\n" " the versions received at that point. If --nicks is given, nicks " "are\n" " associated with the version strings; otherwise, only the version\n" " strings are given.\n" " " msgstr "" "[ [--nicks]\n" "\n" "Küld egy CTCP VERSION-t -ra, kiírva a különféle kapott verziókat. " "10 másodpercig vár a kapott verziók kiírása előtt. Ha --nicks meg van adva, " "a nevek össze vannak kapcsolva a verziókkal; egyébként csak a verziók vannak " "kiírva." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/locales/it.po0000644000175000017500000000356714535072470017103 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 13: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" #: plugin.py:44 msgid "" "\n" " Provides replies to common CTCPs (version, time, etc.), and a command\n" " to fetch version responses from channels.\n" "\n" " Please note that the command `ctcp version` cannot receive any responses " "if the channel is\n" " mode +C or similar which prevents CTCP requests to channel.\n" " " msgstr "" #: plugin.py:89 msgid "^PING(?: (.+))?$" msgstr "" #: plugin.py:98 msgid "^VERSION$" msgstr "" #: plugin.py:103 msgid "^USERINFO$" msgstr "" #: plugin.py:108 msgid "^TIME$" msgstr "" #: plugin.py:113 msgid "^FINGER$" msgstr "" #: plugin.py:116 msgid "Supybot, the best Python IRC bot in existence!" msgstr "Supybot, il miglior bot IRC in Python esistente!" #: plugin.py:119 msgid "^SOURCE$" msgstr "" #: plugin.py:135 msgid "" "[] [--nicks]\n" "\n" " Sends a CTCP VERSION to , returning the various\n" " version strings returned. It waits for 10 seconds before returning\n" " the versions received at that point. If --nicks is given, nicks " "are\n" " associated with the version strings; otherwise, only the version\n" " strings are given.\n" " " msgstr "" "[] [--nicks]\n" "\n" " Invia un CTCP VERSION a restituendo le varie stringhe\n" " ricevute. Attende 10 secondi prima di mostrare le versioni ottenute\n" " fino a quel momento. Se --nicks è specificato, i nick sono " "associati\n" " alle stringhe di versione; altrimenti vengono fornite solo le " "stringhe.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/plugin.py0000644000175000017500000001574014535072470016351 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 time import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.schedule as schedule import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Ctcp') class Ctcp(callbacks.PluginRegexp): """ Provides replies to common CTCPs (version, time, etc.), and a command to fetch version responses from channels. Please note that the command `ctcp version` cannot receive any responses if the channel is mode +C or similar which prevents CTCP requests to channel. """ public = False regexps = ('ctcpPing', 'ctcpVersion', 'ctcpUserinfo', 'ctcpTime', 'ctcpFinger', 'ctcpSource') def __init__(self, irc): self.__parent = super(Ctcp, self) self.__parent.__init__(irc) self.ignores = ircutils.IrcDict() self.floods = ircutils.FloodQueue(conf.supybot.abuse.flood.interval()) conf.supybot.abuse.flood.interval.addCallback(self.setFloodQueueTimeout) def setFloodQueueTimeout(self, *args, **kwargs): self.floods.timeout = conf.supybot.abuse.flood.interval() def callCommand(self, command, irc, msg, *args, **kwargs): if conf.supybot.abuse.flood.ctcp(): now = time.time() for (ignore, expiration) in list(self.ignores.items()): if expiration < now: del self.ignores[ignore] elif ircutils.hostmaskPatternEqual(ignore, msg.prefix): return self.floods.enqueue(msg) max = conf.supybot.abuse.flood.ctcp.maximum() if self.floods.len(msg) > max: expires = conf.supybot.abuse.flood.ctcp.punishment() self.log.warning('Apparent CTCP flood from %s, ' 'ignoring CTCP messages for %s seconds.', msg.prefix, expires) ignoreMask = '*!%s@%s' % (msg.user, msg.host) self.ignores[ignoreMask] = now + expires return self.__parent.callCommand(command, irc, msg, *args, **kwargs) def _reply(self, irc, msg, s): s = '\x01%s\x01' % s irc.reply(s, notice=True, private=True, to=msg.nick, stripCtcp=False) def ctcpPing(self, irc, msg, match): "^\x01PING(?: (.+))?\x01$" self.log.info('Received CTCP PING from %s', msg.prefix) payload = match.group(1) if payload: self._reply(irc, msg, 'PING %s' % match.group(1)) else: self._reply(irc, msg, 'PING') def ctcpVersion(self, irc, msg, match): "^\x01VERSION\x01$" self.log.info('Received CTCP VERSION from %s', msg.prefix) self._reply(irc, msg, 'VERSION Limnoria %s' % conf.version) def ctcpUserinfo(self, irc, msg, match): "^\x01USERINFO\x01$" self.log.info('Received CTCP USERINFO from %s', msg.prefix) self._reply(irc, msg, 'USERINFO %s' % self.registryValue('userinfo')) def ctcpTime(self, irc, msg, match): "^\x01TIME\x01$" self.log.info('Received CTCP TIME from %s', msg.prefix) self._reply(irc, msg, 'TIME %s' % time.ctime()) def ctcpFinger(self, irc, msg, match): "^\x01FINGER\x01$" self.log.info('Received CTCP FINGER from %s', msg.prefix) self._reply(irc, msg, 'FINGER ' + _('Supybot, the best Python IRC bot in existence!')) def ctcpSource(self, irc, msg, match): "^\x01SOURCE\x01$" self.log.info('Received CTCP SOURCE from %s', msg.prefix) self._reply(irc, msg, 'SOURCE https://github.com/progval/Limnoria') def doNotice(self, irc, msg): if ircmsgs.isCtcp(msg): try: (version, payload) = msg.args[1][1:-1].split(None, 1) except ValueError: return if version == 'VERSION': self.versions.setdefault(payload, []).append(msg.nick) @internationalizeDocstring def version(self, irc, msg, args, channel, optlist): """[] [--nicks] Sends a CTCP VERSION to , returning the various version strings returned. It waits for 10 seconds before returning the versions received at that point. If --nicks is given, nicks are associated with the version strings; otherwise, only the version strings are given. """ self.versions = ircutils.IrcDict() nicks = False for (option, arg) in optlist: if option == 'nicks': nicks = True irc.queueMsg(ircmsgs.privmsg(channel, '\x01VERSION\x01')) def doReply(): if self.versions: L = [] for (reply, nickslist) in self.versions.items(): if nicks: L.append(format('%L responded with %q', nickslist, reply)) else: L.append(reply) irc.reply(format('%L', L)) else: irc.reply('I received no version responses.') wait = self.registryValue('versionWait') schedule.addEvent(doReply, time.time()+wait) version = wrap(version, ['channel', getopts({'nicks':''})]) Class = Ctcp # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Ctcp/test.py0000644000175000017500000000337114535072470016027 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class CtcpTestCase(PluginTestCase): plugins = ('Ctcp',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3337548 limnoria-2023.11.18/plugins/DDG/0000755000175000017500000000000014535072535014201 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/DDG/__init__.py0000644000175000017500000000515614535072470016317 0ustar00valval### # Copyright (c) 2014-2015, James Lu # Copyright (c) 2020-2021, 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. ### """ Searches for results on DuckDuckGo's web search. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.jlu __maintainer__ = supybot.authors.limnoria_core # 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__ = 'https://github.com/progval/Limnoria/tree/testing/plugins/DDG' from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/DDG/config.py0000644000175000017500000000640414535072470016022 0ustar00valval### # Copyright (c) 2014-2015, James Lu # Copyright (c) 2020-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('DDG') 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('DDG', True) class SafeSearch(registry.OnlySomeStrings): validStrings = ['active', 'moderate', 'off'] DDG = conf.registerPlugin('DDG') conf.registerChannelValue(DDG, 'maxResults', registry.PositiveInteger(4, _("""Determines the maximum number of results the bot will respond with."""))) conf.registerChannelValue(DDG, 'showSnippet', registry.Boolean(True, _("""Determines whether the bot will show a snippet of each resulting link. If False, it will show the title of the link instead."""))) conf.registerChannelValue(DDG, 'region', registry.String("", _("""Set the DDG search region to return results for the language/country of your choice. E.g. 'us-en' for United States. https://duckduckgo.com/params"""))) conf.registerChannelValue(DDG, 'searchFilter', SafeSearch('moderate', _("""Determines what level of search filtering to use by default. 'active' - most filtering, 'moderate' - default filtering, 'off' - no filtering"""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/DDG/parser.py0000644000175000017500000001362514535072470016054 0ustar00valval### # Copyright (c) 2020-2021, 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 enum import collections from html.parser import HTMLParser DEBUG = False def debug(msg, *args): if DEBUG: print(msg % args) result = collections.namedtuple('result', 'link title snippet') @enum.unique class ParserState(enum.Enum): OUTSIDE = 0 IN_TITLE = 1 TITLE_PARSED = 2 IN_SNIPPET = 3 # This is implemented as a stack automaton. Here is the transition graph. # See comments below to find the description of each transition # # --> OUTSIDE --(1)--> IN_TITLE --(2)--> TITLE_PARSED --(3)--> IN_SNIPPET # ^ ^ | | # | | | | # | +----------------(5)---------------+ | # | | # +------------------------------(4)--------------------------+ STACKED_TAGS = ('table', 'tr', 'td', 'a') class DDGHTMLParser(HTMLParser): def __init__(self): super().__init__() self.stack = [] self.results = [] self.reset_current_result() def reset_current_result(self): self.state = ParserState.OUTSIDE self.current_link = None self.current_title = None self.current_snippet = None def handle_starttag(self, tag, attrs): attrs = dict(attrs) classes = attrs.get('class', '').split() if tag in STACKED_TAGS: debug('Stacking %s with classes %s', tag, classes) self.stack.append((tag, classes)) if ('tr', ['result-sponsored']) in self.stack: # Skip sponsored results return if tag == 'a' and 'result-link' in classes: # 1. Starts the title of a result; transition from OUTSIDE # to IN_TITLE debug('Got result-link') assert self.state == ParserState.OUTSIDE, (self.state, self.current_title) self.state = ParserState.IN_TITLE self.current_link = attrs['href'] self.current_title = [] elif tag == 'td' and 'result-snippet' in classes: # 3. Starts a snippet. Normally, just after a title ended. # Transition from TITLE_PARSED to IN_SNIPPET debug('Got result-snipper') assert self.state == ParserState.TITLE_PARSED, self.state self.state = ParserState.IN_SNIPPET self.current_snippet = [] elif tag == 'span' and 'link-text' in classes: # 5. This is the link, after a snippet if any. We're catching it # detect results without a snippet. If so, transition directly # from TITLE_PARSED to OUTSIDE debug('Got link-text') if self.state == ParserState.TITLE_PARSED: # No snippet self.state = ParserState.OUTSIDE self.current_snippet = [] def handle_endtag(self, tag): if tag in STACKED_TAGS: item = self.stack.pop() assert item[0] == tag, (item, tag) if tag == 'a' and self.state == ParserState.IN_TITLE: # 2. End of the node matched in step 1; transition from # IN_TITLE to TITLE_PARSED debug('Title parsed') self.state = ParserState.TITLE_PARSED elif tag == 'td' and self.state == ParserState.IN_SNIPPET: # 4. End of the node matched in step 3, this concludes the # parsing of this result. Transition from IN_SNIPPET to OUTSIDE debug('Snippet parsed') self.build_result() self.state = ParserState.OUTSIDE def handle_data(self, data): if self.state == ParserState.IN_TITLE: debug('Got title data: %s', data) self.current_title.append(data) elif self.state == ParserState.IN_SNIPPET: debug('Got snippet data: %s', data) self.current_snippet.append(data) def build_result(self): res = result( link=self.current_link, title=''.join(self.current_title), snippet=''.join(self.current_snippet), ) debug('Finished parsing result: %r', res) self.results.append(res) self.reset_current_result() if __name__ == '__main__': parser = DDGHTMLParser() with open('ddg2.html') as fd: parser.feed(fd.read()) print(parser.results) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/DDG/plugin.py0000644000175000017500000001551414535072470016055 0ustar00valval### # Copyright (c) 2014-2020, James Lu # Copyright (c) 2020-2021, 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 functools from html.parser import HTMLParser from urllib.parse import urlencode, parse_qs import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks import supybot.log as log try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('DDG') except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x from .parser import DDGHTMLParser class DDG(callbacks.Plugin): """ Searches for results on DuckDuckGo. Example:: <+jlu5> %ddg search eiffel tower <@Atlas> The Eiffel Tower is an iron lattice tower located on the Champ de Mars in Paris. It was named after the engineer Gustave Eiffel, whose company designed and built the tower. - """ threaded = True @staticmethod def _ddgurl(text, region=None, safeSearch=None): # DuckDuckGo has a 'lite' site free of unparseable JavaScript # elements, so we'll use that to our advantage! url = "https://lite.duckduckgo.com/lite?" params = {"q": text} if region: params["kl"] = region if safeSearch: if safeSearch == "active": params["kp"] = 1 elif safeSearch == "moderate": params["kp"] = -1 elif safeSearch == "off": params["kp"] = -2 url += urlencode(params) log.debug("DDG: Using URL %s for search %s", url, text) real_url, data = utils.web.getUrlTargetAndContent(url) data = data.decode("utf-8") parser = DDGHTMLParser() parser.feed(data) # Remove "sponsored link" results return (url, real_url, parser.results) def search_core(self, text, channel_context=None, max_results=None, show_snippet=None): """ Core results fetcher for the DDG plugin. Other plugins can call this as well via irc.getCallback('DDG').search_core(...) """ if show_snippet is None: # Note: don't use ternary here, or the registry value will override any False # settings given to the function directly. show_snippet = self.registryValue("showSnippet", channel_context) maxr = max_results or self.registryValue("maxResults", channel_context) self.log.debug('DDG: got %s for max results', maxr) # In a nutshell, the 'lite' site puts all of its usable content # into tables. This does mean that headings, result snippets and # everything else are all using the same tag (), so parsing is # still somewhat tricky. results = [] region = self.registryValue("region", channel_context) safeSearch = self.registryValue("searchFilter", channel_context) url, real_url, raw_results = self._ddgurl(text, region, safeSearch) if real_url != url: # We received a redirect, likely from something like a !bang request. # Don't bother parsing the target page, as it probably won't work anyways. return [('', '', real_url)] for t in raw_results: if self.registryValue("showsnippet", channel_context): snippet = t.snippet.strip() else: snippet = '' title = t.title.strip() origlink = link = t.link # As of 2017-01-20, some links on DuckDuckGo's site are shown going through # a redirect service. The links are in the format "/l/?kh=-1&uddg=https%3A%2F%2Fduckduckgo.com%2F" # instead of simply being "https://duckduckgo.com". So, we decode these links here. if link.startswith('/l/') or link.startswith('//duckduckgo.com/l/'): linkparse = utils.web.urlparse(link) try: link = parse_qs(linkparse.query)['uddg'][0] except KeyError: # No link was given here, skip. continue except IndexError: self.log.exception("DDG: failed to expand redirected result URL %s", origlink) continue else: self.log.debug("DDG: expanded result URL from %s to %s", origlink, link) # Return a list of tuples in the form (link title, snippet text, link) results.append((title, snippet, link)) return results[:maxr] @wrap(['text']) def search(self, irc, msg, args, text): """ Searches for on DuckDuckGo's web search.""" results = self.search_core(text, msg.args[0]) if not results: irc.error("No results found.") else: strings = [] for r in results: if not r[0]: # This result has no title, so it's likely a redirect from !bang. strings.append(format("See %u", r[2])) else: strings.append(format("%s - %s %u", ircutils.bold(r[0]), r[1], r[2])) irc.reply(', '.join(strings)) Class = DDG # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/DDG/test.py0000644000175000017500000000452514535072470015536 0ustar00valval### # Copyright (c) 2014-2020, James Lu # Copyright (c) 2020-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf from supybot.test import * class DDGTestCase(PluginTestCase): plugins = ('DDG',) if network: def testSearch(self): self.assertRegexp( 'ddg search wikipedia', 'Wikipedia.*? - .*?https?\:\/\/') self.assertRegexp( 'ddg search en.wikipedia.org', 'Wikipedia, the free encyclopedia\x02 - ' '.* ') with conf.supybot.plugins.DDG.region.context('fr-fr'): self.assertRegexp( 'ddg search wikipedia', 'Wikipédia, l\'encyclopédie libre - .*?https?\:\/\/') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Debug/0000755000175000017500000000000014535072535014631 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Debug/__init__.py0000644000175000017500000000451114535072470016741 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This is for developers debugging their plugins; it provides an eval command as well as some other useful commands. It should not be loaded with a default installation. """ import supybot import supybot.world as world from sys import version_info __version__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Debug/config.py0000644000175000017500000000453714535072470016457 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry 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('Debug', True) Debug = conf.registerPlugin('Debug') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Debug, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Debug/plugin.py0000644000175000017500000001612514535072477016513 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This is for debugging purposes only and you shouldn't load this it unless a Supybot developer requests you to debug some issue. """ # Import supybot for easier access to the module namespace import supybot import supybot.plugins as plugins import gc import os import sys try: import exceptions except ImportError: # Python 3 import builtins class exceptions: """Pseudo-module""" pass for (key, value) in list(exceptions.__dict__.items()): if isinstance(value, type) and issubclass(value, Exception): exceptions[key] = value import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.callbacks as callbacks def getTracer(fd): def tracer(frame, event, _): if event == 'call': code = frame.f_code fd.write('%s: %s\n' % (code.co_filename, code.co_name)) return tracer def checkAllowShell(irc): if not conf.supybot.commands.allowShell(): irc.error('This command is not available, because ' 'supybot.commands.allowShell is False.', Raise=True) class Debug(callbacks.Privmsg): """This plugin provides debugging abilities for Supybot. It should not be loaded with a default installation.""" capability = 'owner' def __init__(self, irc): # Setup exec command. self.__parent = super(Debug, self) self.__parent.__init__(irc) setattr(self.__class__, 'exec', self.__class__._exec) def callCommand(self, name, irc, msg, *args, **kwargs): if ircdb.checkCapability(msg.prefix, self.capability): self.__parent.callCommand(name, irc, msg, *args, **kwargs) else: irc.errorNoCapability(self.capability) _evalEnv = {'_': None, '__': None, '___': None, } _evalEnv.update(globals()) def eval(self, irc, msg, args, s): """ Evaluates (which should be a Python expression) and returns its value. If an exception is raised, reports the exception (and logs the traceback to the bot's logfile). """ checkAllowShell(irc) try: self._evalEnv.update(locals()) x = eval(s, self._evalEnv, self._evalEnv) self._evalEnv['___'] = self._evalEnv['__'] self._evalEnv['__'] = self._evalEnv['_'] self._evalEnv['_'] = x irc.reply(repr(x)) except SyntaxError as e: irc.reply(format('%s: %q', utils.exnToString(e), s)) eval = wrap(eval, ['text']) def _exec(self, irc, msg, args, s): """ Execs . Returns success if it didn't raise any exceptions. """ checkAllowShell(irc) exec(s) irc.replySuccess() _exec = wrap(_exec, ['text']) def simpleeval(self, irc, msg, args, text): """ Evaluates the given expression. """ checkAllowShell(irc) try: irc.reply(repr(eval(text))) except Exception as e: irc.reply(utils.exnToString(e)) simpleeval = wrap(simpleeval, ['text']) def exn(self, irc, msg, args, name): """ Raises the exception matching . """ checkAllowShell(irc) # Just to be safe, but probably not needed. if isinstance(__builtins__, dict): exn = __builtins__[name] else: exn = getattr(__builtins__, name) raise exn(msg.prefix) exn = wrap(exn, ['text']) def sendquote(self, irc, msg, args, text): """ Sends (not queues) the raw IRC message given. """ msg = ircmsgs.IrcMsg(text) irc.sendMsg(msg) sendquote = wrap(sendquote, ['text']) def settrace(self, irc, msg, args, filename): """[] Starts tracing function calls to . If is not given, sys.stdout is used. This causes much output. """ checkAllowShell(irc) if filename: fd = open(filename, 'a') else: fd = sys.stdout sys.settrace(getTracer(fd)) irc.replySuccess() settrace = wrap(settrace, [additional('filename')]) def unsettrace(self, irc, msg, args): """takes no arguments Stops tracing function calls on stdout. """ checkAllowShell(irc) sys.settrace(None) irc.replySuccess() unsettrace = wrap(unsettrace) def channeldb(self, irc, msg, args, channel): """[] Returns the result of the channeldb converter. """ irc.reply(channel) channeldb = wrap(channeldb, ['channeldb']) def collect(self, irc, msg, args, times): """[] Does gc collections, returning the number of objects collected each time. defaults to 1. """ L = [] while times: L.append(gc.collect()) times -= 1 irc.reply(format('%L', list(map(str, L)))) collect = wrap(collect, [additional('positiveInt', 1)]) def environ(self, irc, msg, args): """takes no arguments Returns the environment of the supybot process. """ checkAllowShell(irc) # possibly some secret data in the env irc.reply(repr(os.environ)) environ = wrap(environ) Class = Debug # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Debug/test.py0000644000175000017500000000513014535072470016157 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class DebugTestCase(PluginTestCase): plugins = ('Debug',) def testShellForbidden(self): self.assertResponse('debug eval 1+2', '3') self.assertResponse('debug simpleeval 1+2', '3') self.assertResponse('debug exec irc.reply(1+2)', '3') while self.irc.takeMsg(): pass self.assertNotError('debug environ') with conf.supybot.commands.allowShell.context(False): self.assertRegexp('debug eval 1+2', 'Error:.*not available.*supybot.commands.allowShell') self.assertRegexp('debug simpleeval 1+2', 'Error:.*not available.*supybot.commands.allowShell') self.assertRegexp('debug exec irc.reply(1+2)', 'Error:.*not available.*supybot.commands.allowShell') self.assertRegexp('debug environ', 'Error:.*not available.*supybot.commands.allowShell') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Dict/0000755000175000017500000000000014535072535014466 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/__init__.py0000644000175000017500000000477714535072470016614 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Commands that use the dictd protocol to define words. In order to use this plugin you must have the following modules installed: * dictclient: http://quux.org:70/devel/dictclient """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib import reload from .local import dictclient reload(plugin) # In case we're being reloaded. reload(dictclient) if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/config.py0000644000175000017500000000547514535072470016316 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Dict') def configure(advanced): from supybot.questions import output, expect, anything, something, yn conf.registerPlugin('Dict', True) output(_('The default dictd server is dict.org.')) if yn(_('Would you like to specify a different dictd server?')): server = something('What server?') conf.supybot.plugins.Dict.server.set(server) Dict = conf.registerPlugin('Dict') conf.registerGlobalValue(Dict, 'server', registry.String('dict.org', _("""Determines what server the bot will retrieve definitions from."""))) conf.registerChannelValue(Dict, 'default', registry.String('*', _("""Determines the default dictionary the bot will ask for definitions in. If this value is '*' (without the quotes) the bot will use all dictionaries to define words."""))) conf.registerChannelValue(Dict, 'showDictName', registry.Boolean(True, _("""Determines whether the bot will show which dictionaries responded to a query, if the selected dictionary is '*'. """))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Dict/local/0000755000175000017500000000000014535072535015560 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/local/__init__.py0000644000175000017500000000007214535072470017666 0ustar00valval# Stub so local is a module, used for third-party modules ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/local/dictclient.py0000644000175000017500000003002414535072470020251 0ustar00valval# Client for the DICT protocol (RFC2229) # # Copyright (C) 2002 John Goerzen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import socket, re version = '1.0' def dequote(s): """Will remove single or double quotes from the start and end of a string and return the result.""" return s.strip("'\"") def enquote(str): """This function will put a string in double quotes, properly escaping any existing double quotes with a backslash. It will return the result.""" return '"' + str.replace('"', "\\\"") + '"' class Connection: """This class is used to establish a connection to a database server. You will usually use this as the first call into the dictclient library. Instantiating it takes two optional arguments: a hostname (a string) and a port (an int). The hostname defaults to localhost and the port to 2628, the port specified in RFC.""" def __init__(self, hostname = 'localhost', port = 2628): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((hostname, port)) self.rfile = self.sock.makefile("rb") self.wfile = self.sock.makefile("wb", 0) self.saveconnectioninfo() def getresultcode(self): """Generic function to get a result code. It will return a list consisting of two items: the integer result code and the text following. You will not usually use this function directly.""" line = self.rfile.readline().decode('utf8').strip() if line.startswith('['): return [None, line] code, text = line.split(' ', 1) return [int(code), text] def get200result(self): """Used when expecting a single line of text -- a 200-class result. Returns [intcode, remaindertext]""" code, text = self.getresultcode() if code < 200 or code >= 300: raise Exception("Got '%s' when 200-class response expected" % \ line) return [code, text] def get100block(self): """Used when expecting multiple lines of text -- gets the block part only. Does not get any codes or anything! Returns a string.""" data = [] while True: line = self.rfile.readline().decode('utf8').strip() if line == '.': break data.append(line) return "\n".join(data) def get100result(self): """Used when expecting multiple lines of text, terminated by a period and a 200 code. Returns: [initialcode, [bodytext_1lineperentry], finalcode]""" code, text = self.getresultcode() if code < 100 or code >= 200: raise Exception("Got '%s' when 100-class response expected" % \ code) bodylines = self.get100block().split("\n") code2 = self.get200result()[0] return [code, bodylines, code2] def get100dict(self): """Used when expecting a dictionary of results. Will read from the initial 100 code, to a period and the 200 code.""" dict = {} for line in self.get100result()[1]: key, val = line.split(' ', 1) dict[key] = dequote(val) return dict def saveconnectioninfo(self): """Called by __init__ to handle the initial connection. Will save off the capabilities and messageid.""" code, string = self.get200result() assert code == 220 capstr, msgid = re.search('<(.*)> (<.*>)$', string).groups() self.capabilities = capstr.split('.') self.messageid = msgid def getcapabilities(self): """Returns a list of the capabilities advertised by the server.""" return self.capabilities def getmessageid(self): """Returns the message id, including angle brackets.""" return self.messageid def getdbdescs(self): """Gets a dict of available databases. The key is the db name and the value is the db description. This command may generate network traffic!""" if hasattr(self, 'dbdescs'): return self.dbdescs self.sendcommand("SHOW DB") self.dbdescs = self.get100dict() return self.dbdescs def getstratdescs(self): """Gets a dict of available strategies. The key is the strat name and the value is the strat description. This call may generate network traffic!""" if hasattr(self, 'stratdescs'): return self.stratdescs self.sendcommand("SHOW STRAT") self.stratdescs = self.get100dict() return self.stratdescs def getdbobj(self, dbname): """Gets a Database object corresponding to the database name passed in. This function explicitly will *not* generate network traffic. If you have not yet run getdbdescs(), it will fail.""" if not hasattr(self, 'dbobjs'): self.dbobjs = {} if dbname in self.dbobjs: return self.dbobjs[dbname] # We use self.dbdescs explicitly since we don't want to # generate net traffic with this request! if dbname != '*' and dbname != '!' and \ not dbname in self.dbdescs.keys(): raise Exception("Invalid database name '%s'" % dbname) self.dbobjs[dbname] = Database(self, dbname) return self.dbobjs[dbname] def sendcommand(self, command): """Takes a command, without a newline character, and sends it to the server.""" self.wfile.write(command.encode('utf-8') + b"\n") def define(self, database, word): """Returns a list of Definition objects for each matching definition. Parameters are the database name and the word to look up. This is one of the main functions you will use to interact with the server. Returns a list of Definition objects. If there are no matches, an empty list is returned. Note: database may be '*' which means to search all databases, or '!' which means to return matches from the first database that has a match.""" self.getdbdescs() # Prime the cache if database != '*' and database != '!' and \ not database in self.getdbdescs(): raise Exception("Invalid database '%s' specified" % database) self.sendcommand("DEFINE " + enquote(database) + " " + enquote(word)) code = self.getresultcode()[0] retval = [] if code == 552: # No definitions. return [] if code != 150: raise Exception("Unknown code %d" % code) while True: code, text = self.getresultcode() if code != 151 or code is None: break resultword, resultdb = re.search('^"(.+)" (\S+)', text).groups() defstr = self.get100block() retval.append(Definition(self, self.getdbobj(resultdb), resultword, defstr)) return retval def match(self, database, strategy, word): """Gets matches for a query. Arguments are database name, the strategy (see available ones in getstratdescs()), and the pattern/word to look for. Returns a list of Definition objects. If there is no match, an empty list is returned. Note: database may be '*' which means to search all databases, or '!' which means to return matches from the first database that has a match.""" self.getstratdescs() # Prime the cache self.getdbdescs() # Prime the cache if not strategy in self.getstratdescs().keys(): raise Exception("Invalid strategy '%s'" % strategy) if database != '*' and database != '!' and \ not database in self.getdbdescs().keys(): raise Exception("Invalid database name '%s'" % database) self.sendcommand("MATCH %s %s %s" % (enquote(database), enquote(strategy), enquote(word))) code = self.getresultcode()[0] if code == 552: # No Matches return [] if code != 152: raise Exception("Unexpected code %d" % code) retval = [] for matchline in self.get100block().split("\n"): matchdict, matchword = matchline.split(" ", 1) retval.append(Definition(self, self.getdbobj(matchdict), dequote(matchword))) if self.getresultcode()[0] != 250: raise Exception("Unexpected end-of-list code %d" % code) return retval class Database: """An object corresponding to a particular database in a server.""" def __init__(self, dictconn, dbname): """Initialize the object -- requires a Connection object and a database name.""" self.conn = dictconn self.name = dbname def getname(self): """Returns the short name for this database.""" return self.name def getdescription(self): if hasattr(self, 'description'): return self.description if self.getname() == '*': self.description = 'All Databases' elif self.getname() == '!': self.description = 'First matching database' else: self.description = self.conn.getdbdescs()[self.getname()] return self.description def getinfo(self): """Returns a string of info describing this database.""" if hasattr(self, 'info'): return self.info if self.getname() == '*': self.info = "This special database will search all databases on the system." elif self.getname() == '!': self.info = "This special database will return matches from the first matching database." else: self.conn.sendcommand("SHOW INFO " + self.name) self.info = "\n".join(self.conn.get100result()[1]) return self.info def define(self, word): """Get a definition from within this database. The argument, word, is the word to look up. The return value is the same as from Connection.define().""" return self.conn.define(self.getname(), word) def match(self, strategy, word): """Get a match from within this database. The argument, word, is the word to look up. The return value is the same as from Connection.define().""" return self.conn.match(self.getname(), strategy, word) class Definition: """An object corresponding to a single definition.""" def __init__(self, dictconn, db, word, defstr = None): """Instantiate the object. Requires: a Connection object, a Database object (NOT corresponding to '*' or '!' databases), a word. Optional: a definition string. If not supplied, it will be fetched if/when it is requested.""" self.conn = dictconn self.db = db self.word = word self.defstr = defstr def getdb(self): """Get the Database object corresponding to this definition.""" return self.db def getdefstr(self): """Get the definition string (the actual content) of this definition.""" if not self.defstr: self.defstr = self.conn.define(self.getdb().getname(), self.word)[0].getdefstr() return self.defstr def getword(self): """Get the word this object describes.""" return self.word ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Dict/locales/0000755000175000017500000000000014535072535016110 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/locales/fi.po0000644000175000017500000000722314535072470017050 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Dict plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:23+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:39 msgid "The default dictd server is dict.org." msgstr "Oletus dictd-palvelin on dict.org." #: config.py:40 msgid "Would you like to specify a different dictd server?" msgstr "Tahtoisitko määrittää eri dictd-palvelimen?" #: config.py:46 msgid "" "Determines what server the bot will\n" " retrieve definitions from." msgstr "" "Määrittää miltä palvelimelta botti\n" "hakee määritykset." #: config.py:49 #, fuzzy msgid "" "Determines the default dictionary the bot\n" " will ask for definitions in. If this value is '*' (without the quotes)\n" " the bot will use all dictionaries to define words." msgstr "" "Määrittää oletus sanakirjan, josta botti \n" "pyytää määrityksiä. Jos arvo on '*' (ilman lainausmerkkejä) botti\n" "käyttää kaikkia sanakirjoja määrittääkseen sanat." #: config.py:53 msgid "" "Determines whether the bot will show which\n" " dictionaries responded to a query, if the selected dictionary is '*'.\n" " " msgstr "" #: plugin.py:47 msgid "" "This plugin provides a function to look up words from different\n" " dictionaries." msgstr "Tämä plugin tarjoaa toiminnon sanojen etsimiseen eri sanakirjoista." #: plugin.py:52 msgid "" "takes no arguments\n" "\n" " Returns the dictionaries valid for the dict command.\n" " " msgstr "" "Ei ota parametrejä\n" "\n" " Palauttaa sanakirjat, jotka kelpaavat dict komennolle.\n" " " #: plugin.py:68 msgid "" "takes no arguments\n" "\n" " Returns a random valid dictionary.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa satunnaisen kelvollisen sanakirjan.\n" " " #: plugin.py:83 msgid "" "[] \n" "\n" " Looks up the definition of on the dictd server specified by\n" " the supybot.plugins.Dict.server config variable.\n" " " msgstr "" "[] \n" "\n" " Etsii määritystä dictd-palvelimelta, joka on määritetty\n" " supybot.plugins.Dict.server asetus arvossa.\n" " " #: plugin.py:106 msgid "You must give a word to define." msgstr "Sinun täytyy antaa sana määritettäväksi." #: plugin.py:112 msgid "No definition for %q could be found." msgstr "Määritystä %q:lle ei löydetty." #: plugin.py:115 msgid "No definition for %q could be found in %s" msgstr "Määritystä %q:lle ei löydetty %s:stä." #: plugin.py:128 msgid "%L responded: %s" msgstr "%L vastasi: %s" #: plugin.py:135 #, fuzzy msgid "" " [ ...]\n" "\n" " Gets a random synonym from the Moby Thesaurus (moby-thesaurus) " "database.\n" "\n" " If given many words, gets a random synonym for each of them.\n" "\n" " Quote phrases to have them treated as one lookup word.\n" " " msgstr "" " [ ...]\n" " Hakee satunnaisen synonyymin Moby Thesaurus (moby-thes) " "tietokannasta.\n" " \n" " Jos monia sanoja on annettu, hakee satunnaisen synonyymin niille " "kaikille.\n" " \n" " Laita lausekkeita lainausmerkkeihin saadaksesi ne kohdelluiksi " "yhtenä hakusanalla.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/locales/fr.po0000644000175000017500000000655114535072470017064 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:39 msgid "The default dictd server is dict.org." msgstr "Le serveur dictd par défaut est dict.org" #: config.py:40 msgid "Would you like to specify a different dictd server?" msgstr "Voulez-vous spécifier un serveur dictd différent ?" #: config.py:46 msgid "" "Determines what server the bot will\n" " retrieve definitions from." msgstr "Détermine de quel serveur le bot récupérera les définitions." #: config.py:49 #, fuzzy msgid "" "Determines the default dictionary the bot\n" " will ask for definitions in. If this value is '*' (without the quotes)\n" " the bot will use all dictionaries to define words." msgstr "" "Détermine le dictionnaire par défaut dans lequel le bot cherchera les " "définitions. Si la valeur est '*' (sans les guillemets), le bot utilisera " "tous les dictionnaires pour définir le mot." #: config.py:53 msgid "" "Determines whether the bot will show which\n" " dictionaries responded to a query, if the selected dictionary is '*'.\n" " " msgstr "" #: plugin.py:47 msgid "" "This plugin provides a function to look up words from different\n" " dictionaries." msgstr "" #: plugin.py:52 msgid "" "takes no arguments\n" "\n" " Returns the dictionaries valid for the dict command.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne les dictionnaires valides pour la commande dict." #: plugin.py:68 msgid "" "takes no arguments\n" "\n" " Returns a random valid dictionary.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne un dictionnaire valide aléatoire." #: plugin.py:83 msgid "" "[] \n" "\n" " Looks up the definition of on the dictd server specified by\n" " the supybot.plugins.Dict.server config variable.\n" " " msgstr "" "[] \n" "\n" "Recherche la définition du mot sur le serveur dictd spécifié par la variable " "de configuration supybot.plugins.Dict.server." #: plugin.py:106 msgid "You must give a word to define." msgstr "Vous devez donner un mot à définir." #: plugin.py:112 msgid "No definition for %q could be found." msgstr "La définition de %q ne peut être trouvée." #: plugin.py:115 msgid "No definition for %q could be found in %s" msgstr "La définition de %q ne peut être trouvée dans %s." #: plugin.py:128 msgid "%L responded: %s" msgstr "%L a répondu : %s" #: plugin.py:135 #, fuzzy msgid "" " [ ...]\n" "\n" " Gets a random synonym from the Moby Thesaurus (moby-thesaurus) " "database.\n" "\n" " If given many words, gets a random synonym for each of them.\n" "\n" " Quote phrases to have them treated as one lookup word.\n" " " msgstr "" " [ ...]\n" "\n" "Récupère un synonyme au hasard de la base de données du Moby Thesaurus (moby-" "thes). Si plusieurs mots sont donnés, récupère un synonyme au hasard pour " "chacun d'eux. Citez les phrases pour qu'elles soient traitées comme un " "unique mot." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/locales/it.po0000644000175000017500000000644514535072470017073 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-28 20:21+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:39 msgid "The default dictd server is dict.org." msgstr "Il server dictd predefinito è dict.org." #: config.py:40 msgid "Would you like to specify a different dictd server?" msgstr "Vuoi specificare un server dictd diverso?" #: config.py:46 msgid "" "Determines what server the bot will\n" " retrieve definitions from." msgstr "Determina da quale server il bot recupererà le definizioni." #: config.py:49 #, fuzzy msgid "" "Determines the default dictionary the bot\n" " will ask for definitions in. If this value is '*' (without the quotes)\n" " the bot will use all dictionaries to define words." msgstr "" "Determina il dizionario predefinito dal quale il bot chiederà le " "definizioni.\n" " Se il valore è \"*\" (senza virgolette) il bot userà tutti i dizionari." #: config.py:53 msgid "" "Determines whether the bot will show which\n" " dictionaries responded to a query, if the selected dictionary is '*'.\n" " " msgstr "" #: plugin.py:47 msgid "" "This plugin provides a function to look up words from different\n" " dictionaries." msgstr "" #: plugin.py:52 msgid "" "takes no arguments\n" "\n" " Returns the dictionaries valid for the dict command.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta i dizionari validi per il comando dict.\n" " " #: plugin.py:68 msgid "" "takes no arguments\n" "\n" " Returns a random valid dictionary.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta un dizionario casuale.\n" " " #: plugin.py:83 msgid "" "[] \n" "\n" " Looks up the definition of on the dictd server specified by\n" " the supybot.plugins.Dict.server config variable.\n" " " msgstr "" "[] \n" "\n" " Cerca la definizione di sul server dictd specificato dalla\n" " variabile supybot.plugins.Dict.server.\n" " " #: plugin.py:106 msgid "You must give a word to define." msgstr "Devi fornire una parola da definire." #: plugin.py:112 msgid "No definition for %q could be found." msgstr "Non è stata trovata nessuna definizione per %q." #: plugin.py:115 msgid "No definition for %q could be found in %s" msgstr "Non è stata trovata nessuna definizione per %q in %s" #: plugin.py:128 msgid "%L responded: %s" msgstr "%L ha risposto: %s" #: plugin.py:135 #, fuzzy msgid "" " [ ...]\n" "\n" " Gets a random synonym from the Moby Thesaurus (moby-thesaurus) " "database.\n" "\n" " If given many words, gets a random synonym for each of them.\n" "\n" " Quote phrases to have them treated as one lookup word.\n" " " msgstr "" " [ ...]\n" "\n" " Ricava un sinonimo casuale dal database di Moby Thesaurus (moby-" "thes).\n" "\n" " Se sono state fornite più parole ottiene un sinonimo casuale per " "ognuna.\n" "\n" " Affinché vengano trattate come un'unica frase racchiuderle tra " "virgolette.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/plugin.py0000644000175000017500000001447214535072470016344 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 socket import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Dict') import random from .local import dictclient class Dict(callbacks.Plugin): """This plugin provides a function to look up words from different dictionaries.""" threaded = True @internationalizeDocstring def dictionaries(self, irc, msg, args): """takes no arguments Returns the dictionaries valid for the dict command. """ try: server = conf.supybot.plugins.Dict.server() conn = dictclient.Connection(server) dbs = list(conn.getdbdescs().keys()) dbs.sort() irc.reply(format('%L', dbs)) except socket.error as e: irc.error(utils.web.strError(e)) dictionaries = wrap(dictionaries) @internationalizeDocstring def random(self, irc, msg, args): """takes no arguments Returns a random valid dictionary. """ try: server = conf.supybot.plugins.Dict.server() conn = dictclient.Connection(server) dbs = conn.getdbdescs().keys() irc.reply(utils.iter.choice(dbs)) except socket.error as e: irc.error(utils.web.strError(e)) random = wrap(random) @internationalizeDocstring def dict(self, irc, msg, args, words): """[] Looks up the definition of on the dictd server specified by the supybot.plugins.Dict.server config variable. """ try: server = conf.supybot.plugins.Dict.server() conn = dictclient.Connection(server) except socket.error as e: irc.error(utils.web.strError(e), Raise=True) dbs = set(conn.getdbdescs()) if words[0] in dbs: dictionary = words.pop(0) else: default = self.registryValue('default', msg.channel, irc.network) if default in dbs: dictionary = default else: if default: self.log.info('Default dict for %s @ %s is not a supported ' 'dictionary: %s.', msg.channel, irc.network, default) dictionary = '*' if not words: irc.error(_('You must give a word to define.'), Raise=True) word = ' '.join(words) definitions = conn.define(dictionary, word) dbs = set() if not definitions: if dictionary == '*': irc.reply(format(_('No definition for %q could be found.'), word)) else: irc.reply(format(_('No definition for %q could be found in ' '%s'), word, ircutils.bold(dictionary))) return L = [] for d in definitions: dbs.add(ircutils.bold(d.getdb().getname())) (db, s) = (d.getdb().getname(), d.getdefstr()) db = ircutils.bold(db) s = utils.str.normalizeWhitespace(s).rstrip(';.,') L.append('%s: %s' % (db, s)) utils.sortBy(len, L) if dictionary == '*' and len(dbs) > 1 and \ self.registryValue("showDictName", msg.channel, irc.network): s = format(_('%L responded: %s'), list(dbs), '; '.join(L)) else: s = '; '.join(L) irc.reply(s) dict = wrap(dict, [many('something')]) def synonym(self, irc, msg, args, words): """ [ ...] Gets a random synonym from the Moby Thesaurus (moby-thesaurus) database. If given many words, gets a random synonym for each of them. Quote phrases to have them treated as one lookup word. """ try: server = conf.supybot.plugins.Dict.server() conn = dictclient.Connection(server) except socket.error as e: irc.error(utils.web.strError(e), Raise=True) dictionary = 'moby-thesaurus' response = [] for word in words: definitions = conn.define(dictionary, word) if not definitions: asynonym = word else: defstr = definitions[0].getdefstr() synlist = ' '.join(defstr.split('\n')).split(': ', 1)[1].split(',') asynonym = random.choice(synlist).strip() response.append(asynonym) irc.reply(' '.join(response)) synonym = wrap(synonym, [many('something')]) Class = Dict # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dict/test.py0000644000175000017500000000476414535072470016030 0ustar00valval# -*- coding: utf8 -*- ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class DictTestCase(PluginTestCase): plugins = ('Dict',) if network: def testDict(self): self.assertNotError('dict slash') self.assertNotError('dict flutter') self.assertNotRegexp('dict web1913 slash', 'foldoc') self.assertError('dict ""') self.assertRegexp('dict fd-eng-fra school', 'école') def testDictionaries(self): self.assertNotError('dictionaries') def testRandomDictionary(self): self.assertNotError('random') self.assertNotError('dict [random] moo') def testSynonym(self): self.assertNotError('synonym stuff') self.assertNotError('synonym someone goes home') self.assertRegexp('synonym nanotube', 'nanotube') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Dunno/0000755000175000017500000000000014535072535014666 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/__init__.py0000644000175000017500000000540114535072470016775 0ustar00valval### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ The Dunno module is used to spice up the reply when given an invalid command with random 'I dunno'-like responses. If you want something spicier than ' is not a valid command'-like responses, use this plugin. Like Success does for the 'The operation succeeded.' reply. """ 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__ = "0.1" __author__ = supybot.authors.strike __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = { supybot.authors.jemfinch: ['Flatfile DB implementation.'], } from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/config.py0000644000175000017500000000471714535072470016514 0ustar00valval### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Dunno') 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('Dunno', True) Dunno = conf.registerPlugin('Dunno') conf.registerChannelValue(Dunno, 'prefixNick', registry.Boolean(True, _("""Determines whether the bot will prefix the nick of the user giving an invalid command to the "dunno" response."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Dunno/locales/0000755000175000017500000000000014535072535016310 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/locales/de.po0000644000175000017500000000260614535072470017242 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-11-04 18:45+0100\n" "Last-Translator: Florian Besser \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" #: config.py:47 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the \"dunno\" response." msgstr "Legt fest ob der Bot " #: plugin.py:38 #, fuzzy msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'Error: is not a valid " "command.'\n" " messages with messages kept in a database, able to give more personable\n" " responses.\n" "\n" " ``$command`` in the message will be replaced by the command's name." msgstr "" "Dieses plugin wurde ursprünglich geschrieben um mit MoobotFactoids betrieben " "zu werden, die zwei zusammen um ein gleiches MooBot und Blootbot Interface " "für Factoids zu bieten. Es ersetzt die Standard 'Fehler: ist kein " "zulässiger Befehl.' Nachricht mit einer Nachricht die in der Datenbank " "gespeichert ist, um die Antworten mehr zu personalisieren." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/locales/fi.po0000644000175000017500000000314514535072470017247 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-08 16:50+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: config.py:47 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the \"dunno\" response." msgstr "" "Määrittää lisääkö botti virheellisen komennon antaneen käyttäjän\n" " nimimerkin etuliitteeksi \"dunno\" vastaukselle." #: plugin.py:38 #, fuzzy msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'Error: is not a valid " "command.'\n" " messages with messages kept in a database, able to give more personable\n" " responses.\n" "\n" " ``$command`` in the message will be replaced by the command's name." msgstr "" "Tämä lisäosa kirjoitettiin toimimaan MoobotFactoidsin kanssa, molemmat\n" " niistä tarjoavat samankaltainen-moobottiin-ja-blootbottiin " "käyttöliittymä factoideille.\n" " Yksinkertaisesti, it se korvaa perus 'Virhe: ei ole kelvollinen " "komento'\n" " viestit viesteillä, joita pidetään tietokannassa pystyäkseen antamaan " "persoonallisempia\n" " vastauksia." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/locales/fr.po0000644000175000017500000000302514535072470017255 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:47 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the \"dunno\" response." msgstr "" "Détermine si le bot utilisera comme préfixe le nick de l'utilisateur donnant " "la commande invalide dans la réponse \"dunno\"." #: plugin.py:38 #, fuzzy msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'Error: is not a valid " "command.'\n" " messages with messages kept in a database, able to give more personable\n" " responses.\n" "\n" " ``$command`` in the message will be replaced by the command's name." msgstr "" "Ce plugin était à l'origine écrit pour fonctionner avec MoobotFactoids, pour " "fournir une interface similaire aux factoids de moobot et blootbot. Remplace " "simplement les message 'Error: is not a valid commande' par des messages " "configurés dans la base de données, ce qui vous permet de rendre les " "réponses plus personnalisables." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/locales/it.po0000644000175000017500000000264114535072470017265 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 13:58+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:47 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the \"dunno\" response." msgstr "" "Determina se il bot userà il nick dell'utente che dà un comando non\n" " valido come prefisso per la risposta \"dunno\"." #: plugin.py:38 #, fuzzy msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'Error: is not a valid " "command.'\n" " messages with messages kept in a database, able to give more personable\n" " responses.\n" "\n" " ``$command`` in the message will be replaced by the command's name." msgstr "" "Questo plugin fu inizialmente scritto per funzionare con MoobotFactoids, " "per\n" " fornire un'interfaccia simile ai factoid di moobot e blootbot.\n" " Sostituisce i messaggi standard \"Errore: non è un comando valido." "\"\n" " con quelli contenuti in un database, in modo da rendere le risposte più " "piacevoli." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/plugin.py0000644000175000017500000000602714535072470016541 0ustar00valval### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Dunno') class Dunno(plugins.ChannelIdDatabasePlugin): """This plugin was written initially to work with MoobotFactoids, the two of them to provide a similar-to-moobot-and-blootbot interface for factoids. Basically, it replaces the standard 'Error: is not a valid command.' messages with messages kept in a database, able to give more personable responses. ``$command`` in the message will be replaced by the command's name.""" callAfter = ['MoobotFactoids', 'Factoids', 'Infobot'] def invalidCommand(self, irc, msg, tokens): if msg.channel: dunno = self.db.random(msg.channel) if dunno is not None: dunno = dunno.text prefixNick = self.registryValue('prefixNick', msg.channel, irc.network) env = {'command': tokens[0]} self.log.info('Issuing "dunno" answer, %s is not a command.', tokens[0]) dunno = ircutils.standardSubstitute(irc, msg, dunno, env=env) irc.reply(dunno, prefixNick=prefixNick) Dunno = internationalizeDocstring(Dunno) Class = Dunno # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Dunno/test.py0000644000175000017500000000656614535072470016232 0ustar00valval### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 * class DunnoTestCase(ChannelPluginTestCase): plugins = ('Dunno', 'User') def setUp(self): PluginTestCase.setUp(self) self.prefix = 'foo!bar@baz' self.assertNotError('register tester moo', private=True) def testDunnoAdd(self): self.assertNotError('dunno add moo') self.assertResponse('asdfagagfosdfk', 'moo') def testDunnoRemove(self): self.assertNotError('dunno add moo') self.assertNotError('dunno remove 1') def testDunnoSearch(self): self.assertNotError('dunno add foo') self.assertRegexp('dunno search moo', 'No.*dunnos.*found') # Test searching using just the getopts self.assertRegexp('dunno search --regexp m/foo/', r'1 found') self.assertNotError('dunno add moo') self.assertRegexp('dunno search moo', r'1 found') self.assertRegexp('dunno search m', r'1 found') # Test multiple adds for i in range(5): self.assertNotError('dunno add moo%s' % i) self.assertRegexp('dunno search moo', r'6 found') def testDunnoGet(self): self.assertNotError('dunno add moo') self.assertRegexp('dunno get 1', r'#1.*moo') self.assertNotError('dunno add $who') self.assertRegexp('dunno get 2', r'#2.*\$who') self.assertError('dunno get 3') self.assertError('dunno get a') def testDunnoChange(self): self.assertNotError('dunno add moo') self.assertNotError('dunno change 1 s/moo/bar/') self.assertRegexp('dunno get 1', '.*?: [\'"]bar[\'"]') def testDollarCommand(self): self.assertNotError("dunno add I can't $command.") self.assertResponse('asdf', "I can't asdf.") # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Factoids/0000755000175000017500000000000014535072535015337 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/__init__.py0000644000175000017500000000526214535072470017453 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Handles 'factoids,' little tidbits of information held in a database and available on demand via several 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__ = "0.1" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # 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/Factoids/download' from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/config.py0000644000175000017500000001140314535072470017153 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Factoids') 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('Factoids', True) class FactoidFormat(registry.TemplatedString): """Value must include $value, otherwise the factoid's value would be left out.""" requiredTemplates = ['value'] Factoids = conf.registerPlugin('Factoids') conf.registerGroup(Factoids, 'web') conf.registerGlobalValue(Factoids.web, 'enable', registry.Boolean(False, _("""Determines whether the Factoids plugins will be browsable on the HTTP server."""))) conf.registerChannelValue(Factoids.web, 'channel', registry.Boolean(False, _("""Determines whether factoids can be displayed via the web server."""))) conf.registerChannelValue(Factoids, 'requireVoice', registry.Boolean(False, _("""Only allows a user with voice or above on a channel to use the 'learn' and 'forget' commands."""))) conf.registerChannelValue(Factoids, 'learnSeparator', registry.String('is', _("""Determines what separator must be used in the learn command. Defaults to 'is' -- learn is . Users might want to change this to something else, so it's configurable."""))) conf.registerChannelValue(Factoids, 'showFactoidIfOnlyOneMatch', registry.Boolean(True, _("""Determines whether the bot will reply with the single matching factoid if only one factoid matches when using the search command."""))) conf.registerChannelValue(Factoids, 'replyWhenInvalidCommand', registry.Boolean(True, _("""Determines whether the bot will reply to invalid commands by searching for a factoid; basically making the whatis unnecessary when you want all factoids for a given key."""))) conf.registerChannelValue(Factoids, 'replyApproximateSearchKeys', registry.Boolean(True, _("""If you try to look up a nonexistent factoid, this setting make the bot try to find some possible matching keys through several approximate matching algorithms and return a list of matching keys, before giving up."""))) conf.registerChannelValue(Factoids, 'format', FactoidFormat(_('$value'), _("""Determines the format of the response given when a factoid's value is requested. All the standard substitutes apply, in addition to "$key" for the factoid's key and "$value" for the factoid's value."""))) conf.registerChannelValue(Factoids, 'keepRankInfo', registry.Boolean(True, """Determines whether we keep updating the usage count for each factoid, for popularity ranking.""")) conf.registerChannelValue(Factoids, 'rankListLength', registry.Integer(20, """Determines the number of factoid keys returned by the factrank command.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Factoids/locales/0000755000175000017500000000000014535072535016761 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/locales/fi.po0000644000175000017500000003452014535072470017721 0ustar00valval# Factoids plugin in Limnoria # Copyright (C) 2011-2014 Limnoria # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: Factoids plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14:33+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:46 msgid "" "Value must include $value, otherwise the factoid's value would be left\n" " out." msgstr "Arvon täytyy sisältää $value, muuten factoidin arvo jätettäisiin ulos." #: config.py:53 msgid "" "Determines whether the Factoids plugins will\n" " be browsable on the HTTP server." msgstr "Määrittää onko Factoids lisä-osa selattavissa HTTP-palvelimella." #: config.py:56 msgid "" "Determines whether factoids can be displayed\n" " via the web server." msgstr "Määrittää voidaanko factoideja näyttää verkkopalvelimen kautta." #: config.py:60 #, fuzzy msgid "" "Only allows a user with voice or above on a\n" " channel to use the 'learn' and 'forget' commands." msgstr "" "Vain käyttäjät, joilla on ääni tai korkeampi valtuus kanavalla\n" " voivat käyttää tätä komentoa." #: config.py:63 msgid "" "Determines what separator must be used in \n" " the learn command. Defaults to 'is' -- learn is . \n" " Users might want to change this to something else, so it's\n" " configurable." msgstr "" "Määrittää sanan, jota käytetään erottimenä \"learn\"-komennossa. Tämä on\n" " oletuksena 'is\" -- \"learn is .\" Käyttäjät voivat haluta " "vaihtaa tämän\n" " joksikin muuksi, joten se on määritettävissä." #: config.py:68 #, fuzzy msgid "" "Determines whether the bot will reply with the\n" " single matching factoid if only one factoid matches when using the " "search\n" " command." msgstr "" "Määrittää vastaako botti yhden komennon täsmäävään\n" " factoidiin, jos vain yksi täsmää \"search\" komentoa käytettäessä.\n" #: config.py:72 msgid "" "Determines whether the bot will reply to invalid\n" " commands by searching for a factoid; basically making the whatis\n" " unnecessary when you want all factoids for a given key." msgstr "" "Määrittää vastaako botti virheelliseen komentoon etsimällä Factoidia; " "perusteellisesti\n" " tehden \"whatis\" komennon tarpeettomaksi, kun tahdotaan kaikki factoidit, " "jotka\n" " täsmäävät annettuun avaimeen." #: config.py:76 msgid "" "If you try to look up a nonexistent factoid,\n" " this setting make the bot try to find some possible matching keys " "through\n" " several approximate matching algorithms and return a list of matching " "keys,\n" " before giving up." msgstr "" "Jos sinä yritetään etsiä olematonta factoidia, tämä asetus saa botin " "yrittämään \n" " löytää mahdollisesti täsmääviä avaimia muutaman tarkan täsmäusalgoritmin " "läpi ja\n" " palauttaa listan täsmäävistä avaimista ennen luovuttamistaan." #: config.py:81 msgid "$value" msgstr "$value" #: config.py:81 msgid "" "Determines the format of\n" " the response given when a factoid's value is requested. All the " "standard\n" " substitutes apply, in addition to \"$key\" for the factoid's key and " "\"$value\"\n" " for the factoid's value." msgstr "" "Määrittää missä muodossa factoidin arvo annetaan, kun factoidin arvoa " "pyydetään.\n" " Kaikki peruskorvaukset ovat voimassa, \"$key\" factoidin avaimelle ja\n" " \"$value\" factoidin arvolle." #: plugin.py:111 msgid "key" msgstr "avain" #: plugin.py:112 msgid "id" msgstr "id" #: plugin.py:113 msgid "fact" msgstr "facta" #: plugin.py:195 msgid "Provides the ability to show Factoids." msgstr "Tarjoaa kyvyn näyttää Factoideja." #: plugin.py:288 msgid "You have to be at least voiced to teach factoids." msgstr "Sinulla täytyy olla vähintään ääni opettaaksesi factoideja." #: plugin.py:325 msgid "" "[] %s \n" "\n" " Associates with . is only\n" " necessary if the message isn't sent on the channel\n" " itself. The word '%s' is necessary to separate " "the\n" " key from the value. It can be changed to another " "word\n" " via the learnSeparator registry value.\n" " " msgstr "" "[] %s \n" "\n" " Liittää . on vaadittu vain, jos viestiä ei " "lähetetä\n" " kanavalla itsellään. Sana '%s' on vaadittu erottamaan avain arvosta. Se " "voidaan\n" " vaihtaa toiseksi sanaksi rekisteriarvolla plugins.Factoids.learnSeparator." #: plugin.py:345 #, fuzzy msgid "" "Try to typo-match input to possible factoids.\n" " \n" " Assume first letter is correct, to reduce processing time. \n" " First, try a simple wildcard search.\n" " If that fails, use the Damerau-Levenshtein edit-distance metric.\n" " " msgstr "" "Yritetäänkö mahdollisia factoideja etsiä kirjoitusvirhekorjauksella. " "Oletetaan, että\n" " ensimmäinen kirjain on oikein, jotta käsittelyaikaa voidaan vähentää.\n" " Ensin yritetään yksinkertaista jokerimerkki hakua ja jos se epäonnistuu, " "käytetään\n" " Damerau-Levenshtein muokkaus-etäisyys asteikkoa." #: plugin.py:403 plugin.py:534 msgid "That's not a valid number for that key." msgstr "Tuo ei ole kelvollinen numero tuolle avaimelle." #: plugin.py:426 plugin.py:520 plugin.py:749 msgid "No factoid matches that key." msgstr "Mikään factoidi ei täsmää tuota avainta." #: plugin.py:452 msgid "" "[] [--raw] []\n" "\n" " Looks up the value of in the factoid database. If given a\n" " number, will return only that exact factoid. If '--raw' option is\n" " given, no variable substitution will take place on the factoid.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--raw] []\n" "\n" " Etsii arvoa factoidi tietokannasta. Jos numero on annettu,\n" " palauttaa vain juuri sen factoidin. Jos '--raw' asetus on\n" " annettu, muuttujan korvike ei vaikuta factoidissa.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään." #: plugin.py:469 plugin.py:674 msgid "key id" msgstr "avaimen id" # (verbatim) ? #: plugin.py:482 #, fuzzy msgid "" "[] []\n" "\n" " Adds a new key for factoid associated with .\n" " is only necessary if there's more than one factoid " "associated\n" " with .\n" "\n" " The same action can be accomplished by using the 'learn' function " "with\n" " a new key but an existing (verbatim) factoid content.\n" " " msgstr "" "[] []\n" "\n" " Lisää factoidiin, joka on liitetty .\n" " on vaadittu vain, jos vain yksi factoidi on liitetty\n" " .\n" "\n" " Sama toiminto voidaan suorittaa 'learn' toiminnolla, mutta\n" " uusi avain tulee olemassa olamassa olevan (verbatim) factoidin sisällön " "kautta." #: plugin.py:529 plugin.py:546 msgid "This key-factoid relationship already exists." msgstr "Tämä avain-factoidi suhde on jo olemassa." #: plugin.py:537 msgid "" "This key has more than one factoid associated with it, but you have not " "provided a number." msgstr "" "Tähän avaimeen on liitetty useampi, kuin yksi factoidi, mutta numeroa ei ole " "annettu." #: plugin.py:551 msgid "" "[] [--plain] [--alpha] []\n" "\n" " Returns a list of top-ranked factoid keys, sorted by usage count\n" " (rank). If is not provided, the default number of factoid " "keys\n" " returned is set by the rankListLength registry value.\n" "\n" " If --plain option is given, rank numbers and usage counts are not\n" " included in output.\n" "\n" " If --alpha option is given in addition to --plain, keys are sorted\n" " alphabetically, instead of by rank.\n" "\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--plain] [--alpha] []\n" "\n" " Palauttaa factoidien top-listan. käyttömäärän (rank) perusteella. Jos " "\n" " ei ole annettu, palauttaa factoideja rankListLength rekisteriarvon " "määrittämän\n" " määrän.\n" "\n" " Jos asetus --plain on annettu, ranking numeroita ja käyttömääriä ei " "sisällytetä\n" " ulostuloon.\n" "\n" " Jos --alpha asetus on annettu --plain:in lisäksi, avaimet lajitellaan \n" " aakkosjärjestyksessä, rankingin sijasta.\n" "\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään." #: plugin.py:596 msgid "" "[] \n" "\n" " Locks the factoid(s) associated with so that they cannot be\n" " removed or added to. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Lukitsee factoidi(n/t), jotka on liitetty , jotta niitä ei voida\n" " poistaa, eikä lisätä. on vaadittu vain, jos viestiä ei lähetetä\n" " kanavalla itsellään." #: plugin.py:614 msgid "" "[] \n" "\n" " Unlocks the factoid(s) associated with so that they can be\n" " removed or added to. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Avaa factoidi(n/t), jotka on liitetty , jotta niitä voidaan\n" " lisätä ja poistaa. on vaadittu vain mikäli viestiä ei lähetetä " "kanavalla\n" " itsellään." #: plugin.py:653 msgid "" "[] [|*]\n" "\n" " Removes a key-fact relationship for key from the factoids\n" " database. If there is more than one such relationship for this " "key,\n" " a number is necessary to determine which one should be removed.\n" " A * can be used to remove all relationships for .\n" "\n" " If as a result, the key (factoid) remains without any relationships " "to\n" " a factoid (key), it shall be removed from the database.\n" "\n" " is only necessary if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[] [|*]\n" "\n" " Poistaa avain-factoidi suhteen factoidi tietokannasta. Jos " "avaimelle\n" " on useampi kuin yksi suhde, numero on vaadittu määrittämään mikä pitäisi " "poistaa/\n" " *:teä voidaan käyttää poistamaan kaikki suhteet .\n" "\n" " Jos tuloksena, avain (factoidi) jää ilman factoidia (avain), se poistetaan\n" " tietokannasta.\n" "\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla itsellään." #: plugin.py:668 msgid "You have to be at least voiced to remove factoids." msgstr "Sinulla täytyy olla ainakin ääni poistaaksesi factoideja." #: plugin.py:688 msgid "There is no such factoid." msgstr "Tuollaista factoidia ei ole." #: plugin.py:698 msgid "Invalid factoid number." msgstr "Epäkelvollinen factoidin numero." #: plugin.py:703 msgid "" "%s factoids have that key. Please specify which one to remove, or use * to " "designate all of them." msgstr "" "%s factoidilla on tuo avain. Määritä poistettava factoidi tai käytä *-" "merkkiä\n" " poistaaksesi ne kakki." #: plugin.py:711 #, fuzzy msgid "" "[]\n" "\n" " Returns random factoids from the database for . \n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa satunnaisen factoidin tietokannasta. on " "vaadittu vain\n" " jos viestiä ei lähetetä kanavalla itsellään." #: plugin.py:733 msgid "I couldn't find a factoid." msgstr "En voinut löytää factoidia." #: plugin.py:738 msgid "" "[] \n" "\n" " Gives information about the factoid(s) associated with .\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Antaa tiedot factoid(ista/eista), jotka on liitetty .\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" " itsellään." #: plugin.py:764 msgid "#%i was added by %s at %s, and has been recalled %n" msgstr "#Factoidin %i lisäsi %s %s, ja sitä on kutsuttu %n" #: plugin.py:767 msgid "time" msgstr "kerta" #: plugin.py:777 msgid "" "[] \n" "\n" " Changes the factoid # associated with according to\n" " .\n" " " msgstr "" "[] \n" "\n" " Vaihtaa factoidin #, joka on liitetty \n" " mukaan." #: plugin.py:791 msgid "I couldn't find any key %q" msgstr "En voinut löytää yhtään avainta %q" #: plugin.py:806 #, fuzzy msgid "" "[] [--values] [--regexp ] [--author ] " "[ ...]\n" "\n" " Searches the keyspace for keys matching . If --regexp is " "given,\n" " its associated value is taken as a regexp and matched against the " "keys.\n" " If --values is given, search the value space instead of the " "keyspace.\n" " " msgstr "" "[] [--values] [--{regexp} ] [ ...]\n" "\n" " Etsii avainavaruudesta avaimia, jotka täsmäävät . Jos --regexp " "on\n" " annettu, liittää arvon, joka on otettu säännöllisestä lausekkeesta ja " "täsmätty\n" " avaimia vastaan. Jos --values on annettu, etsii arvoavaruudesta " "avainavaruuden\n" " sijaan.\n" " " #: plugin.py:854 plugin.py:864 msgid "No keys matched that query." msgstr "Yksikään avain ei täsmännyt hakuun." #: plugin.py:860 plugin.py:870 msgid "More than 100 keys matched that query; please narrow your query." msgstr "Yli 100 avainta täsmäsi tuohon hakuun; ole hyvä ja kavenna hakuasi." #~ msgid "$key could be $value." #~ msgstr "$key voisi olla $value." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/locales/fr.po0000644000175000017500000003357714535072470017745 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:46 msgid "" "Value must include $value, otherwise the factoid's value would be left\n" " out." msgstr "" "La valeur doit inclure $value, sinon la valeur de la factoid ne serait pas " "affichée." #: config.py:53 msgid "" "Determines whether the Factoids plugins will\n" " be browsable on the HTTP server." msgstr "Détermine si le plugin Factoids sera accessible par le serveur HTTP." #: config.py:56 msgid "" "Determines whether factoids can be displayed\n" " via the web server." msgstr "Détermine si les factoids seront affichées via le serveur web." #: config.py:60 #, fuzzy msgid "" "Only allows a user with voice or above on a\n" " channel to use the 'learn' and 'forget' commands." msgstr "" "Autoriser uniquement un(e) utilisateur(trice) qui a un voice ou mieux dans " "le salon à utiliser la commande." #: config.py:63 #, fuzzy msgid "" "Determines what separator must be used in \n" " the learn command. Defaults to 'is' -- learn is . \n" " Users might want to change this to something else, so it's\n" " configurable." msgstr "" "Détermine quel séparateur est utilisé par la commande 'learn'. Par défaut, " "il s'agit de 'est' : 'learn as . Les utilisateurs sont " "susceptibles de trouver 'is' plus confortable, ou n'importe quoi, c'est " "configurable." #: config.py:68 msgid "" "Determines whether the bot will reply with the\n" " single matching factoid if only one factoid matches when using the " "search\n" " command." msgstr "" "Détermine si le bot répondra avec une seule factoid, même si la recherche de " "l'utilisateur donne plusieurs résultats." #: config.py:72 msgid "" "Determines whether the bot will reply to invalid\n" " commands by searching for a factoid; basically making the whatis\n" " unnecessary when you want all factoids for a given key." msgstr "" "Détermine si le bot répondra aux commandes invalides lors de la recherche " "d'une factoid ; permet simplement de rendre la commande 'whatis' inutile " "lorsque vous voulez toutes les factoids d'un clef donnée." #: config.py:76 msgid "" "If you try to look up a nonexistent factoid,\n" " this setting make the bot try to find some possible matching keys " "through\n" " several approximate matching algorithms and return a list of matching " "keys,\n" " before giving up." msgstr "" "Si vous essayez de chercher une factoid inexistante, cette option permet de " "faire en sorte que le bot recherche les clefs de factoids dont le nom est " "proche, et qu'il les affiche." #: config.py:81 msgid "$value" msgstr "" #: config.py:81 msgid "" "Determines the format of\n" " the response given when a factoid's value is requested. All the " "standard\n" " substitutes apply, in addition to \"$key\" for the factoid's key and " "\"$value\"\n" " for the factoid's value." msgstr "" "Détermine le format de la réponse donnée lorsqu'une valeur de factoid est " "demandée. Tous les substitus standards s'appliquent, en plus de \"$key\" " "pour la clef de la factoid et \"$value\" pour la valeur de la factoid." #: plugin.py:111 msgid "key" msgstr "clé" #: plugin.py:112 msgid "id" msgstr "n°" #: plugin.py:113 msgid "fact" msgstr "factoid" #: plugin.py:195 msgid "Provides the ability to show Factoids." msgstr "" #: plugin.py:288 msgid "You have to be at least voiced to teach factoids." msgstr "Vous devez au moins être voice pour m’apprendre des factoids." #: plugin.py:325 msgid "" "[] %s \n" "\n" " Associates with . is only\n" " necessary if the message isn't sent on the channel\n" " itself. The word '%s' is necessary to separate " "the\n" " key from the value. It can be changed to another " "word\n" " via the learnSeparator registry value.\n" " " msgstr "" "[] %s \n" "\n" "Associer la avec la . n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même. Le mot '%s' est nécessaire " "pour séparer la clef de la valeur. Il peut être modifié avec la valeur de " "registre learnSeparator." #: plugin.py:345 msgid "" "Try to typo-match input to possible factoids.\n" " \n" " Assume first letter is correct, to reduce processing time. \n" " First, try a simple wildcard search.\n" " If that fails, use the Damerau-Levenshtein edit-distance metric.\n" " " msgstr "." #: plugin.py:403 plugin.py:534 msgid "That's not a valid number for that key." msgstr "Ce n'est pas un nombre valide pour cette clef." #: plugin.py:426 plugin.py:520 plugin.py:749 msgid "No factoid matches that key." msgstr "Aucune factoid ne correspond à cette clef." #: plugin.py:452 msgid "" "[] [--raw] []\n" "\n" " Looks up the value of in the factoid database. If given a\n" " number, will return only that exact factoid. If '--raw' option is\n" " given, no variable substitution will take place on the factoid.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] []\n" "\n" "Regarde la valeur de la dans la base de données de factoids. Si un " " est donné, retourne la factoid exacte. Si l'option --raw est " "donnée, aucune substitution de variable ne sera effectuée avant d'afficher " "la factoid. n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:469 plugin.py:674 msgid "key id" msgstr "id de clef" #: plugin.py:482 msgid "" "[] []\n" "\n" " Adds a new key for factoid associated with .\n" " is only necessary if there's more than one factoid " "associated\n" " with .\n" "\n" " The same action can be accomplished by using the 'learn' function " "with\n" " a new key but an existing (verbatim) factoid content.\n" " " msgstr "" "[] []\n" "\n" "Ajoute une à la factoid correspondant à l'. " "Le n'est nécessaire que si il y a plus d'une factoid associée à " "l'.La même action peut être accomplie en utilisant la " "fonction 'learn' avec la , sans le contenu actuel de la " "factoid." #: plugin.py:529 plugin.py:546 msgid "This key-factoid relationship already exists." msgstr "Cette relation clef-factoid existe déjà." #: plugin.py:537 msgid "" "This key has more than one factoid associated with it, but you have not " "provided a number." msgstr "" "Cette clef a plus d'une factoid associée, mais vous n'avez pas fourni un " "nombre." #: plugin.py:551 msgid "" "[] [--plain] [--alpha] []\n" "\n" " Returns a list of top-ranked factoid keys, sorted by usage count\n" " (rank). If is not provided, the default number of factoid " "keys\n" " returned is set by the rankListLength registry value.\n" "\n" " If --plain option is given, rank numbers and usage counts are not\n" " included in output.\n" "\n" " If --alpha option is given in addition to --plain, keys are sorted\n" " alphabetically, instead of by rank.\n" "\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--plain] [--alpha] []\n" "\n" "Retourne une liste des factoids les plus utilisées. Si le n'est pas " "fourni, il correspond par défaut au nombre de clefs de factoids défini dans " "la clef de registre rankListLength. Si --plain est donné, le numéro des " "rangs et le comptage des utilisations n'est pas inclu dans la sortie. Si --" "alpha est donné, en plus de --plain, les clefs seront triées " "alphabétiquement, au lieu de l'être par leur rang. n'est nécessaire " "que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:596 msgid "" "[] \n" "\n" " Locks the factoid(s) associated with so that they cannot be\n" " removed or added to. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Verrouille la/les factoid(s) associé(e) à la , pour qu'elles ne " "puissent plus être supprimées ou modifiées. n'est nécessaire que si " "le message n'est pas envoyé sur le canal lui-même." #: plugin.py:614 msgid "" "[] \n" "\n" " Unlocks the factoid(s) associated with so that they can be\n" " removed or added to. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Verrouille la/les factoid(s) associé(e) à la , pour qu'elles puissent " "être supprimées ou modifiées. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:653 msgid "" "[] [|*]\n" "\n" " Removes a key-fact relationship for key from the factoids\n" " database. If there is more than one such relationship for this " "key,\n" " a number is necessary to determine which one should be removed.\n" " A * can be used to remove all relationships for .\n" "\n" " If as a result, the key (factoid) remains without any relationships " "to\n" " a factoid (key), it shall be removed from the database.\n" "\n" " is only necessary if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[] [|*]\n" "\n" "Enlève la factoid de la base de données. Si il y a plus d'une factoid " "avec cette clef, un est requis pour déterminer laquelle sera " "supprimée. Un joker * peut être utilisé pour enlever toutes les factoids " "avec cette clef. Si, en tant que résultat, la clef (factoid) n'a plus aucune " "relation avec une autre factoid (clef), elle devrait être supprimée de la " "base de données. n'est nécessaire que si le message n'est pas envoyé " "sur le canal lui-même." #: plugin.py:668 msgid "You have to be at least voiced to remove factoids." msgstr "Vous devez au moins être voice pour supprimer des factoids." #: plugin.py:688 msgid "There is no such factoid." msgstr "Cette factoid n'existe pas." #: plugin.py:698 msgid "Invalid factoid number." msgstr "Numéro de factoid invalide." #: plugin.py:703 msgid "" "%s factoids have that key. Please specify which one to remove, or use * to " "designate all of them." msgstr "" "%s factoids ont cette clef. Veuillez spécifier laquelle vous voulez " "supprimer ou utiliser * pour toutes les désigner." #: plugin.py:711 msgid "" "[]\n" "\n" " Returns random factoids from the database for . \n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne une factoid aléatoire de la base de données pour le canal. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:733 msgid "I couldn't find a factoid." msgstr "Je ne peux trouver une factoid" #: plugin.py:738 msgid "" "[] \n" "\n" " Gives information about the factoid(s) associated with .\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Donne des informations sur la/les factoid(s) associée(s) à la . " " n'est nécessaire que si le message n'est pas envoyé sur le canal lui-" "même." #: plugin.py:764 msgid "#%i was added by %s at %s, and has been recalled %n" msgstr "#%i a été ajouté par %s le %s, et il y a eu %n." #: plugin.py:767 msgid "time" msgstr "rappel" #: plugin.py:777 msgid "" "[] \n" "\n" " Changes the factoid # associated with according to\n" " .\n" " " msgstr "" "[] \n" "\n" "Change la factoid associée à la , en accord avec " "l'. n'est nécessaire que si le message n'est " "pas envoyé sur le canal lui-même." #: plugin.py:791 msgid "I couldn't find any key %q" msgstr "Je ne peux trouver de clef %q" #: plugin.py:806 #, fuzzy msgid "" "[] [--values] [--regexp ] [--author ] " "[ ...]\n" "\n" " Searches the keyspace for keys matching . If --regexp is " "given,\n" " its associated value is taken as a regexp and matched against the " "keys.\n" " If --values is given, search the value space instead of the " "keyspace.\n" " " msgstr "" "[] [--values] [--{regexp} ] [...]\n" "\n" "Recherche les clefs correspondant au . Si --regexp est donné, " "recherche les clefs correspondantes à l'. Si --values " "est donné, recherche parmi les valeurs, plutôt que parmi les clefs. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:854 plugin.py:864 msgid "No keys matched that query." msgstr "Aucune clef ne correspond à cette requête." #: plugin.py:860 plugin.py:870 msgid "More than 100 keys matched that query; please narrow your query." msgstr "" "Plus de 100 clefs correspondent à votre requête ; veuillez la préciser." #~ msgid "$key could be $value." #~ msgstr "$key semble être $value." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/locales/it.po0000644000175000017500000003324214535072470017737 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-18 13:05+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:46 msgid "" "Value must include $value, otherwise the factoid's value would be left\n" " out." msgstr "" "Il valore deve includere $value, altrimenti il valore del factoid non viene " "mostrato." #: config.py:53 msgid "" "Determines whether the Factoids plugins will\n" " be browsable on the HTTP server." msgstr "" #: config.py:56 msgid "" "Determines whether factoids can be displayed\n" " via the web server." msgstr "" #: config.py:60 msgid "" "Only allows a user with voice or above on a\n" " channel to use the 'learn' and 'forget' commands." msgstr "" #: config.py:63 #, fuzzy msgid "" "Determines what separator must be used in \n" " the learn command. Defaults to 'is' -- learn is . \n" " Users might want to change this to something else, so it's\n" " configurable." msgstr "" "Determina quale separatore utilizzare nel comando \"learn\". Il predefinito " "è\n" " \"as\": learn as . Gli utenti possono trovare più " "comodo\n" " \"is\" o altro, per cui è configurabile." #: config.py:68 msgid "" "Determines whether the bot will reply with the\n" " single matching factoid if only one factoid matches when using the " "search\n" " command." msgstr "" "Determina se, usando il comando \"search\", il bot mosterà il contenuto del\n" " factoid se ne trova uno solo corrispondente." #: config.py:72 msgid "" "Determines whether the bot will reply to invalid\n" " commands by searching for a factoid; basically making the whatis\n" " unnecessary when you want all factoids for a given key." msgstr "" "Determina se il bot risponderà a comandi non validi cercando un factoid;\n" " fondamentalmente rende il comando \"whatis\" non necessario quando si\n" " vogliono tutti i factoid per una chiave specifica." #: config.py:76 msgid "" "If you try to look up a nonexistent factoid,\n" " this setting make the bot try to find some possible matching keys " "through\n" " several approximate matching algorithms and return a list of matching " "keys,\n" " before giving up." msgstr "" "Richiamando un factoid non esistente, con questa opzione il bot fa una " "ricerca\n" " di possibili chiavi corrispondenti tramite vari algoritmi di " "approssimazione\n" " e restituisce un elenco di chiavi simili." #: config.py:81 msgid "$value" msgstr "" #: config.py:81 msgid "" "Determines the format of\n" " the response given when a factoid's value is requested. All the " "standard\n" " substitutes apply, in addition to \"$key\" for the factoid's key and " "\"$value\"\n" " for the factoid's value." msgstr "" "Determina il formato delle risposte date quando viene richiesto il valore di " "un\n" " factoid. In aggiunta a \"$key\" per la chiave e \"$value\" per il " "valore,\n" " sono applicati tutti gli standard di sostituzione." #: plugin.py:111 #, fuzzy msgid "key" msgstr "ID chiave" #: plugin.py:112 msgid "id" msgstr "" #: plugin.py:113 msgid "fact" msgstr "" #: plugin.py:195 msgid "Provides the ability to show Factoids." msgstr "" #: plugin.py:288 msgid "You have to be at least voiced to teach factoids." msgstr "" #: plugin.py:325 msgid "" "[] %s \n" "\n" " Associates with . is only\n" " necessary if the message isn't sent on the channel\n" " itself. The word '%s' is necessary to separate " "the\n" " key from the value. It can be changed to another " "word\n" " via the learnSeparator registry value.\n" " " msgstr "" "[] %s \n" "\n" " Associa a . è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso. La parola \"%s\" è necessaria " "per\n" " separare il valore della chiave; può essere modificata con una " "diversa\n" " tramite la voce di configurazione \"learnSeparator\".\n" " " #: plugin.py:345 msgid "" "Try to typo-match input to possible factoids.\n" " \n" " Assume first letter is correct, to reduce processing time. \n" " First, try a simple wildcard search.\n" " If that fails, use the Damerau-Levenshtein edit-distance metric.\n" " " msgstr "" #: plugin.py:403 plugin.py:534 msgid "That's not a valid number for that key." msgstr "Non è un numero valido per questa chiave." #: plugin.py:426 plugin.py:520 plugin.py:749 msgid "No factoid matches that key." msgstr "Nessun factoid corrisponde a questa chiave." #: plugin.py:452 msgid "" "[] [--raw] []\n" "\n" " Looks up the value of in the factoid database. If given a\n" " number, will return only that exact factoid. If '--raw' option is\n" " given, no variable substitution will take place on the factoid.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--raw] []\n" "\n" " Cerca il valore di nel database dei factoid. Fornendo un " "numero\n" " restituirà solo l'esatto factoid; se l'opzione --raw è specificata " "non\n" " verrà effettuata alcuna sostituzione di variabile. è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso. " #: plugin.py:469 plugin.py:674 msgid "key id" msgstr "ID chiave" #: plugin.py:482 msgid "" "[] []\n" "\n" " Adds a new key for factoid associated with .\n" " is only necessary if there's more than one factoid " "associated\n" " with .\n" "\n" " The same action can be accomplished by using the 'learn' function " "with\n" " a new key but an existing (verbatim) factoid content.\n" " " msgstr "" "[] []\n" "\n" " Aggiunge al factoid associato a .\n" " è necessario solo se c'è più di un factoid associato a " ".\n" "\n" " La stessa cosa può essere ottenuta utilizzando la funzione \"learn" "\"\n" " con una nuova chiave su un factoid esistente.\n" " " #: plugin.py:529 plugin.py:546 msgid "This key-factoid relationship already exists." msgstr "Questa relazione chiave-factoid esiste già." #: plugin.py:537 msgid "" "This key has more than one factoid associated with it, but you have not " "provided a number." msgstr "" "Questa chiave ha più di un factoid associato ma non è stato fornito un " "numero." #: plugin.py:551 msgid "" "[] [--plain] [--alpha] []\n" "\n" " Returns a list of top-ranked factoid keys, sorted by usage count\n" " (rank). If is not provided, the default number of factoid " "keys\n" " returned is set by the rankListLength registry value.\n" "\n" " If --plain option is given, rank numbers and usage counts are not\n" " included in output.\n" "\n" " If --alpha option is given in addition to --plain, keys are sorted\n" " alphabetically, instead of by rank.\n" "\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] [--plain] [--alpha] []\n" "\n" " Riporta un elenco dei factoid più utilizzati. Se non è " "fornito,\n" " corrisponde al numero di chiavi definite dalla voce di registro " "rankListLength.\n" "\n" " Se l'opzione --plain è specificata, i numeri di classifica e il " "conteggio\n" " dell'uso non verranno inclusi nel risultato.\n" "\n" " Se l'opzione --alpha è specificata in aggiunta a --plain, le chiavi " "saranno\n" " ordinate alfabeticamente anziché per numero di classifica.\n" "\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:596 msgid "" "[] \n" "\n" " Locks the factoid(s) associated with so that they cannot be\n" " removed or added to. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Blocca i factoid associati a in modo che non sia possibile " "rimuoverne o\n" " aggiungerne. è necessario solo se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:614 msgid "" "[] \n" "\n" " Unlocks the factoid(s) associated with so that they can be\n" " removed or added to. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Sblocca i factoid associati a in modo che sia possibile " "rimuoverne o\n" " aggiungerne. è necessario solo se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:653 msgid "" "[] [|*]\n" "\n" " Removes a key-fact relationship for key from the factoids\n" " database. If there is more than one such relationship for this " "key,\n" " a number is necessary to determine which one should be removed.\n" " A * can be used to remove all relationships for .\n" "\n" " If as a result, the key (factoid) remains without any relationships " "to\n" " a factoid (key), it shall be removed from the database.\n" "\n" " is only necessary if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[] [|*]\n" "\n" " Rimuove una relazione chiave-factoid di dal database.\n" " Se è presnete più di una associazione, è necessario un numero per\n" " determinare quale vada rimossa. Un * (asterisco) può essere " "utilizzato\n" " per rimuovere tutte le relazioni di .\n" "\n" " Se in seguito a questa operazione la chiave (factoid) rimane senza " "una\n" " relazione con un factoid (chiave), questa andrà rimossa dal " "database.\n" "\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:668 msgid "You have to be at least voiced to remove factoids." msgstr "" #: plugin.py:688 msgid "There is no such factoid." msgstr "Non c'è alcun factoid." #: plugin.py:698 msgid "Invalid factoid number." msgstr "Numero di factoid non valido." #: plugin.py:703 msgid "" "%s factoids have that key. Please specify which one to remove, or use * to " "designate all of them." msgstr "" "%s factoid hanno questa chiave; specifica quale desideri rimuovere o " "utilizza * per definirli tutti." #: plugin.py:711 #, fuzzy msgid "" "[]\n" "\n" " Returns random factoids from the database for . \n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Restituisce un factoid casuale dal database di . è\n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:733 msgid "I couldn't find a factoid." msgstr "Impossibile trovare un factoid." #: plugin.py:738 msgid "" "[] \n" "\n" " Gives information about the factoid(s) associated with .\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Fornisce informazioni sui factoid associati a . è\n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:764 msgid "#%i was added by %s at %s, and has been recalled %n" msgstr "#%i è stato aggiunto da %s il %s e richiamato %n" #: plugin.py:767 msgid "time" msgstr "volte" #: plugin.py:777 msgid "" "[] \n" "\n" " Changes the factoid # associated with according to\n" " .\n" " " msgstr "" "[] \n" "\n" " Modifica il del factoid associato a in base a " ".\n" " " #: plugin.py:791 msgid "I couldn't find any key %q" msgstr "Non trovo nessuna chiave %q" #: plugin.py:806 #, fuzzy msgid "" "[] [--values] [--regexp ] [--author ] " "[ ...]\n" "\n" " Searches the keyspace for keys matching . If --regexp is " "given,\n" " its associated value is taken as a regexp and matched against the " "keys.\n" " If --values is given, search the value space instead of the " "keyspace.\n" " " msgstr "" "[] [--values] [--{regexp} ] [ ...]\n" "\n" " Cerca le chiavi corrispondenti a . Se --regexp è " "fornita,\n" " ricerca le chiavi corrispondenti all'espressione regolare. Se --" "values è\n" " specificato, cerca i valori piuttosto che le chiavi.\n" " " #: plugin.py:854 plugin.py:864 msgid "No keys matched that query." msgstr "Nessuna chiave corrisponde a questa richiesta." #: plugin.py:860 plugin.py:870 msgid "More than 100 keys matched that query; please narrow your query." msgstr "" "A questa richiesta corrispondono più di 100 chiavi, restringi la ricerca." #~ msgid "$key could be $value." #~ msgstr "$key potrebbe essere $value." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/plugin.py0000644000175000017500000011124314535072470017207 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, 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 sys import time import string 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.utils.minisix as minisix import supybot.ircutils as ircutils import supybot.callbacks as callbacks import supybot.httpserver as httpserver from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Factoids') import sqlite3 import re from supybot.utils.seq import dameraulevenshtein def getFactoid(irc, msg, args, state): assert not state.channel callConverter('channel', irc, msg, args, state) separator = state.cb.registryValue('learnSeparator', state.channel, irc.network) try: i = args.index(separator) except ValueError: raise callbacks.ArgumentError args.pop(i) key = [] value = [] for (j, s) in enumerate(args[:]): if j < i: key.append(args.pop(0)) else: value.append(args.pop(0)) if not key or not value: raise callbacks.ArgumentError state.args.append(' '.join(key)) state.args.append(' '.join(value)) def getFactoidId(irc, msg, args, state): Type = 'key id' p = lambda i: i > 0 callConverter('int', irc, msg, args, state, Type, p) addConverter('factoid', getFactoid) addConverter('factoidId', getFactoidId) PAGE_SKELETON = """\ Factoids %s """ DEFAULT_TEMPLATES = { 'factoids/index.html': PAGE_SKELETON % """\

Factoids

""", 'factoids/channel.html': PAGE_SKELETON % """\

Factoids of %(channel)s

%(rows)s
""" + _('key') + """ """ + _('id') + """ """ + _('fact') + """
""" } httpserver.set_default_templates(DEFAULT_TEMPLATES) class FactoidsCallback(httpserver.SupyHTTPServerCallback): name = 'Factoids web interface' def doGetOrHead(self, handler, path, write_content): parts = path.split('/')[1:] if path == '/': self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() self.write(httpserver.get_template('factoids/index.html')) elif len(parts) == 2: channel = utils.web.urlunquote(parts[0]) if not ircutils.isChannel(channel): self.send_response(404) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() if write_content: self.write(httpserver.get_template('generic/error.html')% {'title': 'Factoids - not a channel', 'error': 'This is not a channel'}) return if not self._plugin.registryValue('web.channel', channel): self.send_response(403) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() if write_content: self.write(httpserver.get_template('generic/error.html')% {'title': 'Factoids - unavailable', 'error': 'This channel does not exist or its factoids ' 'are not available here.'}) return db = self._plugin.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT keys.key, factoids.id, factoids.fact FROM factoids, keys, relations WHERE relations.key_id=keys.id AND relations.fact_id=factoids.id """) factoids = {} for (key, id_, fact) in cursor.fetchall(): if key not in factoids: factoids[key] = {} factoids[key][id_] = fact content = '' keys = sorted(factoids.keys()) for key in keys: facts = factoids[key] content += '' content += ('' '
%s' '') % (len(facts), key, key, key) for id_, fact in facts.items(): content += '%i' % id_ content += '%s' % fact content += '' content = content[:-len('')] self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() if write_content: self.write(httpserver.get_template('factoids/channel.html')% {'channel': channel, 'rows': content}) def doPost(self, handler, path, form): if 'chan' in form: self.send_response(303) self.send_header('Location', './%s/' % utils.web.urlquote(form['chan'].value)) self.end_headers() else: self.send_response(400) self.send_header('Content-type', 'text/plain; charset=utf-8') self.end_headers() self.write('Missing field \'chan\'.') class Factoids(callbacks.Plugin, plugins.ChannelDBHandler): """Provides the ability to show Factoids.""" def __init__(self, irc): callbacks.Plugin.__init__(self, irc) plugins.ChannelDBHandler.__init__(self) self._http_running = False conf.supybot.plugins.Factoids.web.enable.addCallback(self._doHttpConf) if self.registryValue('web.enable'): self._startHttp() def _doHttpConf(self, *args, **kwargs): if self.registryValue('web.enable'): if not self._http_running: self._startHttp() else: if self._http_running: self._stopHttp() def _startHttp(self): callback = FactoidsCallback() callback._plugin = self httpserver.hook('factoids', callback) self._http_running = True def _stopHttp(self): httpserver.unhook('factoids') self._http_running = False def die(self): if self.registryValue('web.enable'): self._stopHttp() super(self.__class__, self).die() def makeDb(self, filename): if os.path.exists(filename): db = sqlite3.connect(filename) if minisix.PY2: db.text_factory = str return db db = sqlite3.connect(filename) if minisix.PY2: db.text_factory = str cursor = db.cursor() cursor.execute("""CREATE TABLE keys ( id INTEGER PRIMARY KEY, key TEXT UNIQUE ON CONFLICT REPLACE )""") cursor.execute("""CREATE TABLE factoids ( id INTEGER PRIMARY KEY, added_by TEXT, added_at TIMESTAMP, fact TEXT UNIQUE ON CONFLICT REPLACE, locked BOOLEAN )""") cursor.execute("""CREATE TABLE relations ( id INTEGER PRIMARY KEY, key_id INTEGER, fact_id INTEGER, usage_count INTEGER )""") db.commit() return db def getCommandHelp(self, command, simpleSyntax=None): method = self.getCommandMethod(command) if method.__func__.__name__ == 'learn': chan = None network = None if dynamic.msg is not None: chan = dynamic.msg.channel if dynamic.irc is not None: network = dynamic.irc.network s = self.registryValue('learnSeparator', chan, network) help = callbacks.getHelp if simpleSyntax is None: irc = dynamic.irc network = irc.network if irc else None simpleSyntax = conf.supybot.reply.showSimpleSyntax.getSpecific( network, chan)() if simpleSyntax: help = callbacks.getSyntax return help(method, doc=method._fake__doc__ % (s, s), name=callbacks.formatCommand(command)) return super(Factoids, self).getCommandHelp(command, simpleSyntax) def _getKeyAndFactId(self, channel, key, factoid): db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id FROM keys WHERE key=?", (key,)) keyresults = cursor.fetchall() cursor.execute("SELECT id FROM factoids WHERE fact=?", (factoid,)) factresults = cursor.fetchall() return (keyresults, factresults,) def learn(self, irc, msg, args, channel, key, factoid): if self.registryValue('requireVoice', channel, irc.network) and \ not irc.state.channels[channel].isVoicePlus(msg.nick): irc.error(_('You have to be at least voiced to teach factoids.'), Raise=True) # if neither key nor factoid exist, add them. # if key exists but factoid doesn't, add factoid, link it to existing key # if factoid exists but key doesn't, add key, link it to existing factoid # if both key and factoid already exist, and are linked, do nothing, print nice message db = self.getDb(channel) cursor = db.cursor() (keyid, factid) = self._getKeyAndFactId(channel, key, factoid) if len(keyid) == 0: cursor.execute("""INSERT INTO keys VALUES (NULL, ?)""", (key,)) db.commit() if len(factid) == 0: if ircdb.users.hasUser(msg.prefix): name = ircdb.users.getUser(msg.prefix).name else: name = msg.nick cursor.execute("""INSERT INTO factoids VALUES (NULL, ?, ?, ?, ?)""", (name, int(time.time()), factoid, 0)) db.commit() (keyid, factid) = self._getKeyAndFactId(channel, key, factoid) cursor.execute("""SELECT id, key_id, fact_id from relations WHERE key_id=? AND fact_id=?""", (keyid[0][0], factid[0][0],)) existingrelation = cursor.fetchall() if len(existingrelation) == 0: cursor.execute("""INSERT INTO relations VALUES (NULL, ?, ?, ?)""", (keyid[0][0],factid[0][0],0,)) db.commit() irc.replySuccess() else: irc.error("This key-factoid relationship already exists.") learn = wrap(learn, ['factoid'], checkDoc=False) learn._fake__doc__ = _("""[] %s Associates with . is only necessary if the message isn't sent on the channel itself. The word '%s' is necessary to separate the key from the value. It can be changed to another word via the learnSeparator registry value. """) def _lookupFactoid(self, channel, key): db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT factoids.fact, factoids.id, relations.id FROM factoids, keys, relations WHERE keys.key LIKE ? AND relations.key_id=keys.id AND relations.fact_id=factoids.id ORDER BY factoids.id LIMIT 20""", (key,)) return cursor.fetchall() def _searchFactoid(self, channel, key): """Try to typo-match input to possible factoids. Assume first letter is correct, to reduce processing time. First, try a simple wildcard search. If that fails, use the Damerau-Levenshtein edit-distance metric. """ # if you made a typo in a two-character key, boo on you. if len(key) < 3: return [] db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT key FROM keys WHERE key LIKE ?""", ('%' + key + '%',)) wildcardkeys = cursor.fetchall() if len(wildcardkeys) > 0: return [line[0] for line in wildcardkeys] cursor.execute("""SELECT key FROM keys WHERE key LIKE ?""", (key[0] + '%',)) flkeys = cursor.fetchall() if len(flkeys) == 0: return [] flkeys = [line[0] for line in flkeys] dl_metrics = [dameraulevenshtein(key, sourcekey) for sourcekey in flkeys] dict_metrics = dict(list(zip(flkeys, dl_metrics))) if min(dl_metrics) <= 2: return [key for key,item in dict_metrics.items() if item <= 2] if min(dl_metrics) <= 3: return [key for key,item in dict_metrics.items() if item <= 3] return [] def _updateRank(self, network, channel, factoids): if self.registryValue('keepRankInfo', channel, network): db = self.getDb(channel) cursor = db.cursor() for (fact,factid,relationid) in factoids: cursor.execute("""SELECT relations.usage_count FROM relations WHERE relations.id=?""", (relationid,)) old_count = cursor.fetchall()[0][0] cursor.execute("UPDATE relations SET usage_count=? WHERE id=?", (old_count + 1, relationid,)) db.commit() def _replyFactoids(self, irc, msg, key, channel, factoids, number=0, error=True, raw=False): def format_fact(text): if raw: return text else: return ircutils.standardSubstitute(irc, msg, text) if factoids: if number: try: irc.reply(format_fact(factoids[number-1][0])) self._updateRank(irc.network, channel, [factoids[number-1]]) except IndexError: irc.error(_('That\'s not a valid number for that key.')) return else: env = {'key': key} def prefixer(v): env['value'] = v formatter = self.registryValue('format', msg.channel, irc.network) return ircutils.standardSubstitute(irc, msg, formatter, env) if len(factoids) == 1: irc.reply(format_fact(prefixer(factoids[0][0]))) else: factoidsS = [] counter = 1 for factoid in factoids: factoidsS.append(format('(#%i) %s', counter, format_fact(factoid[0]))) counter += 1 irc.replies(factoidsS, prefixer=prefixer, joiner=', or ', onlyPrefixFirst=True) self._updateRank(irc.network, channel, factoids) elif error: irc.error(_('No factoid matches that key.')) def _replyApproximateFactoids(self, irc, msg, channel, key, error=True): if self.registryValue('replyApproximateSearchKeys'): factoids = self._searchFactoid(channel, key) factoids.sort() if factoids: keylist = ["'%s'" % (fact,) for fact in factoids] keylist = ', '.join(keylist) irc.reply("I do not know about '%s', but I do know about these similar topics: %s" % (key, keylist)) elif error: irc.error('No factoid matches that key.') def invalidCommand(self, irc, msg, tokens): channel = msg.channel if channel: if self.registryValue('replyWhenInvalidCommand', channel, irc.network): key = ' '.join(tokens) factoids = self._lookupFactoid(channel, key) if factoids: self._replyFactoids(irc, msg, key, channel, factoids, error=False) else: self._replyApproximateFactoids(irc, msg, channel, key, error=False) @internationalizeDocstring def whatis(self, irc, msg, args, channel, optlist, words): """[] [--raw] [] Looks up the value of in the factoid database. If given a number, will return only that exact factoid. If '--raw' option is given, no variable substitution will take place on the factoid. is only necessary if the message isn't sent in the channel itself. """ raw = False for (option, arg) in optlist: if option == 'raw': raw = True number = None if len(words) > 1: if words[-1].isdigit(): number = int(words.pop()) if number <= 0: irc.errorInvalid(_('key id')) key = ' '.join(words) factoids = self._lookupFactoid(channel, key) if factoids: self._replyFactoids(irc, msg, key, channel, factoids, number, raw=raw) else: self._replyApproximateFactoids(irc, msg, channel, key) whatis = wrap(whatis, ['channel', getopts({'raw': '',}), many('something')]) @internationalizeDocstring def alias(self, irc, msg, args, channel, oldkey, newkey, number): """[] [] Adds a new key for factoid associated with . is only necessary if there's more than one factoid associated with . The same action can be accomplished by using the 'learn' function with a new key but an existing (verbatim) factoid content. """ def _getNewKey(channel, newkey, arelation): db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT id FROM keys WHERE key=?""", (newkey,)) newkey_info = cursor.fetchall() if len(newkey_info) == 1: # check if we already have the requested relation cursor.execute("""SELECT id FROM relations WHERE key_id=? and fact_id=?""", (arelation[1], arelation[2])) existentrelation = cursor.fetchall() if len(existentrelation) != 0: newkey_info = False if len(newkey_info) == 0: cursor.execute("""INSERT INTO keys VALUES (NULL, ?)""", (newkey,)) db.commit() cursor.execute("""SELECT id FROM keys WHERE key=?""", (newkey,)) newkey_info = cursor.fetchall() return newkey_info db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT relations.id, relations.key_id, relations.fact_id FROM keys, relations WHERE keys.key=? AND relations.key_id=keys.id""", (oldkey,)) results = cursor.fetchall() if len(results) == 0: irc.error(_('No factoid matches that key.')) return elif len(results) == 1: newkey_info = _getNewKey(channel, newkey, results[0]) if newkey_info is not False: cursor.execute("""INSERT INTO relations VALUES(NULL, ?, ?, ?)""", (newkey_info[0][0], results[0][2], 0,)) irc.replySuccess() else: irc.error(_('This key-factoid relationship already exists.')) elif len(results) > 1: try: arelation = results[number-1] except IndexError: irc.error(_("That's not a valid number for that key.")) return except TypeError: irc.error(_("This key has more than one factoid associated " "with it, but you have not provided a number.")) return newkey_info = _getNewKey(channel, newkey, arelation) if newkey_info is not False: cursor.execute("""INSERT INTO relations VALUES(NULL, ?, ?, ?)""", (newkey_info[0][0], arelation[2], 0,)) irc.replySuccess() else: irc.error(_('This key-factoid relationship already exists.')) alias = wrap(alias, ['channel', 'something', 'something', optional('int')]) @internationalizeDocstring def rank(self, irc, msg, args, channel, optlist, number): """[] [--plain] [--alpha] [] Returns a list of top-ranked factoid keys, sorted by usage count (rank). If is not provided, the default number of factoid keys returned is set by the rankListLength registry value. If --plain option is given, rank numbers and usage counts are not included in output. If --alpha option is given in addition to --plain, keys are sorted alphabetically, instead of by rank. is only necessary if the message isn't sent in the channel itself. """ if not number: number = self.registryValue('rankListLength', channel, irc.network) db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT keys.key, relations.usage_count FROM keys, relations WHERE relations.key_id=keys.id ORDER BY relations.usage_count DESC LIMIT ?""", (number,)) factkeys = cursor.fetchall() plain=False alpha=False for (option, arg) in optlist: if option == 'plain': plain = True elif option =='alpha': alpha = True if plain: s = [ "%s" % (key[0],) for i, key in enumerate(factkeys) ] if alpha: s.sort() else: s = [ "#%d %s (%d)" % (i+1, key[0], key[1]) for i, key in enumerate(factkeys) ] irc.reply(", ".join(s)) rank = wrap(rank, ['channel', getopts({'plain': '', 'alpha': '',}), optional('int')]) @internationalizeDocstring def lock(self, irc, msg, args, channel, key): """[] Locks the factoid(s) associated with so that they cannot be removed or added to. is only necessary if the message isn't sent in the channel itself. """ db = self.getDb(channel) cursor = db.cursor() cursor.execute("UPDATE factoids " "SET locked=1 WHERE factoids.id IN " "(SELECT fact_id FROM relations WHERE key_id IN " "(SELECT id FROM keys WHERE key LIKE ?));", (key,)) db.commit() irc.replySuccess() lock = wrap(lock, ['channel', 'text']) @internationalizeDocstring def unlock(self, irc, msg, args, channel, key): """[] Unlocks the factoid(s) associated with so that they can be removed or added to. is only necessary if the message isn't sent in the channel itself. """ db = self.getDb(channel) cursor = db.cursor() cursor.execute("UPDATE factoids " "SET locked=0 WHERE factoids.id IN " "(SELECT fact_id FROM relations WHERE key_id IN " "(SELECT id FROM keys WHERE key LIKE ?));", (key,)) db.commit() irc.replySuccess() unlock = wrap(unlock, ['channel', 'text']) def _deleteRelation(self, channel, relationlist): db = self.getDb(channel) cursor = db.cursor() for (keyid, factid, relationid) in relationlist: cursor.execute("""DELETE FROM relations where relations.id=?""", (relationid,)) db.commit() cursor.execute("""SELECT id FROM relations WHERE relations.key_id=?""", (keyid,)) remaining_key_relations = cursor.fetchall() if len(remaining_key_relations) == 0: cursor.execute("""DELETE FROM keys where id=?""", (keyid,)) cursor.execute("""SELECT id FROM relations WHERE relations.fact_id=?""", (factid,)) remaining_fact_relations = cursor.fetchall() if len(remaining_fact_relations) == 0: cursor.execute("""DELETE FROM factoids where id=?""", (factid,)) db.commit() @internationalizeDocstring def forget(self, irc, msg, args, channel, words): """[] [|*] Removes a key-fact relationship for key from the factoids database. If there is more than one such relationship for this key, a number is necessary to determine which one should be removed. A * can be used to remove all relationships for . If as a result, the key (factoid) remains without any relationships to a factoid (key), it shall be removed from the database. is only necessary if the message isn't sent in the channel itself. """ if self.registryValue('requireVoice', channel, irc.network) and \ not irc.state.channels[channel].isVoicePlus(msg.nick): irc.error(_('You have to be at least voiced to remove factoids.'), Raise=True) number = None if len(words) > 1: if words[-1].isdigit(): number = int(words.pop()) if number <= 0: irc.errorInvalid(_('key id')) elif words[-1] == '*': words.pop() number = True key = ' '.join(words) db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT keys.id, factoids.id, relations.id FROM keys, factoids, relations WHERE key LIKE ? AND relations.key_id=keys.id AND relations.fact_id=factoids.id""", (key,)) results = cursor.fetchall() if len(results) == 0: irc.error(_('There is no such factoid.')) elif len(results) == 1 or number is True: self._deleteRelation(channel, results) irc.replySuccess() else: if number is not None: #results = cursor.fetchall() try: arelation = results[number-1] except IndexError: irc.error(_('Invalid factoid number.')) return self._deleteRelation(channel, [arelation,]) irc.replySuccess() else: irc.error(_('%s factoids have that key. ' 'Please specify which one to remove, ' 'or use * to designate all of them.') % len(results)) forget = wrap(forget, ['channel', many('something')]) @internationalizeDocstring def random(self, irc, msg, args, channel): """[] Returns random factoids from the database for . is only necessary if the message isn't sent in the channel itself. """ db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT id, key_id, fact_id FROM relations ORDER BY random() LIMIT 3""") results = cursor.fetchall() if len(results) != 0: L = [] for (relationid, keyid, factid) in results: cursor.execute("""SELECT keys.key, factoids.fact FROM keys, factoids WHERE factoids.id=? AND keys.id=?""", (factid,keyid,)) (key,factoid) = cursor.fetchall()[0] L.append('"%s": %s' % (ircutils.bold(key), factoid)) irc.reply('; '.join(L)) else: irc.error(_('I couldn\'t find a factoid.')) random = wrap(random, ['channel']) @internationalizeDocstring def info(self, irc, msg, args, channel, key): """[] Gives information about the factoid(s) associated with . is only necessary if the message isn't sent in the channel itself. """ db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id FROM keys WHERE key LIKE ?", (key,)) results = cursor.fetchall() if len(results) == 0: irc.error(_('No factoid matches that key.')) return id = results[0][0] cursor.execute("""SELECT factoids.added_by, factoids.added_at, factoids.locked, relations.usage_count FROM factoids, relations WHERE relations.key_id=? AND relations.fact_id=factoids.id ORDER BY relations.id""", (id,)) factoids = cursor.fetchall() L = [] counter = 0 for (added_by, added_at, locked, usage_count) in factoids: counter += 1 added_at = time.strftime(conf.supybot.reply.format.time(), time.localtime(int(added_at))) L.append(format(_('#%i was added by %s at %s, and has been ' 'recalled %n'), counter, added_by, added_at, (usage_count, _('time')))) factoids = '; '.join(L) s = format('Key %q is %s and has %n associated with it: %s', key, locked and 'locked' or 'not locked', (counter, 'factoid'), factoids) irc.reply(s) info = wrap(info, ['channel', 'text']) @internationalizeDocstring def change(self, irc, msg, args, channel, key, number, replacer): """[] Changes the factoid # associated with according to . """ db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT factoids.id, factoids.fact FROM keys, factoids, relations WHERE keys.key LIKE ? AND keys.id=relations.key_id AND factoids.id=relations.fact_id""", (key,)) results = cursor.fetchall() if len(results) == 0: irc.error(format(_('I couldn\'t find any key %q'), key)) return elif len(results) < number: irc.errorInvalid('key id') (id, fact) = results[number-1] newfact = replacer(fact) cursor.execute("UPDATE factoids SET fact=? WHERE id=?", (newfact, id)) db.commit() irc.replySuccess() change = wrap(change, ['channel', 'something', 'factoidId', 'regexpReplacer']) _sqlTrans = utils.str.MultipleReplacer({'*': '%', '?': '_'}) @internationalizeDocstring def search(self, irc, msg, args, channel, optlist, globs): """[] [--values] [--regexp ] [--author ] [ ...] Searches the keyspace for keys matching . If --regexp is given, its associated value is taken as a regexp and matched against the keys. If --values is given, search the value space instead of the keyspace. """ if not optlist and not globs: raise callbacks.ArgumentError tables = ['keys'] join_factoids = False formats = [] criteria = [] target = 'keys.key' predicateName = 'p' db = self.getDb(channel) for (option, arg) in optlist: if option == 'values': target = 'factoids.fact' join_factoids = True elif option == 'regexp': criteria.append('%s(TARGET)' % predicateName) def p(s, r=arg): return int(bool(r.search(s))) db.create_function(predicateName, 1, p) predicateName += 'p' elif option == 'author': join_factoids = True criteria.append('factoids.added_by=?') formats.append(arg) for glob in globs: criteria.append('TARGET LIKE ?') formats.append(self._sqlTrans(glob)) if join_factoids: if 'factoids' not in tables: tables.append('factoids') tables.append('relations') criteria.append( 'factoids.id=relations.fact_id AND keys.id=relations.key_id' ) cursor = db.cursor() sql = """SELECT keys.key FROM %s WHERE %s""" % \ (', '.join(tables), ' AND '.join(criteria)) sql = sql + " ORDER BY keys.key" sql = sql.replace('TARGET', target) cursor.execute(sql, formats) if cursor.rowcount == 0: irc.reply(_('No keys matched that query.')) elif cursor.rowcount == 1 and \ self.registryValue('showFactoidIfOnlyOneMatch', channel, irc.network): self.whatis(irc, msg, [channel, cursor.fetchone()[0]]) elif cursor.rowcount > 100: irc.reply(_('More than 100 keys matched that query; ' 'please narrow your query.')) results = cursor.fetchall() if len(results) == 0: irc.reply(_('No keys matched that query.')) elif len(results) == 1 and \ self.registryValue('showFactoidIfOnlyOneMatch', channel, irc.network): self.whatis(irc, msg, [channel, results[0][0]]) elif len(results) > 100: irc.reply(_('More than 100 keys matched that query; ' 'please narrow your query.')) else: keys = [repr(t[0]) for t in results] s = format('%L', keys) irc.reply(s) search = wrap(search, ['channel', getopts({ 'values': '', 'regexp': 'regexpMatcher', 'author': 'somethingWithoutSpaces', }), any('glob')]) Class = Factoids # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Factoids/test.py0000644000175000017500000002446014535072470016674 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, 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 * import supybot.conf as conf import sqlite3 class FactoidsTestCase(ChannelPluginTestCase): plugins = ('Factoids',) def testRandomfactoid(self): self.assertError('random') self.assertNotError('learn jemfinch is my primary author') self.assertRegexp('random', 'primary author') def testLearn(self): self.assertError('learn is my primary author') self.assertError('learn jemfinch is') self.assertNotError('learn jemfinch is my primary author') self.assertNotError('info jemfinch') self.assertRegexp('whatis jemfinch', 'my primary author') self.assertRegexp('whatis JEMFINCH', 'my primary author') self.assertRegexp('whatis JEMFINCH 1', 'my primary author') self.assertNotError('learn jemfinch is a bad assembly programmer') self.assertRegexp('whatis jemfinch 2', 'bad assembly') self.assertNotRegexp('whatis jemfinch 2', 'primary author') self.assertRegexp('whatis jemfinch', r'.*primary author.*assembly') self.assertError('forget jemfinch') self.assertError('forget jemfinch 3') self.assertError('forget jemfinch 0') self.assertNotError('forget jemfinch 2') self.assertNotError('forget jemfinch 1') self.assertError('whatis jemfinch') self.assertError('info jemfinch') self.assertNotError('learn foo bar is baz') self.assertNotError('info foo bar') self.assertRegexp('whatis foo bar', 'baz') self.assertNotError('learn foo bar is quux') self.assertRegexp('whatis foo bar', '.*baz.*quux') self.assertError('forget foo bar') self.assertNotError('forget foo bar 2') self.assertNotError('forget foo bar 1') self.assertError('whatis foo bar') self.assertError('info foo bar') self.assertError('learn foo bar baz') # No 'is' self.assertError('learn foo bar') # No 'is' with conf.supybot.plugins.Factoids.requireVoice.context(True): self.assertError('learn jemfinch is my primary author') self.irc.feedMsg(ircmsgs.mode(self.channel, args=('+h', self.nick))) self.assertNotError('learn jemfinch is my primary author') def testChangeFactoid(self): self.assertNotError('learn foo is bar') self.assertNotError('change foo 1 s/bar/baz/') self.assertRegexp('whatis foo', 'baz') self.assertError('change foo 2 s/bar/baz/') self.assertError('change foo 0 s/bar/baz/') def testSearchFactoids(self): self.assertNotError('learn jemfinch is my primary author') self.assertNotError('learn strike is a cool person working on me') self.assertNotError('learn inkedmn is another of my developers') self.assertNotError('learn jamessan is jamessan is a developer of much python') self.assertNotError('learn bwp is bwp is author of my weather command') self.assertRegexp('factoids search --regexp /.w./', 'bwp') self.assertRegexp('factoids search --regexp /^.+i/', 'jemfinch.*strike') self.assertNotRegexp('factoids search --regexp /^.+i/', 'inkedmn') self.assertRegexp('factoids search --regexp m/j/ --regexp m/ss/', 'jamessan') self.assertRegexp('factoids search --regexp m/^j/ *ss*', 'jamessan') self.assertRegexp('factoids search --regexp /^j/', 'jamessan.*jemfinch') self.assertRegexp('factoids search j*', 'jamessan.*jemfinch') self.assertRegexp('factoids search *ke*', 'inkedmn.*strike|strike.*inkedmn') self.assertRegexp('factoids search ke', 'inkedmn.*strike|strike.*inkedmn') self.assertRegexp('factoids search jemfinch', 'my primary author') self.assertRegexp('factoids search --values primary author', 'my primary author') self.assertRegexp('factoids search --author test j*', 'jamessan.*jemfinch') self.assertRegexp('factoids search --author blahblah j*', 'No keys matched that query.') def testWhatisOnNumbers(self): self.assertNotError('learn 911 is emergency number') self.assertRegexp('whatis 911', 'emergency number') def testNotZeroIndexed(self): self.assertNotError('learn foo is bar') self.assertNotRegexp('info foo', '#0') self.assertNotRegexp('whatis foo', '#0') self.assertNotError('learn foo is baz') self.assertNotRegexp('info foo', '#0') self.assertNotRegexp('whatis foo', '#0') def testInfoReturnsRightNumber(self): self.assertNotError('learn foo is bar') self.assertNotRegexp('info foo', '2 factoids') def testInfoUsageCount(self): self.assertNotError('learn moo is cow') self.assertRegexp('info moo', 'recalled 0 times') self.assertNotError('whatis moo') self.assertRegexp('info moo', 'recalled 1 time') def testLearnSeparator(self): self.assertError('learn foo as bar') self.assertNotError('learn foo is bar') self.assertRegexp('whatis foo', 'bar') orig = conf.supybot.plugins.Factoids.learnSeparator() try: conf.supybot.plugins.Factoids.learnSeparator.setValue('as') self.assertError('learn bar is baz') self.assertNotError('learn bar as baz') self.assertRegexp('whatis bar', 'baz') finally: conf.supybot.plugins.Factoids.learnSeparator.setValue(orig) def testShowFactoidIfOnlyOneMatch(self): m1 = self.assertNotError('factoids search m/foo|bar/') orig = conf.supybot.plugins.Factoids.showFactoidIfOnlyOneMatch() try: conf.supybot.plugins.Factoids. \ showFactoidIfOnlyOneMatch.setValue(False) m2 = self.assertNotError('factoids search m/foo/') self.assertTrue(m1.args[1].startswith(m2.args[1])) finally: conf.supybot.plugins.Factoids. \ showFactoidIfOnlyOneMatch.setValue(orig) def testInvalidCommand(self): self.assertNotError('learn foo is bar') self.assertRegexp('foo', 'bar') self.assertNotError('learn mooz is cowz') self.assertRegexp('moo', 'mooz') self.assertRegexp('mzo', 'mooz') self.assertRegexp('moz', 'mooz') self.assertNotError('learn moped is pretty fast') self.assertRegexp('moe', 'mooz.*moped') self.assertError('nosuchthing') def testWhatis(self): self.assertNotError('learn foo is bar') self.assertRegexp('whatis foo', 'bar') self.assertRegexp('whatis foob', 'foo') self.assertNotError('learn foob is barb') self.assertRegexp('whatis foom', 'foo.*foob') def testStandardSubstitute(self): self.assertNotError('learn foo is this is $channel, and hour is $hour') self.assertRegexp('whatis foo', r'this is #test, and hour is \d{1,2}') self.assertRegexp('whatis --raw foo', r'this is \$channel, and hour is \$hour') self.assertNotError('learn bar is this is $$channel escaped') self.assertRegexp('whatis bar', r'this is \$channel') self.assertNotError('learn bar is this is $minute') self.assertRegexp('whatis bar', r'\$channel.*\d{1,2}') def testAlias(self): self.assertNotError('learn foo is bar') self.assertNotError('alias foo zoog') self.assertRegexp('whatis zoog', 'bar') self.assertNotError('learn foo is snorp') self.assertError('alias foo gnoop') self.assertNotError('alias foo gnoop 2') self.assertRegexp('whatis gnoop', 'snorp') def testRank(self): self.assertNotError('learn foo is bar') self.assertNotError('learn moo is cow') self.assertRegexp('factoids rank', r'#1 foo \(0\), #2 moo \(0\)') self.assertRegexp('whatis moo', r'.*cow.*') self.assertRegexp('factoids rank', r'#1 moo \(1\), #2 foo \(0\)') self.assertRegexp('factoids rank 1', r'#1 moo \(1\)') self.assertNotRegexp('factoids rank 1', 'foo') self.assertRegexp('factoids rank --plain', 'moo, foo') self.assertRegexp('factoids rank --plain --alpha', 'foo, moo') self.assertResponse('factoids rank --plain 1', 'moo') def testQuoteHandling(self): self.assertNotError('learn foo is "\\"bar\\""') self.assertRegexp('whatis foo', r'"bar"') def testLock(self): self.assertNotError('learn foo is bar') self.assertNotError('lock foo') self.assertNotError('unlock foo') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Fediverse/0000755000175000017500000000000014535072535015517 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Fediverse/__init__.py0000644000175000017500000000471214535072470017632 0ustar00valval### # Copyright (c) 2020-2021, 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. ### """ Fetches information from ActivityPub servers. """ import sys import supybot from supybot import world # Use this for the version of this plugin. __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 importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Fediverse/activitypub.py0000644000175000017500000002176614535072470020446 0ustar00valval### # Copyright (c) 2020-2021, 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 json import email import base64 import functools import contextlib import urllib.error import urllib.parse import xml.etree.ElementTree as ET from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key from supybot import commands, conf from supybot.utils import gen, web XRD_URI = "{http://docs.oasis-open.org/ns/xri/xrd-1.0}" ACTIVITY_MIMETYPE = "application/activity+json" class ActivityPubError(Exception): pass class ProtocolError(ActivityPubError): pass class ActivityPubProtocolError(ActivityPubError): pass class WebfingerError(ProtocolError): pass class ActorNotFound(ActivityPubError): pass def sandbox(f): """Runs a function in a process with limited memory to prevent XML memory bombs """ @functools.wraps(f) def newf(*args, **kwargs): try: return commands.process( f, *args, timeout=10, heap_size=300 * 1024 * 1024, pn="Fediverse", cn=f.__name__, **kwargs ) except (commands.ProcessTimeoutError, MemoryError): raise web.Error( "Page is too big or the server took " "too much time to answer the request." ) return newf @contextlib.contextmanager def convert_exceptions(to_class, msg="", from_none=False): try: yield except Exception as e: arg = msg + str(e) if from_none: raise to_class(arg) from None else: raise to_class(arg) from e @sandbox def _get_webfinger_url(hostname): try: doc = ET.fromstring( web.getUrlContent("https://%s/.well-known/host-meta" % hostname) ) for link in doc.iter(XRD_URI + "Link"): if link.attrib["rel"] == "lrdd": return link.attrib["template"] except web.Error: # Fall back to the default Webfinger URL return "https://%s/.well-known/webfinger?resource={uri}" % hostname def has_webfinger_support(hostname): """Returns whether the hostname probably supports webfinger or not. This relies on an edge case of the Webfinger specification, so it may not successfully detect some hosts because they don't follow the specification.""" request = urllib.request.Request( "https://%s/.well-known/webfinger" % hostname, method="HEAD" ) try: urllib.request.urlopen(request) except urllib.error.HTTPError as e: if e.code == 400: # RFC 7033 requires a 400 response when the "resource" parameter # is missing: https://tools.ietf.org/html/rfc7033#section-4.2 # # This works for: # * Misskey # * PeerTube # * Pleroma return True elif e.headers.get("Content-Type", "") == "application/jrd+json": # WriteFreely, and possibly others. # https://github.com/writeas/writefreely/issues/310 return True elif e.code == 404: if e.headers.get("Server", "").lower() == "mastodon": # https://github.com/tootsuite/mastodon/issues/13757 return True # Else, the host probably doesn't support Webfinger. # Known false negatives: # * Nextcloud (returns 404) # * Pixelfed (returns 302 to the homepage): # https://github.com/pixelfed/pixelfed/issues/2180 # * Plume (returns 404): # https://github.com/Plume-org/Plume/issues/770 return False def webfinger(hostname, uri): template = _get_webfinger_url(hostname) assert template, "missing webfinger url template for %s" % hostname with convert_exceptions(ActorNotFound): content = web.getUrlContent( template.replace("{uri}", uri), headers={"Accept": "application/json"}, ) with convert_exceptions(WebfingerError, "Invalid JSON: ", True): return json.loads(content.decode()) def get_instance_actor_url(): root_url = conf.supybot.servers.http.publicUrl() if not root_url: return None return urllib.parse.urljoin(root_url, "/fediverse/instance_actor") def _generate_private_key(): return generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() ) def _get_private_key(): path = conf.supybot.directories.data.dirize("Fediverse/instance_key.pem") if not os.path.isfile(path): os.makedirs(os.path.dirname(path), exist_ok=True) key = _generate_private_key() pem = key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) with open(path, "wb") as fd: fd.write(pem) with open(path, "rb") as fd: return serialization.load_pem_private_key( fd.read(), password=None, backend=default_backend() ) def get_public_key(): return _get_private_key().public_key() def get_public_key_pem(): return get_public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) def signed_request(url, headers=None, data=None): method = "get" if data is None else "post" instance_actor_url = get_instance_actor_url() headers = gen.InsensitivePreservingDict( {**web.defaultHeaders, **(headers or {})} ) if "Date" not in headers: headers["Date"] = email.utils.formatdate(usegmt=True) if instance_actor_url: parsed_url = urllib.parse.urlparse(url) signed_headers = [ ("(request-target)", method + " " + parsed_url.path), ("host", parsed_url.hostname), ] for (header_name, header_value) in headers.items(): signed_headers.append((header_name.lower(), header_value)) signed_text = "\n".join("%s: %s" % header for header in signed_headers) private_key = _get_private_key() signature = private_key.sign( signed_text.encode(), padding.PKCS1v15(), hashes.SHA256() ) headers["Signature"] = ( 'keyId="%s#main-key",' % instance_actor_url + 'headers="%s",' % " ".join(k for (k, v) in signed_headers) + 'signature="%s"' % base64.b64encode(signature).decode() ) with convert_exceptions(ActivityPubProtocolError): return web.getUrlContent(url, headers=headers, data=data) def actor_url(localuser, hostname): uri = "acct:%s@%s" % (localuser, hostname) for link in webfinger(hostname, uri)["links"]: if link["rel"] == "self" and link["type"] == ACTIVITY_MIMETYPE: return link["href"] raise ActorNotFound(localuser, hostname) def get_actor(localuser, hostname): url = actor_url(localuser, hostname) return get_resource_from_url(url) def get_resource_from_url(url): content = signed_request(url, headers={"Accept": ACTIVITY_MIMETYPE}) assert content is not None, "Content from %s is None" % url with convert_exceptions(ActivityPubProtocolError, "Invalid JSON: ", True): return json.loads(content.decode()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Fediverse/config.py0000644000175000017500000000677714535072470017355 0ustar00valval### # Copyright (c) 2020-2021, 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 import conf, registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization("Fediverse") 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("Fediverse", True) Fediverse = conf.registerPlugin("Fediverse") conf.registerGroup(Fediverse, "snarfers") conf.registerChannelValue( Fediverse.snarfers, "username", registry.Boolean( False, _( """Determines whether the bot will output the profile of @username@hostname accounts it sees in channel messages.""" ), ), ) conf.registerChannelValue( Fediverse.snarfers, "profile", registry.Boolean( False, _( """Determines whether the bot will output the profile of URLs to Fediverse accounts it sees in channel messages.""" ), ), ) conf.registerChannelValue( Fediverse.snarfers, "status", registry.Boolean( False, _( """Determines whether the bot will output the content of statuses whose URLs it sees in channel messages.""" ), ), ) conf.registerGroup(Fediverse, "format") conf.registerGroup(Fediverse.format, "statuses") conf.registerChannelValue( Fediverse.format.statuses, "showContentWithCW", registry.Boolean( True, _( """Determines whether the content of a status will be shown when the status has a Content Warning.""" ), ), ) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Fediverse/plugin.py0000644000175000017500000004072514535072477017404 0ustar00valval### # Copyright (c) 2020-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import json import importlib import urllib.parse from supybot import utils, callbacks, httpserver from supybot.commands import urlSnarfer, wrap from supybot.i18n import PluginInternationalization from . import activitypub as ap from .utils import parse_xsd_duration importlib.reload(ap) _ = PluginInternationalization("Fediverse") _username_regexp = re.compile("@(?P[^@ ]+)@(?P[^@ ]+)") def html_to_text(html): return utils.web.htmlToText(html).split("\n", 1)[0].strip() class FediverseHttp(httpserver.SupyHTTPServerCallback): name = "minimal ActivityPub server" defaultResponse = _( """ You shouldn't be here, this subfolder is not for you. Go back to the index and try out other plugins (if any).""" ) def doGetOrHead(self, handler, path, write_content): if path == "/instance_actor": self.instance_actor(write_content) else: self.send_response(404) self.end_headers() self.wfile.write(b"Error 404. There is nothing to see here.") def doWellKnown(self, handler, path): actor_url = ap.get_instance_actor_url() instance_hostname = urllib.parse.urlsplit(actor_url).hostname instance_account = "acct:%s@%s" % ( instance_hostname, instance_hostname, ) if path == "/webfinger?resource=%s" % instance_account: headers = {"Content-Type": "application/jrd+json"} content = { "subject": instance_account, "links": [ { "rel": "self", "type": "application/activity+json", "href": actor_url, } ], } return (200, headers, json.dumps(content).encode()) else: return None def instance_actor(self, write_content): self.send_response(200) self.send_header("Content-type", ap.ACTIVITY_MIMETYPE) self.end_headers() if not write_content: return pem = ap.get_public_key_pem() actor_url = ap.get_instance_actor_url() hostname = urllib.parse.urlparse(actor_url).hostname actor = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", ], "id": actor_url, "preferredUsername": hostname, "type": "Service", "publicKey": { "id": actor_url + "#main-key", "owner": actor_url, "publicKeyPem": pem.decode(), }, "inbox": actor_url + "/inbox", } self.wfile.write(json.dumps(actor).encode()) class Fediverse(callbacks.PluginRegexp): """Fetches information from ActivityPub servers. Enabling Secure Fetch ^^^^^^^^^^^^^^^^^^^^^ The default configuration works with most ActivityPub servers, but not all of them, because they require an HTTP Signature to fetch profiles and statuses. Because of how HTTP Signatures work, you need to add some configuration for Limnoria to support it. First, you should set ``supybot.servers.http.port`` to a port you want your bot to listen on (by default it's 8080). If there are already plugins using it (eg. if Fediverse is already running), you should either unload all of them and load them back, or restart your bot. Then, you must configure a reverse-proxy in front of your bot (eg. nginx), and it must support HTTPS. Finally, set ``supybot.servers.http.publicUrl`` to the public URL of this server (when opening this URL in your browser, it should show a page with a title like "Supybot web server index"). """ threaded = True regexps = ["usernameSnarfer", "urlSnarfer_"] callBefore = ("Web",) def __init__(self, irc): super().__init__(irc) self._startHttp() self._actor_cache = utils.structures.TimeoutDict(timeout=600) # Used when snarfing, to cheaply avoid querying non-ActivityPub # servers. # Is also written to when using commands that successfully find # ActivityPub data. self._webfinger_support_cache = utils.structures.TimeoutDict( timeout=60 * 60 * 24 ) def _startHttp(self): callback = FediverseHttp() callback._plugin = self httpserver.hook("fediverse", callback) def die(self): self._stopHttp() super().die() def _stopHttp(self): httpserver.unhook("fediverse") def _has_webfinger_support(self, hostname): if hostname not in self._webfinger_support_cache: try: self._webfinger_support_cache[hostname] = ap.has_webfinger_support( hostname ) except Exception as e: self.log.error( "Checking Webfinger support for %s raised %s", hostname, e ) return False return self._webfinger_support_cache[hostname] def _get_actor(self, irc, username): if username in self._actor_cache: return self._actor_cache[username] match = _username_regexp.match(username) if match: localuser = match.group("localuser") hostname = match.group("hostname") try: actor = ap.get_actor(localuser, hostname) except ap.ActorNotFound: irc.error("Unknown user %s." % username, Raise=True) else: match = utils.web.urlRe.match(username) if match: # TODO: error handling actor = ap.get_resource_from_url(match.group(0)) try: hostname = urllib.parse.urlparse(actor.get("id")).hostname username = "@%s@%s" % ( hostname, actor.get["preferredUsername"], ) except Exception: username = None else: irc.errorInvalid("fediverse username", username) if username: self._actor_cache[username] = actor self._webfinger_support_cache[hostname] = True self._actor_cache[actor["id"]] = actor return actor def _format_actor_fullname(self, actor): try: hostname = urllib.parse.urlparse(actor.get("id")).hostname except Exception: hostname = "" username = actor.get("preferredUsername", "") name = actor.get("name", username) return "\x02%s\x02 (@%s@%s)" % (name, username, hostname) def _format_author(self, irc, author): if isinstance(author, str): # it's an URL try: author = self._get_actor(irc, author) except ap.ActivityPubError as e: return _("") % str(e) else: return self._format_actor_fullname(author) elif isinstance(author, dict): if author.get("type") == "Group": # Typically, there is an actor named "Default channel" # on PeerTube, which we do not want to show. return None if author.get("id"): return self._format_author(irc, author["id"]) elif isinstance(author, list): return format( "%L", filter( bool, [self._format_author(irc, item) for item in author] ), ) else: return "" def _format_status(self, irc, msg, status): if status["type"] == "Create": return self._format_status(irc, msg, status["object"]) elif status["type"] == "Note": cw = status.get("summary") author_fullname = self._format_author( irc, status.get("attributedTo") ) if cw: if self.registryValue( "format.statuses.showContentWithCW", msg.channel, irc.network, ): # show CW and content res = [ _("%s: \x02[CW %s]\x02 %s") % ( author_fullname, cw, html_to_text(status["content"]), ) ] else: # show CW but not content res = [_("%s: CW %s") % (author_fullname, cw)] else: # no CW, show content res = [ _("%s: %s") % ( author_fullname, html_to_text(status["content"]), ) ] for attachment in status.get("attachment", []): res.append(utils.str.url(attachment.get("url"))) return " ".join(res) elif status["type"] == "Announce": # aka boost; let's go fetch the original status try: content = ap.signed_request( status["object"], headers={"Accept": ap.ACTIVITY_MIMETYPE} ) status = json.loads(content.decode()) return self._format_status(irc, msg, status) except ap.ActivityPubProtocolError as e: return "" % e.args[0] elif status["type"] == "Video": author_fullname = self._format_author( irc, status.get("attributedTo") ) return format( _("\x02%s\x02 (%T) by %s: %s"), status["name"], abs(parse_xsd_duration(status["duration"]).total_seconds()), author_fullname, html_to_text(status["content"]), ) else: assert False, "Unknown status type %s: %r" % ( status["type"], status, ) @wrap(["somethingWithoutSpaces"]) def profile(self, irc, msg, args, username): """<@user@instance> Returns generic information on the account @user@instance.""" actor = self._get_actor(irc, username) irc.reply( _("%s: %s") % ( self._format_actor_fullname(actor), html_to_text(actor["summary"]), ) ) def _format_profile(self, irc, msg, actor): return _("%s: %s") % ( self._format_actor_fullname(actor), html_to_text(actor["summary"]), ) def usernameSnarfer(self, irc, msg, match): if callbacks.addressed(irc, msg): return if not self.registryValue( "snarfers.username", msg.channel, irc.network ): return if not self._has_webfinger_support(match.group("hostname")): self.log.debug( "Not snarfing, host doesn't have Webfinger support." ) return try: actor = self._get_actor(irc, match.group(0)) except ap.ActivityPubError as e: self.log.info("Could not fetch %s: %s", match.group(0), e) # Be silent on errors return irc.reply(self._format_profile(irc, msg, actor), prefixNick=False) usernameSnarfer.__doc__ = _username_regexp.pattern @urlSnarfer def urlSnarfer_(self, irc, msg, match): channel = msg.channel network = irc.network url = match.group(0) if not channel: return if callbacks.addressed(irc, msg): return snarf_profile = self.registryValue( "snarfers.profile", channel, network ) snarf_status = self.registryValue("snarfers.status", channel, network) if not snarf_profile and not snarf_status: return hostname = urllib.parse.urlparse(url).hostname if not self._has_webfinger_support(hostname): self.log.debug( "Not snarfing, host doesn't have Webfinger support." ) return try: resource = ap.get_resource_from_url(url) except ap.ActivityPubError: return try: if snarf_profile and resource["type"] in ("Person", "Service"): irc.reply(self._format_profile(irc, msg, resource)) elif snarf_status and resource["type"] in ( "Create", "Note", "Announce", ): irc.reply( self._format_status(irc, msg, resource), prefixNick=False ) except ap.ActivityPubError: return urlSnarfer_.__doc__ = utils.web._httpUrlRe @wrap(["somethingWithoutSpaces"]) def featured(self, irc, msg, args, username): """<@user@instance> Returnes the featured statuses of @user@instance (aka. pinned toots). """ actor = self._get_actor(irc, username) if "featured" not in actor: irc.reply(_("No featured statuses.")) return statuses = json.loads( ap.signed_request(actor["featured"]).decode() ).get("orderedItems", []) if not statuses: irc.reply(_("No featured statuses.")) return irc.replies( filter( bool, (self._format_status(irc, msg, status) for status in statuses), ) ) @wrap(["somethingWithoutSpaces"]) def statuses(self, irc, msg, args, username): """<@user@instance> Returned the last statuses of @user@instance. """ actor = self._get_actor(irc, username) if "outbox" not in actor: irc.error(_("No status."), Raise=True) outbox = json.loads(ap.signed_request(actor["outbox"]).decode()) # Fetches the first page of the outbox. This should be a good-enough # approximation of the number of statuses to show. statuses = json.loads(ap.signed_request(outbox["first"]).decode()).get( "orderedItems", [] ) irc.replies( filter( bool, (self._format_status(irc, msg, status) for status in statuses), ) ) @wrap(["url"]) def status(self, irc, msg, args, url): """ Shows the content of the status at . """ try: status = ap.get_resource_from_url(url) except ap.ActivityPubError as e: irc.error(_("Could not get status: %s") % e.args[0], Raise=True) else: hostname = urllib.parse.urlparse(url).hostname self._webfinger_support_cache[hostname] = True irc.reply(self._format_status(irc, msg, status)) Class = Fediverse # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Fediverse/test.py0000644000175000017500000005222214535072470017051 0ustar00valval### # Copyright (c) 2020-2021, 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 copy import json import functools import contextlib from multiprocessing import Manager from supybot import conf, log, utils from supybot.test import ChannelPluginTestCase, network from . import activitypub as ap from .test_data import ( PRIVATE_KEY, HOSTMETA_URL, HOSTMETA_DATA, WEBFINGER_URL, WEBFINGER_DATA, ACTOR_URL, ACTOR_DATA, OUTBOX_URL, OUTBOX_DATA, STATUS_URL, STATUS_DATA, STATUS_VALUE, STATUS_WITH_PHOTO_URL, STATUS_WITH_PHOTO_DATA, OUTBOX_FIRSTPAGE_URL, OUTBOX_FIRSTPAGE_DATA, BOOSTED_URL, BOOSTED_DATA, BOOSTED_ACTOR_URL, BOOSTED_ACTOR_DATA, PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA, PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA, ) class BaseFediverseTestCase(ChannelPluginTestCase): config = { # Allow snarfing the same URL twice in a row "supybot.snarfThrottle": 0.0 } plugins = ("Fediverse",) def setUp(self): super().setUp() path = conf.supybot.directories.data.dirize( "Fediverse/instance_key.pem" ) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as fd: fd.write(PRIVATE_KEY) class NetworkedFediverseTestCase(BaseFediverseTestCase): if network: def testNetworkProfile(self): self.assertRegexp("profile @val@oc.todon.fr", "0E082B40E4376B1E") # TODO: add a test with an instance which only allows fetches # with valid signatures. def testNetworkProfileUnknown(self): self.assertResponse( "profile @nonexistinguser@oc.todon.fr", "Error: Unknown user @nonexistinguser@oc.todon.fr.", ) def testHasWebfingerSupport(self): self.assertTrue(ap.has_webfinger_support("oc.todon.fr")) self.assertFalse(ap.has_webfinger_support("example.org")) class NetworklessFediverseTestCase(BaseFediverseTestCase): timeout = 1.0 @contextlib.contextmanager def mockWebfingerSupport(self, value): original_has_webfinger_support = ap.has_webfinger_support @functools.wraps(original_has_webfinger_support) def newf(hostname): if value == "not called": assert False assert type(value) is bool return value ap.has_webfinger_support = newf yield ap.has_webfinger_support = original_has_webfinger_support @contextlib.contextmanager def mockRequests(self, expected_requests): with Manager() as m: expected_requests = m.list(list(expected_requests)) original_getUrlContent = utils.web.getUrlContent @functools.wraps(original_getUrlContent) def newf(url, headers={}, data=None): self.assertIsNone(data, "Unexpected POST to %s" % url) assert expected_requests, url (expected_url, response) = expected_requests.pop(0) self.assertEqual(url, expected_url, "Unexpected URL: %s" % url) log.debug("Got request to %s", url) if isinstance(response, bytes): return response elif isinstance(response, Exception): raise response else: assert False, response utils.web.getUrlContent = newf try: yield finally: utils.web.getUrlContent = original_getUrlContent self.assertEqual( list(expected_requests), [], "Less requests than expected." ) def testFeaturedNone(self): featured = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.org/users/someuser/collections/featured", "type": "OrderedCollection", "orderedItems": [], } expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, WEBFINGER_DATA), (ACTOR_URL, ACTOR_DATA), ( "https://example.org/users/someuser/collections/featured", json.dumps(featured).encode(), ), ] with self.mockRequests(expected_requests): self.assertResponse( "featured @someuser@example.org", "No featured statuses." ) def testFeaturedSome(self): featured = { "@context": [ "https://www.w3.org/ns/activitystreams", { "ostatus": "http://ostatus.org#", "atomUri": "ostatus:atomUri", "inReplyToAtomUri": "ostatus:inReplyToAtomUri", "conversation": "ostatus:conversation", "sensitive": "as:sensitive", "toot": "http://joinmastodon.org/ns#", "votersCount": "toot:votersCount", }, ], "id": "https://example.org/users/someuser/collections/featured", "type": "OrderedCollection", "orderedItems": [ { "id": "https://example.org/users/someuser/statuses/123456789", "type": "Note", "summary": None, "inReplyTo": "https://example.org/users/someuser/statuses/100543712346463856", "published": "2018-08-13T15:49:00Z", "url": "https://example.org/@someuser/123456789", "attributedTo": "https://example.org/users/someuser", "to": ["https://example.org/users/someuser/followers"], "cc": ["https://www.w3.org/ns/activitystreams#Public"], "sensitive": False, "atomUri": "https://example.org/users/someuser/statuses/123456789", "inReplyToAtomUri": "https://example.org/users/someuser/statuses/100543712346463856", "conversation": "tag:example.org,2018-08-13:objectId=3002048:objectType=Conversation", "content": "

This is a pinned toot

", "contentMap": {"en": "

This is a pinned toot

"}, "attachment": [], "tag": [], "replies": { "id": "https://example.org/users/someuser/statuses/123456789/replies", "type": "Collection", "first": { "type": "CollectionPage", "next": "https://example.org/users/someuser/statuses/123456789/replies?min_id=100723569923690076&page=true", "partOf": "https://example.org/users/someuser/statuses/123456789/replies", "items": [ "https://example.org/users/someuser/statuses/100723569923690076" ], }, }, } ], } expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, WEBFINGER_DATA), (ACTOR_URL, ACTOR_DATA), ( "https://example.org/users/someuser/collections/featured", json.dumps(featured).encode(), ), ] with self.mockRequests(expected_requests): self.assertRegexp( "featured @someuser@example.org", "This is a pinned toot" ) def testProfile(self): expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, WEBFINGER_DATA), (ACTOR_URL, ACTOR_DATA), ] with self.mockRequests(expected_requests): self.assertResponse( "profile @someuser@example.org", "\x02someuser\x02 (@someuser@example.org): My Biography", ) def testProfileNoHostmeta(self): expected_requests = [ (HOSTMETA_URL, utils.web.Error("blah")), (WEBFINGER_URL, WEBFINGER_DATA), (ACTOR_URL, ACTOR_DATA), ] with self.mockRequests(expected_requests): self.assertResponse( "profile @someuser@example.org", "\x02someuser\x02 (@someuser@example.org): My Biography", ) def testProfileSnarfer(self): with self.mockWebfingerSupport("not called"), self.mockRequests([]): self.assertSnarfNoResponse("aaa @nonexistinguser@example.org bbb") with conf.supybot.plugins.Fediverse.snarfers.username.context(True): expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, WEBFINGER_DATA), (ACTOR_URL, ACTOR_DATA), ] # First request, should work with self.mockWebfingerSupport(True), self.mockRequests( expected_requests ): self.assertSnarfResponse( "aaa @someuser@example.org bbb", "\x02someuser\x02 (@someuser@example.org): My Biography", ) # Same request; it is all cached with self.mockWebfingerSupport("not called"), self.mockRequests( [] ): self.assertSnarfResponse( "aaa @someuser@example.org bbb", "\x02someuser\x02 (@someuser@example.org): My Biography", ) # Nonexisting user expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, utils.web.Error("blah")), ] with self.mockWebfingerSupport("not called"), self.mockRequests( expected_requests ): self.assertSnarfNoResponse( "aaa @nonexistinguser@example.org bbb" ) def testProfileSnarferNoWebfinger(self): with conf.supybot.plugins.Fediverse.snarfers.username.context(False): # No webfinger support, shouldn't make requests with self.mockWebfingerSupport(False), self.mockRequests([]): self.assertSnarfNoResponse("aaa @someuser@example.org bbb") def testProfileUrlSnarfer(self): with self.mockWebfingerSupport("not called"), self.mockRequests([]): self.assertSnarfNoResponse( "aaa https://example.org/users/someuser bbb" ) with conf.supybot.plugins.Fediverse.snarfers.profile.context(True): expected_requests = [(ACTOR_URL, utils.web.Error("blah"))] with self.mockWebfingerSupport(True), self.mockRequests( expected_requests ): self.assertSnarfNoResponse( "aaa https://example.org/users/someuser bbb" ) expected_requests = [(ACTOR_URL, ACTOR_DATA)] with self.mockWebfingerSupport("not called"), self.mockRequests( expected_requests ): self.assertSnarfResponse( "aaa https://example.org/users/someuser bbb", "\x02someuser\x02 (@someuser@example.org): My Biography", ) def testProfileUnknown(self): expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, utils.web.Error("blah")), ] with self.mockRequests(expected_requests): self.assertResponse( "profile @nonexistinguser@example.org", "Error: Unknown user @nonexistinguser@example.org.", ) def testStatus(self): expected_requests = [ (STATUS_URL, STATUS_DATA), (ACTOR_URL, ACTOR_DATA), ] with self.mockRequests(expected_requests): self.assertResponse( "status https://example.org/users/someuser/statuses/1234", "\x02someuser\x02 (@someuser@example.org): " + "@FirstAuthor I am replying to you", ) def testStatusAttachment(self): expected_requests = [ (STATUS_WITH_PHOTO_URL, STATUS_WITH_PHOTO_DATA), (ACTOR_URL, ACTOR_DATA), ] with self.mockRequests(expected_requests): self.assertResponse( "status https://example.org/users/someuser/statuses/123", "\x02someuser\x02 (@someuser@example.org): " + "Here is a picture ", ) def testStatusError(self): expected_requests = [(STATUS_URL, utils.web.Error("blah"))] with self.mockRequests(expected_requests): self.assertResponse( "status https://example.org/users/someuser/statuses/1234", "Error: Could not get status: blah", ) expected_requests = [ (STATUS_URL, STATUS_DATA), (ACTOR_URL, utils.web.Error("blah")), ] with self.mockRequests(expected_requests): self.assertResponse( "status https://example.org/users/someuser/statuses/1234", ": " + "@FirstAuthor I am replying to you", ) def testStatuses(self): expected_requests = [ (HOSTMETA_URL, HOSTMETA_DATA), (WEBFINGER_URL, WEBFINGER_DATA), (ACTOR_URL, ACTOR_DATA), (OUTBOX_URL, OUTBOX_DATA), (OUTBOX_FIRSTPAGE_URL, OUTBOX_FIRSTPAGE_DATA), (BOOSTED_URL, BOOSTED_DATA), (BOOSTED_ACTOR_URL, BOOSTED_ACTOR_DATA), ] with self.mockRequests(expected_requests): self.assertResponse( "statuses @someuser@example.org", "\x02someuser\x02 (@someuser@example.org): " + "@FirstAuthor I am replying to you, " + "\x02someuser\x02 (@someuser@example.org): " + "\x02[CW This is a content warning]\x02 " + "This is a status with a content warning, and " + "\x02Boosted User\x02 (@BoostedUser@example.net): " + "Status Content " + "", ) # The actors are cached from the previous request expected_requests = [ (OUTBOX_URL, OUTBOX_DATA), (OUTBOX_FIRSTPAGE_URL, OUTBOX_FIRSTPAGE_DATA), (BOOSTED_URL, BOOSTED_DATA), ] with self.mockRequests(expected_requests): with conf.supybot.plugins.Fediverse.format.statuses.showContentWithCW.context( False ): self.assertResponse( "statuses @someuser@example.org", "\x02someuser\x02 (@someuser@example.org): " + "@FirstAuthor I am replying to you, " + "\x02someuser\x02 (@someuser@example.org): " + "CW This is a content warning, and " + "\x02Boosted User\x02 (@BoostedUser@example.net): " + "Status Content " + "", ) def testVideo(self): expected_requests = [ (PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA), (PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA), ] with self.mockRequests(expected_requests): self.assertResponse( "status https://example.org/w/gABde9e210FGHre", "\x02name of video\x02 (1 hour, 26 minutes, and 0 seconds) " "by \x02chocobozzz\x02 (@chocobozzz@peertube.cpy.re): " "description of the video with a second line", ) def testStatusUrlSnarferDisabled(self): with self.mockWebfingerSupport("not called"), self.mockRequests([]): self.assertSnarfNoResponse( "aaa https://example.org/users/someuser/statuses/1234 bbb" ) def testStatusUrlSnarfer(self): with conf.supybot.plugins.Fediverse.snarfers.status.context(True): expected_requests = [ (STATUS_URL, STATUS_DATA), (ACTOR_URL, ACTOR_DATA), ] with self.mockWebfingerSupport(True), self.mockRequests( expected_requests ): self.assertSnarfResponse( "aaa https://example.org/users/someuser/statuses/1234 bbb", "\x02someuser\x02 (@someuser@example.org): " + "@FirstAuthor I am replying to you", ) def testStatusUrlSnarferMore(self): # Actually this is a test for src/callbacks.py, but it's easier to # stick it here. status_value = copy.deepcopy(STATUS_VALUE) str_long = "l" + ("o" * 400) + "ng" str_message = "mess" + ("a" * 400) + "ge" status_obj = status_value["object"] status_obj["content"] = status_obj["content"].replace( "to you", "to you with a " + str_long + " " + str_message ) with conf.supybot.plugins.Fediverse.snarfers.status.context(True): expected_requests = [ (STATUS_URL, json.dumps(status_value).encode()), (ACTOR_URL, ACTOR_DATA), ] with self.mockWebfingerSupport(True), self.mockRequests( expected_requests ): self.assertSnarfResponse( "aaa https://example.org/users/someuser/statuses/1234 bbb", "\x02someuser\x02 (@someuser@example.org): " + "@FirstAuthor I am replying to you with a " + " \x02(2 more messages)\x02", ) self.assertNoResponse(" ") self.assertResponse("more", str_long + " \x02(1 more message)\x02") self.assertResponse("more", str_message) def testStatusUrlSnarferErrors(self): with conf.supybot.plugins.Fediverse.snarfers.status.context(True): expected_requests = [(STATUS_URL, utils.web.Error("blah"))] with self.mockWebfingerSupport(True), self.mockRequests( expected_requests ): self.assertSnarfNoResponse( "aaa https://example.org/users/someuser/statuses/1234 bbb" ) expected_requests = [ (STATUS_URL, STATUS_DATA), (ACTOR_URL, utils.web.Error("blah")), ] with self.mockWebfingerSupport("not called"), self.mockRequests( expected_requests ): self.assertSnarfResponse( "aaa https://example.org/users/someuser/statuses/1234 bbb", ": @FirstAuthor I am replying to you", ) def testSnarferType(self): # Sends a request, notices it's a status, gives up with conf.supybot.plugins.Fediverse.snarfers.profile.context(True): expected_requests = [(STATUS_URL, STATUS_DATA)] with self.mockWebfingerSupport(True), self.mockRequests( expected_requests ): self.assertSnarfNoResponse( "aaa https://example.org/users/someuser/statuses/1234 bbb" ) # Sends a request, notices it's a profile, gives up with conf.supybot.plugins.Fediverse.snarfers.profile.context(True): expected_requests = [(ACTOR_URL, ACTOR_DATA)] with self.mockWebfingerSupport("not called"), self.mockRequests( expected_requests ): self.assertSnarfNoResponse( "aaa https://example.org/users/someuser/ bbb" ) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Fediverse/test_data.py0000644000175000017500000005210214535072470020037 0ustar00valval### # Copyright (c) 2020-2021, 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 json PRIVATE_KEY = b""" -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA6jtjTlaTh1aR+q3gpZvb4dj8s81zKmwy7cwn44LtLV+ivNf/ SkWPr1zkm/gWFItC3058Faqk9p4fdJaxVJJTW0KL7LlJs+LTcMsLi2nTgvBZg7oE KRXZxuJJcc5QNkgY8vHt1PxdD17mZBGwfg2loZfnjZOOz4F8wdQ18Da1ZFUFyc+R qj1THdXbpBjF7zcNyJOWzzRwhpiqdJomnTAYDscAkkF2/gI8tYP+Is31GOE1phPC DH20uvJNUtDnXSdUm2Ol21LmePV4pWS75mcIHz5YAKwAGo9XoUQa8lC6IHw6LX+y CVKkoSc0Ouzr3acQCLZ8EDUIh2nMhw/VtYV7JwIDAQABAoIBAFSARkwtqZ1qmtFf xyqXttScblYDaWfFjv4A5+cJBb2XweL03ZGS1MpD7elir7yLnP1omBVM8aRS2TA7 aRAElfPXZxloovE1hGgtqCWMcRTM1s5R3kxgKKe6XRqkfoWGrxF+O/nZbU0tRFqX kx92lulcHtoRgLTVlwdqImddpUTjQrWmrt3nEjTZj5tHcPGdC2ovH/bFrganbCR1 If6xG2r6RWSfMEpj7yFTKRvnLCr2VpviDOwFh/zZdwyqBRKW6LNZP04TtlFfKh5C 1R2tZVRHQ7Ed99yruirW0rmgOjA6dJTpN6oX6x3DpTi48oK2jktEIk07P7jy1mZY NeCQcqkCgYEA+M0DQ+0fBm/RJyDIxsupMAf8De1kG6Bj8gVSRnvtD0Fb3LTswT3I TDnIVttjOzBsbpZVdjdCE9Wcfj9pIwu3YTO54OTS8kiwYRshzEm3UpdPOSCnIZUx jwbbwEHq0zEeIWVjDBDXN2fqEcu7gFqBzYivAh8hYq78BJkUeBWU3N0CgYEA8QJ0 6xS551VEGLbn9h5pPxze7l8a9WJc1uIxRexeBtd4UwJ5e1yLN68FVNjGr3JtreJ3 KP/VyynFubNRvwGEnifKe9QyiATFCbVeAFBQFuA0w89LOmBiHc+uHz1uA5oXnD99 Y0pEu8g+QsBKeQowMhkYnw4h5cq3AVCKRIdNpdMCgYEAwy5p8l7SKQWNagnBGJtr BeAtr2tdToL8BUCBdAQCTCZ0/2b8GPjz6kCmVuVTKnrphbPwJYZiExdP5oauXyzw 1pNyreg1SJcXr4ZOdGocI/HJ18Iy+xiEwXSa7m+H3dg5j+9uzWdkvvWJXh6a4K2g CPLCgIKVeUpXMPA6a55aow0CgYAMpoRckonvipo4ceFbGd2MYoeRG4zetHsLDHRp py6ITWcTdF3MC9+C3Lz65yYGr4ryRaDblhIyx86JINB5piq/4nbOaST93sI48Dwu 6AhMKxiZ7peUSNrdlbkeCqtrpPr4SJzcSVmyQaCDAHToRZCiEI8qSiOdXDae6wtW 7YM14QKBgQDnbseQK0yzrsZoOmQ9PBULr4vNLiL5+OllOG1+GNNztk/Q+Xfx6Hvw h6cgTcpZsvaa2CW6A2yqenmGfKBgiRoN39vFqjVDkjL1HaL3rPeK1H7RWrz1Sto7 rut+UhYHat9fo6950Wvxa4Iee9q0NOF0HUkD6WupcPyC0nSEex8Z6A== -----END RSA PRIVATE KEY----- """ HOSTMETA_URL = "https://example.org/.well-known/host-meta" HOSTMETA_DATA = b""" """ WEBFINGER_URL = "https://example.org/.well-known/webfinger?resource=acct:someuser@example.org" WEBFINGER_VALUE = { "subject": "acct:someuser@example.org", "aliases": [ "https://example.org/@someuser", "https://example.org/users/someuser", ], "links": [ { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://example.org/@someuser", }, { "rel": "self", "type": "application/activity+json", "href": "https://example.org/users/someuser", }, { "rel": "http://ostatus.org/schema/1.0/subscribe", "template": "https://example.org/authorize_interaction?uri={uri}", }, ], } WEBFINGER_DATA = json.dumps(WEBFINGER_VALUE).encode() ACTOR_URL = "https://example.org/users/someuser" ACTOR_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", "toot": "http://joinmastodon.org/ns#", "featured": {"@id": "toot:featured", "@type": "@id"}, "alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"}, "movedTo": {"@id": "as:movedTo", "@type": "@id"}, "schema": "http://schema.org#", "PropertyValue": "schema:PropertyValue", "value": "schema:value", "IdentityProof": "toot:IdentityProof", "discoverable": "toot:discoverable", "Emoji": "toot:Emoji", "focalPoint": {"@container": "@list", "@id": "toot:focalPoint"}, }, ], "id": "https://example.org/users/someuser", "type": "Person", "following": "https://example.org/users/someuser/following", "followers": "https://example.org/users/someuser/followers", "inbox": "https://example.org/users/someuser/inbox", "outbox": "https://example.org/users/someuser/outbox", "featured": "https://example.org/users/someuser/collections/featured", "preferredUsername": "someuser", "name": "someuser", "summary": "

My Biography

", "url": "https://example.org/@someuser", "manuallyApprovesFollowers": False, "discoverable": True, "publicKey": { "id": "https://example.org/users/someuser#main-key", "owner": "https://example.org/users/someuser", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkaY84E/OjpjF7Dgy/nC+\nySBCiQvSOKBpNl468XP1QiOiMsILC1ec2J+LpU1Tm0kAC+uY8budLx6Wt+oz+4FU\n/82S9j9jVkWPiNVHJSQHXi13F9YQ4+MwC8niKc+qsmKUL8crSbd7dmCnOBxhvJWf\nfwOk1TW4u1fxXqHMFuw5zdfDlmRlU2FLX1LYTOxLnGp/ef/BAykV3rz6VouhAQwO\nhRay7ZgI5zlT7NtCoA17I8YiYfEs7MH0nBMrKOMw5eR1WDf5Gw78C/IAZHP1WVMv\n63V3N71OrMSfCH20OZ1H2Gyov5GX4+NSx7HI26dMDldQWOb2rYS9d0/7qM2xNUK8\n3wIDAQAB\n-----END PUBLIC KEY-----\n", }, "attachment": [ {"type": "PropertyValue", "name": "Pronoun", "value": "they"}, {"type": "PropertyValue", "name": "Location", "value": "Somewhere"}, ], "endpoints": {"sharedInbox": "https://example.org/inbox"}, "icon": { "type": "Image", "mediaType": "image/png", "url": "https://assets.example.org/avatar.png", }, "image": { "type": "Image", "mediaType": "image/png", "url": "https://assets.example.org/header.png", }, } ACTOR_DATA = json.dumps(ACTOR_VALUE).encode() OUTBOX_URL = "https://example.org/users/someuser/outbox" OUTBOX_VALUE = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.org/users/someuser/outbox", "type": "OrderedCollection", "totalItems": 4835, "first": "https://example.org/users/someuser/outbox?page=true", "last": "https://example.org/users/someuser/outbox?min_id=0&page=true", } OUTBOX_DATA = json.dumps(OUTBOX_VALUE).encode() STATUS_URL = "https://example.org/users/someuser/statuses/1234" STATUS_VALUE = { "id": "https://example.org/users/someuser/statuses/1234/activity", "type": "Create", "actor": "https://example.org/users/someuser", "published": "2020-05-08T01:23:45Z", "to": ["https://example.org/users/someuser/followers"], "cc": [ "https://www.w3.org/ns/activitystreams#Public", "https://example.com/users/FirstAuthor", ], "object": { "id": "https://example.org/users/someuser/statuses/1234", "type": "Note", "summary": None, "inReplyTo": "https://example.com/users/FirstAuthor/statuses/42", "published": "2020-05-08T01:23:45Z", "url": "https://example.org/@FirstAuthor/42", "attributedTo": "https://example.org/users/someuser", "to": ["https://example.org/users/someuser/followers"], "cc": [ "https://www.w3.org/ns/activitystreams#Public", "https://example.com/users/FirstAuthor", ], "sensitive": False, "atomUri": "https://example.org/users/someuser/statuses/1234", "inReplyToAtomUri": "https://example.com/users/FirstAuthor/statuses/42", "conversation": "tag:example.com,2020-05-08:objectId=aaaa:objectType=Conversation", "content": '

@FirstAuthor I am replying to you

', "contentMap": { "en": '

@FirstAuthor I am replying to you

' }, "attachment": [], "tag": [ { "type": "Mention", "href": "https://example.com/users/FirstAuthor", "name": "@FirstAuthor@example.com", } ], "replies": { "id": "https://example.org/users/someuser/statuses/1234/replies", "type": "Collection", "first": { "type": "CollectionPage", "next": "https://example.org/users/someuser/statuses/1234/replies?only_other_accounts=true&page=true", "partOf": "https://example.org/users/someuser/statuses/1234/replies", "items": [], }, }, }, } STATUS_DATA = json.dumps(STATUS_VALUE).encode() STATUS_WITH_PHOTO_URL = "https://example.org/users/someuser/statuses/123" STATUS_WITH_PHOTO_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://example.org/schemas/litepub-0.1.jsonld", ], "actor": "https://example.org/users/someuser", "attachment": [ { "mediaType": "image/jpeg", "name": "IMG_foo.jpg", "type": "Document", "url": "https://example.org/foo.jpg", } ], "attributedTo": "https://example.org/users/someuser", "cc": ["https://www.w3.org/ns/activitystreams#Public"], "content": "Here is a picture", "id": "https://example.org/users/someuser/statuses/123", "published": "2020-05-08T01:23:45Z", "sensitive": False, "summary": "", "tag": [], "to": ["https://example.org/users/someuser/followers"], "type": "Note", } STATUS_WITH_PHOTO_DATA = json.dumps(STATUS_WITH_PHOTO_VALUE).encode() OUTBOX_FIRSTPAGE_URL = "https://example.org/users/someuser/outbox?page=true" OUTBOX_FIRSTPAGE_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", { "ostatus": "http://ostatus.org#", "atomUri": "ostatus:atomUri", "inReplyToAtomUri": "ostatus:inReplyToAtomUri", "conversation": "ostatus:conversation", "sensitive": "as:sensitive", "toot": "http://joinmastodon.org/ns#", "votersCount": "toot:votersCount", "Emoji": "toot:Emoji", "focalPoint": {"@container": "@list", "@id": "toot:focalPoint"}, }, ], "id": "https://example.org/users/someuser/outbox?page=true", "type": "OrderedCollectionPage", "next": "https://example.org/users/someuser/outbox?max_id=104101144953797529&page=true", "prev": "https://example.org/users/someuser/outbox?min_id=104135036335976677&page=true", "partOf": "https://example.org/users/someuser/outbox", "orderedItems": [ STATUS_VALUE, { "id": "https://example.org/users/someuser/statuses/1235/activity", "type": "Create", "actor": "https://example.org/users/someuser", "published": "2020-05-08T01:23:45Z", "to": ["https://example.org/users/someuser/followers"], "cc": ["https://www.w3.org/ns/activitystreams#Public"], "object": { "id": "https://example.org/users/someuser/statuses/1235", "type": "Note", "summary": "This is a content warning", "attributedTo": "https://example.org/users/someuser", "inReplyTo": None, "content": "

This is a status with a content warning

", }, }, { "id": "https://example.org/users/someuser/statuses/12345/activity", "type": "Announce", "actor": "https://example.org/users/someuser", "published": "2020-05-05T11:22:33Z", "to": ["https://example.org/users/someuser/followers"], "cc": [ "https://example.net/users/BoostedUser", "https://www.w3.org/ns/activitystreams#Public", ], "object": "https://example.net/users/BoostedUser/statuses/123456", "atomUri": "https://example.org/users/someuser/statuses/12345/activity", }, ], } OUTBOX_FIRSTPAGE_DATA = json.dumps(OUTBOX_FIRSTPAGE_VALUE).encode() BOOSTED_URL = "https://example.net/users/BoostedUser/statuses/123456" BOOSTED_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", { "ostatus": "http://ostatus.org#", "atomUri": "ostatus:atomUri", "inReplyToAtomUri": "ostatus:inReplyToAtomUri", "conversation": "ostatus:conversation", "sensitive": "as:sensitive", "toot": "http://joinmastodon.org/ns#", "votersCount": "toot:votersCount", "blurhash": "toot:blurhash", "focalPoint": {"@container": "@list", "@id": "toot:focalPoint"}, }, ], "id": "https://example.net/users/BoostedUser/statuses/123456", "type": "Note", "summary": None, "inReplyTo": None, "published": "2020-05-05T11:00:00Z", "url": "https://example.net/@BoostedUser/123456", "attributedTo": "https://example.net/users/BoostedUser", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://example.net/users/BoostedUser/followers"], "sensitive": False, "atomUri": "https://example.net/users/BoostedUser/statuses/123456", "inReplyToAtomUri": None, "conversation": "tag:example.net,2020-05-05:objectId=bbbbb:objectType=Conversation", "content": "

Status Content

", "contentMap": {"en": "

Status Content

"}, "attachment": [ { "type": "Document", "mediaType": "image/png", "url": "https://example.net/system/media_attachments/image.png", "name": "Alt Text", "focalPoint": [0.0, 0.0], } ], "tag": [], "replies": { "id": "https://example.net/users/BoostedUser/statuses/123456/replies", "type": "Collection", "first": { "type": "CollectionPage", "next": "https://example.net/users/BoostedUser/statuses/123456/replies?only_other_accounts=true&page=true", "partOf": "https://example.net/users/BoostedUser/statuses/123456/replies", "items": [], }, }, } BOOSTED_DATA = json.dumps(BOOSTED_VALUE).encode() BOOSTED_ACTOR_URL = "https://example.net/users/BoostedUser" BOOSTED_ACTOR_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", "toot": "http://joinmastodon.org/ns#", "featured": {"@id": "toot:featured", "@type": "@id"}, "alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"}, "movedTo": {"@id": "as:movedTo", "@type": "@id"}, "schema": "http://schema.org#", "PropertyValue": "schema:PropertyValue", "value": "schema:value", "IdentityProof": "toot:IdentityProof", "discoverable": "toot:discoverable", "focalPoint": {"@container": "@list", "@id": "toot:focalPoint"}, }, ], "id": "https://example.net/users/BoostedUser", "type": "Person", "following": "https://example.net/users/BoostedUser/following", "followers": "https://example.net/users/BoostedUser/followers", "inbox": "https://example.net/users/BoostedUser/inbox", "outbox": "https://example.net/users/BoostedUser/outbox", "featured": "https://example.net/users/BoostedUser/collections/featured", "preferredUsername": "BoostedUser", "name": "Boosted User", "url": "https://example.net/@BoostedUser", "endpoints": {"sharedInbox": "https://example.net/inbox"}, } BOOSTED_ACTOR_DATA = json.dumps(BOOSTED_ACTOR_VALUE).encode() PEERTUBE_ACTOR_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"}, { "pt": "https://joinpeertube.org/ns#", "sc": "http://schema.org/", "playlists": {"@id": "pt:playlists", "@type": "@id"}, }, ], "type": "Person", "id": "https://peertube.cpy.re/accounts/chocobozzz", "following": "https://peertube.cpy.re/accounts/chocobozzz/following", "followers": "https://peertube.cpy.re/accounts/chocobozzz/followers", "playlists": "https://peertube.cpy.re/accounts/chocobozzz/playlists", "inbox": "https://peertube.cpy.re/accounts/chocobozzz/inbox", "outbox": "https://peertube.cpy.re/accounts/chocobozzz/outbox", "preferredUsername": "chocobozzz", "url": "https://peertube.cpy.re/accounts/chocobozzz", "name": "chocobozzz", "published": "2017-11-28T08:48:24.271Z", "summary": None, } PEERTUBE_ACTOR_DATA = json.dumps(PEERTUBE_ACTOR_VALUE).encode() PEERTUBE_ACTOR_URL = "https://peertube.cpy.re/accounts/chocobozzz" PEERTUBE_VIDEO_VALUE = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"}, { "pt": "https://joinpeertube.org/ns#", "sc": "http://schema.org/", "Hashtag": "as:Hashtag", "uuid": "sc:identifier", "category": "sc:category", "licence": "sc:license", "subtitleLanguage": "sc:subtitleLanguage", "sensitive": "as:sensitive", "language": "sc:inLanguage", "icons": "as:icon", "isLiveBroadcast": "sc:isLiveBroadcast", "liveSaveReplay": { "@type": "sc:Boolean", "@id": "pt:liveSaveReplay", }, "permanentLive": { "@type": "sc:Boolean", "@id": "pt:permanentLive", }, "latencyMode": {"@type": "sc:Number", "@id": "pt:latencyMode"}, "Infohash": "pt:Infohash", "originallyPublishedAt": "sc:datePublished", "views": {"@type": "sc:Number", "@id": "pt:views"}, "state": {"@type": "sc:Number", "@id": "pt:state"}, "size": {"@type": "sc:Number", "@id": "pt:size"}, "fps": {"@type": "sc:Number", "@id": "pt:fps"}, "commentsEnabled": { "@type": "sc:Boolean", "@id": "pt:commentsEnabled", }, "downloadEnabled": { "@type": "sc:Boolean", "@id": "pt:downloadEnabled", }, "waitTranscoding": { "@type": "sc:Boolean", "@id": "pt:waitTranscoding", }, "support": {"@type": "sc:Text", "@id": "pt:support"}, "likes": {"@id": "as:likes", "@type": "@id"}, "dislikes": {"@id": "as:dislikes", "@type": "@id"}, "shares": {"@id": "as:shares", "@type": "@id"}, "comments": {"@id": "as:comments", "@type": "@id"}, }, ], "to": ["https://www.w3.org/ns/activitystreams#Public"], "type": "Video", "name": "name of video", "duration": "PT5160S", "tag": [{"type": "Hashtag", "name": "vostfr"}], "category": {"identifier": "2", "name": "Films"}, "licence": {"identifier": "4", "name": "Attribution - Non Commercial"}, "language": {"identifier": "en", "name": "English"}, "views": 13718, "sensitive": False, "waitTranscoding": False, "state": 1, "commentsEnabled": True, "downloadEnabled": True, "published": "2017-10-23T07:54:38.155Z", "originallyPublishedAt": None, "updated": "2022-07-13T07:03:12.373Z", "mediaType": "text/markdown", "content": "description of the video\r\nwith a second line", "support": None, "subtitleLanguage": [], "icon": [ # redacted ], "url": [ # redacted ], "attributedTo": [ {"type": "Person", "id": PEERTUBE_ACTOR_URL}, { "type": "Group", "id": ACTOR_URL, }, ], "isLiveBroadcast": False, "liveSaveReplay": None, "permanentLive": None, "latencyMode": None, } PEERTUBE_VIDEO_DATA = json.dumps(PEERTUBE_VIDEO_VALUE).encode() PEERTUBE_VIDEO_URL = "https://example.org/w/gABde9e210FGHre" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Fediverse/utils.py0000644000175000017500000000477114535072470017240 0ustar00valval### # Copyright (c) 2022, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import datetime # Credits for the regexp and function: https://stackoverflow.com/a/2765366/539465 _XSD_DURATION_RE = re.compile( "(?P-?)P" "(?:(?P\d+)Y)?" "(?:(?P\d+)M)?" "(?:(?P\d+)D)?" "(?:T(?:(?P\d+)H)?(?:(?P\d+)M)?(?:(?P\d+)S)?)?" ) def parse_xsd_duration(s): """Parses this format to a timedelta: https://www.w3.org/TR/xmlschema11-2/#duration""" # Fetch the match groups with default value of 0 (not None) duration = _XSD_DURATION_RE.match(s).groupdict(0) # Create the timedelta object from extracted groups delta = datetime.timedelta( days=int(duration["days"]) + (int(duration["months"]) * 30) + (int(duration["years"]) * 365), hours=int(duration["hours"]), minutes=int(duration["minutes"]), seconds=int(duration["seconds"]), ) if duration["sign"] == "-": delta *= -1 return delta ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Filter/0000755000175000017500000000000014535072535015030 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/__init__.py0000644000175000017500000000520314535072470017137 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Provides numerous filters, and a command (outfilter) to set them as filters on the output of the bot. For instance, you could make everything the bot says be in leetspeak, or Morse code, or any number of other kinds of filters. Not very useful, but definitely quite fun :) See also the :ref:`Format plugin ` for format manipulation 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/config.py0000644000175000017500000000605614535072470016654 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Filter') Filter = conf.registerPlugin('Filter') conf.registerGroup(Filter, 'spellit') conf.registerGlobalValue(Filter.spellit, 'replaceLetters', registry.Boolean(True, _("""Determines whether or not to replace letters in the output of spellit."""))) conf.registerGlobalValue(Filter.spellit, 'replacePunctuation', registry.Boolean(True, _("""Determines whether or not to replace punctuation in the output of spellit."""))) conf.registerGlobalValue(Filter.spellit, 'replaceNumbers', registry.Boolean(True, _("""Determines whether or not to replace numbers in the output of spellit."""))) conf.registerGroup(Filter, 'shrink') conf.registerChannelValue(Filter.shrink, 'minimum', registry.PositiveInteger(4, _("""Determines the minimum number of a letters in a word before it will be shrunken by the shrink command/filter."""))) 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('Filter', True) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Filter/locales/0000755000175000017500000000000014535072535016452 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/locales/fi.po0000644000175000017500000003427314535072470017417 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Filter plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:37+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" #: config.py:39 msgid "" "Determines whether or not to\n" " replace letters in the output of spellit." msgstr "" "Määrittää\n" " korvataanko kirjaimet spellitin ulostulolla." #: config.py:42 msgid "" "Determines whether or not\n" " to replace punctuation in the output of spellit." msgstr "" "Määrittää korvataanko\n" " välimerkit spellitin ulostulolla.." #: config.py:45 msgid "" "Determines whether or not to\n" " replace numbers in the output of spellit." msgstr "" "Määrittää\n" " korvataanko numerot spellitin ulostulolla." #: config.py:49 msgid "" "Determines the minimum number of a letters\n" " in a word before it will be shrunken by the shrink command/filter." msgstr "" "Määritää minimi määrän kirjaimia\n" " sanassa ennen kuin se kutistetaan kutitus komennolla / suodattimella." #: plugin.py:55 msgid "" "This plugin offers several commands which transform text in some way.\n" " It also provides the capability of using such commands to 'filter' the\n" " output of the bot -- for instance, you could make everything the bot " "says\n" " be in leetspeak, or Morse code, or any number of other kinds of " "filters.\n" " Not very useful, but definitely quite fun :)" msgstr "" "Tämä lisäosa tarjoaa muutamia komentoja, jotka muuttavat tekstiä jollakin " "tavalla.\n" " Se antaa myös valtuuden käyttää komentoja, jotka 'suodattavat' \n" " botin ulostuloa -- esimerkiksi voit saada kaiken mitä botti sanoo " "olevan \n" " leetspeakilla, tai Morsen aakkosilla, tai monen muunlaisella " "suodattimella.\n" " Ei kovin hyödyllinen, mutta varmasti aika hauska :)" #: plugin.py:90 msgid "" "[] []\n" "\n" " Sets the outFilter of this plugin to be . If no command " "is\n" " given, unsets the outFilter. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Asettaa tämän lisäosan ulostulo suodatuksen . Jos " "komentoa ei ole\n" " annettu, poistaa ulostulo suodattminen. on vaadittu vain " "jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:103 msgid "That's not a valid filter command." msgstr "Tuo ei ole kelvollinen filter komento." #: plugin.py:114 msgid "" "\n" "\n" " Removes all the vowels from . (If you're curious why this is\n" " named 'hebrew' it's because I (jemfinch) thought of it in Hebrew " "class,\n" " and printed Hebrew often elides the vowels.)\n" " " msgstr "" "\n" "\n" " Poistaa kaikki vokaalit . (Jos olet utelias miksi tämä " "on\n" " nimeltään 'hebrew', se johtuu siitä, että minä (jemfinch) luulin " "sitä Hepreaksi,\n" " ja tulostin Hebrewin usein vokaaleilla.)\n" " " #: plugin.py:128 msgid "" "\n" "\n" " Removes all the spaces from .\n" " " msgstr "" "\n" "\n" " Poistaa kaikki välilyönnit .\n" " " #: plugin.py:137 msgid "" "\n" "\n" " Returns , with all consecutive duplicated letters removed.\n" " " msgstr "" "\n" "\n" " Palauttaa , kaikki peräkkäiset samat kaksi kirjainta " "poistettuna.\n" " " #: plugin.py:150 msgid "" "\n" "\n" " Returns the binary representation of .\n" " " msgstr "" "\n" "\n" " Palauttaa binääri esitelmänä.\n" " " #: plugin.py:184 msgid "" "\n" "\n" " Returns the character representation of binary .\n" " Assumes ASCII, 8 digits per character.\n" " " msgstr "" "\n" "\n" " Palauttaa binäärimerkki esityksen.\n" " Olettaa ASCII, 8 numeroa per merkki.\n" " " #: plugin.py:199 msgid "" "\n" "\n" " Returns a hexstring from the given string; a hexstring is a string\n" " composed of the hexadecimal value of each character in the string\n" " " msgstr "" "\n" "\n" " Palauttaa heksaketjun annetusta ketjusta; heksaketju on ketju, jonka " "muodostaa\n" " jokaisen ketjussa olevan merkin heksadesimaaliarvo. \n" " " #: plugin.py:210 msgid "" "\n" "\n" " Returns the string corresponding to . Obviously,\n" " must be a string of hexadecimal digits.\n" " " msgstr "" "\n" "\n" " Palauttaa ketjun joka vastaa . Ilmeisesti,\n" " täytyy olla ketju heksadesimaalisia numeroita.\n" " " #: plugin.py:219 msgid "Invalid input." msgstr "Virheellinen sisääntulo." #: plugin.py:225 msgid "" "\n" "\n" " Rotates 13 characters to the right in the alphabet. Rot13 " "is\n" " commonly used for text that simply needs to be hidden from " "inadvertent\n" " reading by roaming eyes, since it's easily reversible.\n" " " msgstr "" "\n" "\n" " Kääntää kolmetoista merkkiä oikealle aakkosissa. Rot13 " "käytetään\n" " tekstissä, joka yksinkertaisesti täytyy piilottaa huomaamattomasti\n" " lukijan harhailevilta silmiltä, koska se on helposti kumottavissa.\n" " " #: plugin.py:246 msgid "" "\n" "\n" " Returns the l33tspeak version of \n" " " msgstr "" "\n" "\n" " Palauttaa l33tspeak version \n" " " #: plugin.py:266 msgid "" "\n" "\n" " Replies with an especially k-rad translation of .\n" " " msgstr "" "\n" "\n" " Vastaa ilmeisesti k-rad käännöksellä.\n" " " #: plugin.py:282 msgid "" "\n" "\n" " Replies with a string where each word is scrambled; i.e., each " "internal\n" " letter (that is, all letters but the first and last) are shuffled.\n" " " msgstr "" "\n" "\n" " Vastaa ketjulla, jossa jokainen sana on sekaisin; esim., jokainen " "sisänen\n" " kirjain (eli kaikki kirjaimet paitsi ensinmäinen ja viimeinen) " "sekoitetaan.\n" " " #: plugin.py:347 msgid "" "\n" "\n" " Does the reverse of the morse command.\n" " " msgstr "" "\n" "\n" " Tekee morse komennon käänteisenä.\n" " " #: plugin.py:364 msgid "" "\n" "\n" " Gives the Morse code equivalent of a given string.\n" " " msgstr "" "\n" "\n" " Antaa annetun ketjun Morsen aakkosilla.\n" " " #: plugin.py:376 msgid "" "\n" "\n" " Reverses .\n" " " msgstr "" "\n" "\n" " Kääntää .\n" " " #: plugin.py:394 msgid "" "\n" "\n" " Returns with each character randomly colorized.\n" " " msgstr "" "\n" "\n" " Palauttaa niin, että jokainen merkki on satunnaisesti " "värjätty.\n" " " #: plugin.py:409 msgid "" "\n" "\n" " Returns colorized like a rainbow.\n" " " msgstr "" "\n" "\n" " Palauttaa sateenkaaren väreillä.\n" " " #: plugin.py:426 #, fuzzy msgid "" "\n" "\n" " Strips bold, underline, and colors from ." msgstr "" "\n" "\n" " Poistaa kaikki välilyönnit .\n" " " #: plugin.py:433 msgid "" "\n" "\n" " Returns stripped of all color codes.\n" " " msgstr "" "\n" "\n" " Palauttaa kaikki värikoodit riisuttuna.\n" " " #: plugin.py:442 #, fuzzy msgid "" "\n" "\n" " Returns as if an AOL user had said it.\n" " " msgstr "" "\n" "\n" " Palauttaa kuin AOL käyttäjä olisi sanonut sen.\n" " " #: plugin.py:469 msgid "" "\n" "\n" " Returns as if JeffK had said it himself.\n" " " msgstr "" "\n" "\n" " Palauttaa kuin JeffK olisi sanonut sen itse.\n" " " #: plugin.py:565 msgid "ay" msgstr "aa" #: plugin.py:565 msgid "bee" msgstr "bee" #: plugin.py:565 msgid "dee" msgstr "dee" #: plugin.py:565 msgid "see" msgstr "see" #: plugin.py:566 msgid "aych" msgstr "hoo" #: plugin.py:566 msgid "ee" msgstr "ee" #: plugin.py:566 msgid "eff" msgstr "äf" #: plugin.py:566 msgid "gee" msgstr "gee" #: plugin.py:567 msgid "ell" msgstr "äll" #: plugin.py:567 msgid "eye" msgstr "iii" #: plugin.py:567 msgid "jay" msgstr "jii" #: plugin.py:567 msgid "kay" msgstr "koo" #: plugin.py:568 msgid "cue" msgstr "quu" #: plugin.py:568 msgid "em" msgstr "äm" #: plugin.py:568 msgid "en" msgstr "än" #: plugin.py:568 msgid "oh" msgstr "oo" #: plugin.py:568 msgid "pee" msgstr "pee" #: plugin.py:569 msgid "arr" msgstr "är" #: plugin.py:569 msgid "ess" msgstr "äs" #: plugin.py:569 msgid "tee" msgstr "tee" #: plugin.py:569 msgid "you" msgstr "uu" #: plugin.py:570 msgid "double-you" msgstr "tupla-vee" #: plugin.py:570 msgid "ecks" msgstr "äks" #: plugin.py:570 msgid "vee" msgstr "vee" #: plugin.py:570 msgid "why" msgstr "yy" #: plugin.py:571 msgid "zee" msgstr "zet" #: plugin.py:576 msgid "exclamation point" msgstr "huutomerkki" #: plugin.py:577 msgid "quote" msgstr "lainausmerkki" #: plugin.py:578 msgid "pound" msgstr "punta" #: plugin.py:579 msgid "dollar sign" msgstr "dollari merkki" #: plugin.py:580 msgid "percent" msgstr "prosentti" #: plugin.py:581 msgid "ampersand" msgstr "at-merkki" #: plugin.py:582 msgid "single quote" msgstr "heittomerkki" #: plugin.py:583 msgid "left paren" msgstr "vasen sulku" #: plugin.py:584 msgid "right paren" msgstr "oikea sulku" #: plugin.py:585 msgid "asterisk" msgstr "tähti" #: plugin.py:586 msgid "plus" msgstr "plus" #: plugin.py:587 msgid "comma" msgstr "pilkku" #: plugin.py:588 msgid "minus" msgstr "miinus" #: plugin.py:589 msgid "period" msgstr "piste" #: plugin.py:590 msgid "slash" msgstr "kauttaviiva" #: plugin.py:591 msgid "colon" msgstr "puolipiste" #: plugin.py:592 msgid "semicolon" msgstr "puoli piste" #: plugin.py:593 msgid "less than" msgstr "pienempi kuin" #: plugin.py:594 msgid "equals" msgstr "täsmäävä" #: plugin.py:595 msgid "greater than" msgstr "suurempi kuin" #: plugin.py:596 msgid "question mark" msgstr "kysymysmerkki" #: plugin.py:597 msgid "at" msgstr "miukumauku" #: plugin.py:598 msgid "left bracket" msgstr "vasen hakasulku" #: plugin.py:599 msgid "backslash" msgstr "vasen kenoviiva" #: plugin.py:600 msgid "right bracket" msgstr "oikea hakasulku" #: plugin.py:601 msgid "caret" msgstr "sirkumfleksi" #: plugin.py:602 msgid "underscore" msgstr "alaviiva" #: plugin.py:603 #, fuzzy msgid "backtick" msgstr "heittomerkki" #: plugin.py:604 #, fuzzy msgid "left brace" msgstr "vasen laatikkosulku" #: plugin.py:605 msgid "pipe" msgstr "putki" #: plugin.py:606 #, fuzzy msgid "right brace" msgstr "oikea laatikkosulku" #: plugin.py:607 msgid "tilde" msgstr "vinoviiva" #: plugin.py:610 msgid "one" msgstr "yksi" #: plugin.py:610 msgid "three" msgstr "kolme" #: plugin.py:610 msgid "two" msgstr "kaksi" #: plugin.py:610 msgid "zero" msgstr "nolla" #: plugin.py:611 msgid "five" msgstr "viisi" #: plugin.py:611 msgid "four" msgstr "neljä" #: plugin.py:611 msgid "seven" msgstr "seitsemän" #: plugin.py:611 msgid "six" msgstr "kuusi" #: plugin.py:612 msgid "eight" msgstr "kahdeksan" #: plugin.py:612 msgid "nine" msgstr "yhdeksän" #: plugin.py:616 msgid "" "\n" "\n" " Returns , phonetically spelled out.\n" " " msgstr "" "\n" "\n" " Palauttaa , foneettisesti kirjoitettuna.\n" " " #: plugin.py:646 msgid "" "\n" "\n" " Returns as GNU/RMS would say it.\n" " " msgstr "" "\n" "\n" " Palauttaa kuin GNU/RMS sanoisi sen.\n" " " #: plugin.py:655 msgid "" "\n" "\n" " Returns with each word longer than\n" " supybot.plugins.Filter.shrink.minimum being shrunken (i.e., like\n" " \"internationalization\" becomes \"i18n\").\n" " " msgstr "" "\n" "\n" " Palauttaa jokainen sana joka on suurempi kuin\n" " supybot.plugins.Filter.shrink.minimum kutistettuna (esim., kuten\n" " \"internationalization\" tulee \"i18n\").\n" " " #: plugin.py:714 msgid "" "\n" "\n" " Returns rotated 180 degrees. Only really works for ASCII\n" " printable characters.\n" " " msgstr "" "\n" "\n" " Palauttaa 180 astetta käännettynä. Toimii vain ASCII\n" " tulostettavilla merkeillä.\n" " " #: plugin.py:740 #, fuzzy msgid "" "\n" "\n" " Capitalises the first letter of each word.\n" " " msgstr "" "\n" "\n" " Antaa annetun ketjun Morsen aakkosilla.\n" " " #: plugin.py:749 #, fuzzy msgid "" "\n" "\n" " EVERYONE LOVES CAPS LOCK.\n" " " msgstr "" "\n" "\n" " Kääntää .\n" " " #: plugin.py:758 #, fuzzy msgid "" "\n" "\n" " Returns with vowels rotated\n" " " msgstr "" "\n" "\n" " Palauttaa , foneettisesti kirjoitettuna.\n" " " #: plugin.py:769 #, fuzzy msgid "" "\n" "\n" " Returns in uwu-speak.\n" " " msgstr "" "\n" "\n" " Kääntää .\n" " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the lisping version of \n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa sammaltavan version .\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns with the l's made into r's and r's made into l's.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa l:ät muutettuna r:iksi made, jotka on tehty l:" #~ "ksi.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/locales/fr.po0000644000175000017500000003265014535072470017425 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:39 msgid "" "Determines whether or not to\n" " replace letters in the output of spellit." msgstr "" "Détermine si il faut ou non remplacer les lettres dans la sortie de spellit." #: config.py:42 msgid "" "Determines whether or not\n" " to replace punctuation in the output of spellit." msgstr "" "Détermine si il faut replacer la ponctuation dans la sortie de spellit." #: config.py:45 msgid "" "Determines whether or not to\n" " replace numbers in the output of spellit." msgstr "Détermine si on doit remplacer les nombres dans la sortie de spellit." #: config.py:49 msgid "" "Determines the minimum number of a letters\n" " in a word before it will be shrunken by the shrink command/filter." msgstr "" "Détermine le nombre minimum de lettre dans un mot pour qu'il soit coupé par " "la commande/le filtre shrink." #: plugin.py:55 msgid "" "This plugin offers several commands which transform text in some way.\n" " It also provides the capability of using such commands to 'filter' the\n" " output of the bot -- for instance, you could make everything the bot " "says\n" " be in leetspeak, or Morse code, or any number of other kinds of " "filters.\n" " Not very useful, but definitely quite fun :)" msgstr "" "Ce plugin offre quelques commandes qui peuvent être utilisées pour " "transformer du texte de différentes façons. Il fourni également la " "possiblité d'utiliser ces commandes pour 'filtrer' la sortie du bot ; par " "exemple, vous pouvez faire en sorte que tout ce que le bot dit le soit en " "l33tsp34k, en Morse, ou n'importe lequel des autres filtres. Pas très " "utile, mais plutôt fun :)" #: plugin.py:90 msgid "" "[] []\n" "\n" " Sets the outFilter of this plugin to be . If no command " "is\n" " given, unsets the outFilter. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Définit le filtre de sortie de ce plugin pour être . Si aucune " "commande n'est définie, supprime le filtre de sortie. n'est " "nécessaire que si la commande n'est pas envoyée sur le canal lui-même." #: plugin.py:103 msgid "That's not a valid filter command." msgstr "Ce n'est pas une commande de filtre valide" #: plugin.py:114 msgid "" "\n" "\n" " Removes all the vowels from . (If you're curious why this is\n" " named 'hebrew' it's because I (jemfinch) thought of it in Hebrew " "class,\n" " and printed Hebrew often elides the vowels.)\n" " " msgstr "" "\n" "\n" "Retire toutes les voyelles du (si vous êtes curieux de pourquoi elle " "s'appelle 'hebrew', c'est parce que je (jemfinch) pense que que en Hébreux, " "il manque souvent les voyelles)." #: plugin.py:128 msgid "" "\n" "\n" " Removes all the spaces from .\n" " " msgstr "" "\n" "\n" "Supprime tous les espaces du ." #: plugin.py:137 msgid "" "\n" "\n" " Returns , with all consecutive duplicated letters removed.\n" " " msgstr "" "\n" "\n" "Renvoie le texte, avec toutes les lettres consécutives dupliquées supprimées." #: plugin.py:150 msgid "" "\n" "\n" " Returns the binary representation of .\n" " " msgstr "" "\n" "\n" "Retourne la représentation binaire du ." #: plugin.py:184 msgid "" "\n" "\n" " Returns the character representation of binary .\n" " Assumes ASCII, 8 digits per character.\n" " " msgstr "" "\n" "\n" "Retourne le caractère représentant du . Considère qu'il s'agit " "d'ASCII, 8 bits par caractère." #: plugin.py:199 msgid "" "\n" "\n" " Returns a hexstring from the given string; a hexstring is a string\n" " composed of the hexadecimal value of each character in the string\n" " " msgstr "" "\n" "\n" "Retourne une chaîne héxadécimale à partir de la chaîne donnée ; une chaîne " "héxadécimale est une chaîne composée de la valeur héxadécimale de chaque " "caractère de la chaîne." #: plugin.py:210 msgid "" "\n" "\n" " Returns the string corresponding to . Obviously,\n" " must be a string of hexadecimal digits.\n" " " msgstr "" "\n" "\n" "Retourne la chaîne correspondant à la . Bien sûr, " " ne doit contenir que des caractères hexadécimaux." #: plugin.py:219 msgid "Invalid input." msgstr "Entrée invalide." #: plugin.py:225 msgid "" "\n" "\n" " Rotates 13 characters to the right in the alphabet. Rot13 " "is\n" " commonly used for text that simply needs to be hidden from " "inadvertent\n" " reading by roaming eyes, since it's easily reversible.\n" " " msgstr "" "\n" "\n" "Déplace chaque caractère du de 13 places vers la droite de " "l'alphabet. Rot13 est courremment utilisé pour les textes qui doivent être " "cachés des yeux indiscrets, mais être facilement reversible." #: plugin.py:246 msgid "" "\n" "\n" " Returns the l33tspeak version of \n" " " msgstr "" "\n" "\n" "Retourne la version l33t du ." #: plugin.py:266 msgid "" "\n" "\n" " Replies with an especially k-rad translation of .\n" " " msgstr "" "\n" "\n" "Répond avec une traduction k-rad du ." #: plugin.py:282 msgid "" "\n" "\n" " Replies with a string where each word is scrambled; i.e., each " "internal\n" " letter (that is, all letters but the first and last) are shuffled.\n" " " msgstr "" "\n" "\n" "Répond avec une chaîne où chaque mot est mélangé ; c'est à dire que chaque " "lettre interne (=toute lettre qui n'est pas la première ni la dernière) est " "mélangée avec les autres." #: plugin.py:347 msgid "" "\n" "\n" " Does the reverse of the morse command.\n" " " msgstr "" "\n" "\n" "Fait l'inverse de la commande morse." #: plugin.py:364 msgid "" "\n" "\n" " Gives the Morse code equivalent of a given string.\n" " " msgstr "" "\n" "\n" "Donne le code Morse équivalent à la chaîne donnée." #: plugin.py:376 msgid "" "\n" "\n" " Reverses .\n" " " msgstr "" "\n" "\n" "Inverse le ." #: plugin.py:394 msgid "" "\n" "\n" " Returns with each character randomly colorized.\n" " " msgstr "" "\n" "\n" "Retourne le avec chaque caractère coloré de façon aléatoire." #: plugin.py:409 msgid "" "\n" "\n" " Returns colorized like a rainbow.\n" " " msgstr "" "\n" "\n" "Retourne le texte colorisé comme un arc-en-ciel." #: plugin.py:426 #, fuzzy msgid "" "\n" "\n" " Strips bold, underline, and colors from ." msgstr "" "\n" "\n" "Supprime tous les espaces du ." #: plugin.py:433 msgid "" "\n" "\n" " Returns stripped of all color codes.\n" " " msgstr "" "\n" "\n" "Retourne le texte en retirant tous les codes de couleur" #: plugin.py:442 #, fuzzy msgid "" "\n" "\n" " Returns as if an AOL user had said it.\n" " " msgstr "" "\n" "\n" "Retourne le comme si un AOLuser l'avait dit." #: plugin.py:469 msgid "" "\n" "\n" " Returns as if JeffK had said it himself.\n" " " msgstr "" "\n" "\n" "Retourne le comme si JeffK l'avait dit lui-même." #: plugin.py:565 msgid "ay" msgstr "ah" #: plugin.py:565 msgid "bee" msgstr "bé" #: plugin.py:565 msgid "dee" msgstr "dé" #: plugin.py:565 msgid "see" msgstr "cé" #: plugin.py:566 msgid "aych" msgstr "ache" #: plugin.py:566 msgid "ee" msgstr "euh" #: plugin.py:566 msgid "eff" msgstr "èf" #: plugin.py:566 msgid "gee" msgstr "gé" #: plugin.py:567 msgid "ell" msgstr "èl" #: plugin.py:567 msgid "eye" msgstr "ih" #: plugin.py:567 msgid "jay" msgstr "ji" #: plugin.py:567 msgid "kay" msgstr "ka" #: plugin.py:568 msgid "cue" msgstr "cu" #: plugin.py:568 msgid "em" msgstr "èm" #: plugin.py:568 msgid "en" msgstr "èn" #: plugin.py:568 msgid "oh" msgstr "oh" #: plugin.py:568 msgid "pee" msgstr "pé" #: plugin.py:569 msgid "arr" msgstr "ère" #: plugin.py:569 msgid "ess" msgstr "èce" #: plugin.py:569 msgid "tee" msgstr "té" #: plugin.py:569 msgid "you" msgstr "uh" #: plugin.py:570 msgid "double-you" msgstr "double-vé" #: plugin.py:570 msgid "ecks" msgstr "icks" #: plugin.py:570 msgid "vee" msgstr "vé" #: plugin.py:570 msgid "why" msgstr "i-grec" #: plugin.py:571 msgid "zee" msgstr "zèd" #: plugin.py:576 msgid "exclamation point" msgstr "point d'exclamation" #: plugin.py:577 msgid "quote" msgstr "guillemet double" #: plugin.py:578 msgid "pound" msgstr "livre" #: plugin.py:579 msgid "dollar sign" msgstr "signe du dollar" #: plugin.py:580 msgid "percent" msgstr "pourcent" #: plugin.py:581 msgid "ampersand" msgstr "espèrluette" #: plugin.py:582 msgid "single quote" msgstr "guillemet" #: plugin.py:583 msgid "left paren" msgstr "parenthèse ouvrante" #: plugin.py:584 msgid "right paren" msgstr "parenthèse fermante" #: plugin.py:585 msgid "asterisk" msgstr "asterisque" #: plugin.py:586 msgid "plus" msgstr "plus" #: plugin.py:587 msgid "comma" msgstr "virgule" #: plugin.py:588 msgid "minus" msgstr "moins" #: plugin.py:589 msgid "period" msgstr "point" #: plugin.py:590 msgid "slash" msgstr "slash" #: plugin.py:591 msgid "colon" msgstr "double-point" #: plugin.py:592 msgid "semicolon" msgstr "point-virgule" #: plugin.py:593 msgid "less than" msgstr "inférieur" #: plugin.py:594 msgid "equals" msgstr "moins que" #: plugin.py:595 msgid "greater than" msgstr "supérieur" #: plugin.py:596 msgid "question mark" msgstr "point d'exclamation" #: plugin.py:597 msgid "at" msgstr "arobase" #: plugin.py:598 msgid "left bracket" msgstr "crochet ouvrant" #: plugin.py:599 msgid "backslash" msgstr "anti-slash" #: plugin.py:600 msgid "right bracket" msgstr "crochet fermant" #: plugin.py:601 msgid "caret" msgstr "accent circonflexe" #: plugin.py:602 msgid "underscore" msgstr "underscore" #: plugin.py:603 msgid "backtick" msgstr "accent grave" #: plugin.py:604 msgid "left brace" msgstr "crochet ouvrant" #: plugin.py:605 msgid "pipe" msgstr "pipe" #: plugin.py:606 msgid "right brace" msgstr "crochet fermant" #: plugin.py:607 msgid "tilde" msgstr "tilde" #: plugin.py:610 msgid "one" msgstr "un" #: plugin.py:610 msgid "three" msgstr "trois" #: plugin.py:610 msgid "two" msgstr "deux" #: plugin.py:610 msgid "zero" msgstr "zéro" #: plugin.py:611 msgid "five" msgstr "cinq" #: plugin.py:611 msgid "four" msgstr "quatre" #: plugin.py:611 msgid "seven" msgstr "sept" #: plugin.py:611 msgid "six" msgstr "six" #: plugin.py:612 msgid "eight" msgstr "huit" #: plugin.py:612 msgid "nine" msgstr "neuf" #: plugin.py:616 msgid "" "\n" "\n" " Returns , phonetically spelled out.\n" " " msgstr "" "\n" "\n" "Retourne le , épellé phonétiquement" #: plugin.py:646 msgid "" "\n" "\n" " Returns as GNU/RMS would say it.\n" " " msgstr "" "\n" "\n" "Retourne le comme si GNU/RMS l'avait dite." #: plugin.py:655 msgid "" "\n" "\n" " Returns with each word longer than\n" " supybot.plugins.Filter.shrink.minimum being shrunken (i.e., like\n" " \"internationalization\" becomes \"i18n\").\n" " " msgstr "" "\n" "\n" "Retourne le texte avec chaque mot plus long que supybot.plugins.Filter." "shrink.minimum découpé (par exemple, \"internationalization\" devient i18n)" #: plugin.py:714 msgid "" "\n" "\n" " Returns rotated 180 degrees. Only really works for ASCII\n" " printable characters.\n" " " msgstr "" "\n" "\n" "Retourne le tourné à 180 degrés. Ne marche pour de bon qu'avec des " "caractères ASCII imprimables" #: plugin.py:740 #, fuzzy msgid "" "\n" "\n" " Capitalises the first letter of each word.\n" " " msgstr "" "\n" "\n" "Donne le code Morse équivalent à la chaîne donnée." #: plugin.py:749 #, fuzzy msgid "" "\n" "\n" " EVERYONE LOVES CAPS LOCK.\n" " " msgstr "" "\n" "\n" "Inverse le ." #: plugin.py:758 #, fuzzy msgid "" "\n" "\n" " Returns with vowels rotated\n" " " msgstr "" "\n" "\n" "Retourne le , épellé phonétiquement" #: plugin.py:769 #, fuzzy msgid "" "\n" "\n" " Returns in uwu-speak.\n" " " msgstr "" "\n" "\n" "Inverse le ." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the lisping version of \n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne la version zézéyée du texte." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns with the l's made into r's and r's made into l's.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne le avec les l transormés en r et les r transformés en l." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/locales/it.po0000644000175000017500000003373514535072470017437 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-19 22:51+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:39 msgid "" "Determines whether or not to\n" " replace letters in the output of spellit." msgstr "Determina se sostituire o meno le lettere nell'output di spellit." #: config.py:42 msgid "" "Determines whether or not\n" " to replace punctuation in the output of spellit." msgstr "" "Determina se sostituire o meno la punteggiatura nell'output di spellit." #: config.py:45 msgid "" "Determines whether or not to\n" " replace numbers in the output of spellit." msgstr "Determina se sostituire o meno i numeri nell'output di spellit." #: config.py:49 msgid "" "Determines the minimum number of a letters\n" " in a word before it will be shrunken by the shrink command/filter." msgstr "" "Determina il numero minimo di lettere in una parola prima di essere\n" " abbreviata dal comando shrink." #: plugin.py:55 msgid "" "This plugin offers several commands which transform text in some way.\n" " It also provides the capability of using such commands to 'filter' the\n" " output of the bot -- for instance, you could make everything the bot " "says\n" " be in leetspeak, or Morse code, or any number of other kinds of " "filters.\n" " Not very useful, but definitely quite fun :)" msgstr "" "Questo plugin offre svariati comandi che trasformano il testo in vari modi.\n" " Fornisce anche la possibilità di utilizzare questi comandi per \"filtrare" "\"\n" " l'output del bot; è possibile, ad esempio, far sì che qualsiasi cosa " "dica\n" " sia in l33t5p34k, codice Morse o altri tipi di trasformazione.\n" " Non molto utile ma decisamente divertente :)" #: plugin.py:90 msgid "" "[] []\n" "\n" " Sets the outFilter of this plugin to be . If no command " "is\n" " given, unsets the outFilter. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Imposta il filtro di uscita di questo plugin per essere .\n" " Se non viene specificato alcun comando il filtro sarà disattivato.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:103 msgid "That's not a valid filter command." msgstr "Questo non è un filtro valido." #: plugin.py:114 msgid "" "\n" "\n" " Removes all the vowels from . (If you're curious why this is\n" " named 'hebrew' it's because I (jemfinch) thought of it in Hebrew " "class,\n" " and printed Hebrew often elides the vowels.)\n" " " msgstr "" "\n" "\n" " Rimuove tutte le vocali da (se siete curiosi di sapere come " "mai si\n" " chiama \"hebrew\" è perché io (jemfinch) l'ho pensata in ebraico, le " "stampe\n" " ebraiche sono spesso prive di vocali).\n" " " #: plugin.py:128 msgid "" "\n" "\n" " Removes all the spaces from .\n" " " msgstr "" "\n" "\n" " Rimuove tutti gli spazi da .\n" " " #: plugin.py:137 msgid "" "\n" "\n" " Returns , with all consecutive duplicated letters removed.\n" " " msgstr "" "\n" "\n" " Restituisce rimuovendo tutte le lettere doppie consecutive.\n" " " #: plugin.py:150 msgid "" "\n" "\n" " Returns the binary representation of .\n" " " msgstr "" "\n" "\n" " Restituisce la rappresentazione binaria di .\n" " " #: plugin.py:184 msgid "" "\n" "\n" " Returns the character representation of binary .\n" " Assumes ASCII, 8 digits per character.\n" " " msgstr "" "\n" "\n" " Restituisce la rappresentazione in caratteri di \n" " binario. Presuppone sia ASCII, 8 cifre per carattere.\n" " " #: plugin.py:199 msgid "" "\n" "\n" " Returns a hexstring from the given string; a hexstring is a string\n" " composed of the hexadecimal value of each character in the string\n" " " msgstr "" "\n" "\n" " Restituisce una stringa esadecimale dalla stringa data; una stringa\n" " esadecimale è una stringa composta dal valore esadecimale di ogni " "carattere.\n" " " #: plugin.py:210 msgid "" "\n" "\n" " Returns the string corresponding to . Obviously,\n" " must be a string of hexadecimal digits.\n" " " msgstr "" "\n" "\n" " Restituisce la stringa corrispondente a ,\n" " quest'ultima deve ovviamente contenere caratteri esadecimali.\n" " " #: plugin.py:219 msgid "Invalid input." msgstr "Input non valido." #: plugin.py:225 msgid "" "\n" "\n" " Rotates 13 characters to the right in the alphabet. Rot13 " "is\n" " commonly used for text that simply needs to be hidden from " "inadvertent\n" " reading by roaming eyes, since it's easily reversible.\n" " " msgstr "" "\n" "\n" " Sposta ogni carattere di 13 posizioni a destra " "nell'alfabeto.\n" " Giacché Rot13 è facilmente reversibile, è comunemente usato per " "offuscare\n" " del testo da letture involontarie.\n" " " #: plugin.py:246 msgid "" "\n" "\n" " Returns the l33tspeak version of \n" " " msgstr "" "\n" "\n" " Restituisce la versione l33t di \n" " " #: plugin.py:266 msgid "" "\n" "\n" " Replies with an especially k-rad translation of .\n" " " msgstr "" "\n" "\n" " Risponde con una particolare traduzione k-rad di .\n" " " #: plugin.py:282 msgid "" "\n" "\n" " Replies with a string where each word is scrambled; i.e., each " "internal\n" " letter (that is, all letters but the first and last) are shuffled.\n" " " msgstr "" "\n" "\n" " Risponde con una stringa dove ogni parola è mescolata; ovvero ogni " "lettera\n" " interna (tutte le lettere tranne la prima e l'ultima) è mischiata " "con le altre.\n" " " #: plugin.py:347 msgid "" "\n" "\n" " Does the reverse of the morse command.\n" " " msgstr "" "\n" "\n" " Fa il contrario del comando morse.\n" " " #: plugin.py:364 msgid "" "\n" "\n" " Gives the Morse code equivalent of a given string.\n" " " msgstr "" "\n" "\n" " Mostra il codice Morse equivalente alla stringa fornita.\n" " " #: plugin.py:376 msgid "" "\n" "\n" " Reverses .\n" " " msgstr "" "\n" "\n" " Riporta in maniera speculare.\n" " " #: plugin.py:394 msgid "" "\n" "\n" " Returns with each character randomly colorized.\n" " " msgstr "" "\n" "\n" " Restituisce con ogni carattere colorato in modo casuale.\n" " " #: plugin.py:409 msgid "" "\n" "\n" " Returns colorized like a rainbow.\n" " " msgstr "" "\n" "\n" " Restituisce colorato come un arcobaleno.\n" " " #: plugin.py:426 #, fuzzy msgid "" "\n" "\n" " Strips bold, underline, and colors from ." msgstr "" "\n" "\n" " Rimuove tutti gli spazi da .\n" " " #: plugin.py:433 msgid "" "\n" "\n" " Returns stripped of all color codes.\n" " " msgstr "" "\n" "\n" " Restituisce privato di tutti i codici colore.\n" " " #: plugin.py:442 #, fuzzy msgid "" "\n" "\n" " Returns as if an AOL user had said it.\n" " " msgstr "" "\n" "\n" " Restituisce come pronunciato da un utente AOL.\n" " " #: plugin.py:469 msgid "" "\n" "\n" " Returns as if JeffK had said it himself.\n" " " msgstr "" "\n" "\n" " Restituisce come pronunciato da JeffK.\n" " " #: plugin.py:565 msgid "ay" msgstr "a" #: plugin.py:565 msgid "bee" msgstr "bi" #: plugin.py:565 msgid "dee" msgstr "di" #: plugin.py:565 msgid "see" msgstr "ci" #: plugin.py:566 msgid "aych" msgstr "acca" #: plugin.py:566 msgid "ee" msgstr "e" #: plugin.py:566 msgid "eff" msgstr "effe" #: plugin.py:566 msgid "gee" msgstr "gi" #: plugin.py:567 msgid "ell" msgstr "elle" #: plugin.py:567 msgid "eye" msgstr "i" #: plugin.py:567 msgid "jay" msgstr "gei" #: plugin.py:567 msgid "kay" msgstr "cappa" #: plugin.py:568 msgid "cue" msgstr "cu" #: plugin.py:568 msgid "em" msgstr "emme" #: plugin.py:568 msgid "en" msgstr "enne" #: plugin.py:568 msgid "oh" msgstr "o" #: plugin.py:568 msgid "pee" msgstr "pi" #: plugin.py:569 msgid "arr" msgstr "erre" #: plugin.py:569 msgid "ess" msgstr "esse" #: plugin.py:569 msgid "tee" msgstr "ti" #: plugin.py:569 msgid "you" msgstr "u" #: plugin.py:570 msgid "double-you" msgstr "doppia vu" #: plugin.py:570 msgid "ecks" msgstr "ics" #: plugin.py:570 msgid "vee" msgstr "vi" #: plugin.py:570 msgid "why" msgstr "ipsilon" #: plugin.py:571 msgid "zee" msgstr "zeta" #: plugin.py:576 msgid "exclamation point" msgstr "punto esclamativo" #: plugin.py:577 msgid "quote" msgstr "apice doppio" #: plugin.py:578 msgid "pound" msgstr "cancelletto" #: plugin.py:579 msgid "dollar sign" msgstr "dollaro" #: plugin.py:580 msgid "percent" msgstr "percentuale" #: plugin.py:581 msgid "ampersand" msgstr "e commerciale" #: plugin.py:582 msgid "single quote" msgstr "apice singolo" #: plugin.py:583 msgid "left paren" msgstr "parentesi tonda sinistra" #: plugin.py:584 msgid "right paren" msgstr "parentesi tonda destra" #: plugin.py:585 msgid "asterisk" msgstr "asterisco" #: plugin.py:586 msgid "plus" msgstr "più" #: plugin.py:587 msgid "comma" msgstr "virgola" #: plugin.py:588 msgid "minus" msgstr "meno" #: plugin.py:589 msgid "period" msgstr "punto" #: plugin.py:590 msgid "slash" msgstr "slash" #: plugin.py:591 msgid "colon" msgstr "due punti" #: plugin.py:592 msgid "semicolon" msgstr "punto e virgola" #: plugin.py:593 msgid "less than" msgstr "minore" #: plugin.py:594 msgid "equals" msgstr "uguale" #: plugin.py:595 msgid "greater than" msgstr "maggiore" #: plugin.py:596 msgid "question mark" msgstr "punto interrogativo" #: plugin.py:597 msgid "at" msgstr "chiocciola" #: plugin.py:598 msgid "left bracket" msgstr "parentesi quadra sinistra" #: plugin.py:599 msgid "backslash" msgstr "backslash" #: plugin.py:600 msgid "right bracket" msgstr "parentesi quadra destra" #: plugin.py:601 msgid "caret" msgstr "circonflesso" #: plugin.py:602 msgid "underscore" msgstr "trattino basso" #: plugin.py:603 msgid "backtick" msgstr "accento grave" #: plugin.py:604 msgid "left brace" msgstr "parentesi graffa sinistra" #: plugin.py:605 msgid "pipe" msgstr "pipe" #: plugin.py:606 msgid "right brace" msgstr "parentesi graffa destra" #: plugin.py:607 msgid "tilde" msgstr "tilde" #: plugin.py:610 msgid "one" msgstr "uno" #: plugin.py:610 msgid "three" msgstr "tre" #: plugin.py:610 msgid "two" msgstr "due" #: plugin.py:610 msgid "zero" msgstr "zero" #: plugin.py:611 msgid "five" msgstr "cinque" #: plugin.py:611 msgid "four" msgstr "quattro" #: plugin.py:611 msgid "seven" msgstr "sette" #: plugin.py:611 msgid "six" msgstr "sei" #: plugin.py:612 msgid "eight" msgstr "otto" #: plugin.py:612 msgid "nine" msgstr "nove" #: plugin.py:616 msgid "" "\n" "\n" " Returns , phonetically spelled out.\n" " " msgstr "" "\n" "\n" " Restituisce con trascrizione fonetica (spelling).\n" " " #: plugin.py:646 msgid "" "\n" "\n" " Returns as GNU/RMS would say it.\n" " " msgstr "" "\n" "\n" " Restituisce come pronunciato da GNU/RMS.\n" " " #: plugin.py:655 msgid "" "\n" "\n" " Returns with each word longer than\n" " supybot.plugins.Filter.shrink.minimum being shrunken (i.e., like\n" " \"internationalization\" becomes \"i18n\").\n" " " msgstr "" "\n" "\n" " Restituisce con ogni parola più lunga di\n" " supybot.plugins.Filter.shrink.minimum abbreviata\n" " (ad esempio \"internationalization\" diventa \"i18n\").\n" " " #: plugin.py:714 msgid "" "\n" "\n" " Returns rotated 180 degrees. Only really works for ASCII\n" " printable characters.\n" " " msgstr "" "\n" "\n" " Restituisce ruotato di 180 gradi. Funziona solo con " "caratteri ASCII stampabili.\n" " " #: plugin.py:740 #, fuzzy msgid "" "\n" "\n" " Capitalises the first letter of each word.\n" " " msgstr "" "\n" "\n" " Mostra il codice Morse equivalente alla stringa fornita.\n" " " #: plugin.py:749 #, fuzzy msgid "" "\n" "\n" " EVERYONE LOVES CAPS LOCK.\n" " " msgstr "" "\n" "\n" " Riporta in maniera speculare.\n" " " #: plugin.py:758 #, fuzzy msgid "" "\n" "\n" " Returns with vowels rotated\n" " " msgstr "" "\n" "\n" " Restituisce con trascrizione fonetica (spelling).\n" " " #: plugin.py:769 #, fuzzy msgid "" "\n" "\n" " Returns in uwu-speak.\n" " " msgstr "" "\n" "\n" " Riporta in maniera speculare.\n" " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the lisping version of \n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce la versione blesa di \n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns with the l's made into r's and r's made into l's.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce con le elle trasformate in erre e viceversa.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/plugin.py0000644000175000017500000006523614535072470016712 0ustar00valval# -*- encoding: utf-8 -*- ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 __future__ import unicode_literals import re import sys import codecs import string import random import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.utils.minisix as minisix import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Filter') class MyFilterProxy(object): def reply(self, s): self.s = s class Filter(callbacks.Plugin): """This plugin offers several commands which transform text in some way. It also provides the capability of using such commands to 'filter' the output of the bot -- for instance, you could make everything the bot says be in leetspeak, or Morse code, or any number of other kinds of filters. Not very useful, but definitely quite fun :)""" def __init__(self, irc): self.__parent = super(Filter, self) self.__parent.__init__(irc) self.outFilters = ircutils.IrcDict() def outFilter(self, irc, msg): if msg.command in ('PRIVMSG', 'NOTICE'): if msg.channel in self.outFilters: if ircmsgs.isAction(msg): s = ircmsgs.unAction(msg) else: s = msg.args[1] methods = self.outFilters[msg.channel] for filtercommand in methods: myIrc = MyFilterProxy() filtercommand(myIrc, msg, [s]) s = myIrc.s if ircmsgs.isAction(msg): msg = ircmsgs.action(msg.args[0], s, msg=msg) else: msg = ircmsgs.IrcMsg(msg=msg, args=(msg.args[0], s)) return msg _filterCommands = ['jeffk', 'leet', 'rot13', 'hexlify', 'binary', 'scramble', 'morse', 'reverse', 'colorize', 'squish', 'supa1337', 'stripcolor', 'aol', 'rainbow', 'spellit', 'hebrew', 'undup', 'uwu', 'gnu', 'shrink', 'uniud', 'capwords', 'caps', 'vowelrot', 'stripformatting'] @internationalizeDocstring def outfilter(self, irc, msg, args, channel, command): """[] [] Sets the outFilter of this plugin to be . If no command is given, unsets the outFilter. is only necessary if the message isn't sent in the channel itself. """ if command: if not self.isDisabled(command) and \ command in self._filterCommands: method = getattr(self, command) self.outFilters.setdefault(channel, []).append(method) irc.replySuccess() else: irc.error(_('That\'s not a valid filter command.')) else: self.outFilters[channel] = [] irc.replySuccess() outfilter = wrap(outfilter, [('checkChannelCapability', 'op'), additional('commandName')]) _hebrew_remover = utils.str.MultipleRemover('aeiou') @internationalizeDocstring def hebrew(self, irc, msg, args, text): """ Removes all the vowels from . (If you're curious why this is named 'hebrew' it's because I (jemfinch) thought of it in Hebrew class, and printed Hebrew often elides the vowels.) """ irc.reply(self._hebrew_remover(text)) hebrew = wrap(hebrew, ['text']) def _squish(self, text): return text.replace(' ', '') @internationalizeDocstring def squish(self, irc, msg, args, text): """ Removes all the spaces from . """ irc.reply(self._squish(text)) squish = wrap(squish, ['text']) @internationalizeDocstring def undup(self, irc, msg, args, text): """ Returns , with all consecutive duplicated letters removed. """ L = [text[0]] for c in text: if c != L[-1]: L.append(c) irc.reply(''.join(L)) undup = wrap(undup, ['text']) @internationalizeDocstring def binary(self, irc, msg, args, text): """ Returns the binary representation of . """ L = [] if minisix.PY3: if isinstance(text, str): bytes_ = text.encode() else: bytes_ = text else: if isinstance(text, unicode): text = text.encode() bytes_ = map(ord, text) for i in bytes_: LL = [] assert i<=256 counter = 8 while i: counter -= 1 if i & 1: LL.append('1') else: LL.append('0') i >>= 1 while counter: LL.append('0') counter -= 1 LL.reverse() L.extend(LL) irc.reply(''.join(L)) binary = wrap(binary, ['text']) def unbinary(self, irc, msg, args, text): """ Returns the character representation of binary . Assumes ASCII, 8 digits per character. """ text = self._squish(text) # Strip spaces. try: L = [chr(int(text[i:(i+8)], 2)) for i in range(0, len(text), 8)] irc.reply(''.join(L)) except ValueError: irc.errorInvalid('binary string', text) unbinary = wrap(unbinary, ['text']) _hex_encoder = staticmethod(codecs.getencoder('hex_codec')) def hexlify(self, irc, msg, args, text): """ Returns a hexstring from the given string; a hexstring is a string composed of the hexadecimal value of each character in the string """ irc.reply(self._hex_encoder(text.encode('utf8'))[0].decode('utf8')) hexlify = wrap(hexlify, ['text']) _hex_decoder = staticmethod(codecs.getdecoder('hex_codec')) @internationalizeDocstring def unhexlify(self, irc, msg, args, text): """ Returns the string corresponding to . Obviously, must be a string of hexadecimal digits. """ try: irc.reply(self._hex_decoder(text.encode('utf8'))[0] .decode('utf8', 'replace')) except TypeError: irc.error(_('Invalid input.')) unhexlify = wrap(unhexlify, ['text']) _rot13_encoder = codecs.getencoder('rot-13') @internationalizeDocstring def rot13(self, irc, msg, args, text): """ Rotates 13 characters to the right in the alphabet. Rot13 is commonly used for text that simply needs to be hidden from inadvertent reading by roaming eyes, since it's easily reversible. """ if minisix.PY2: text = text.decode('utf8') irc.reply(self._rot13_encoder(text)[0]) rot13 = wrap(rot13, ['text']) _leettrans = utils.str.MultipleReplacer(dict(list(zip('oOaAeElBTiIts', '004433187!1+5')))) _leetres = [(re.compile(r'\b(?:(?:[yY][o0O][oO0uU])|u)\b'), 'j00'), (re.compile(r'fear'), 'ph33r'), (re.compile(r'[aA][tT][eE]'), '8'), (re.compile(r'[aA][tT]'), '@'), (re.compile(r'[sS]\b'), 'z'), (re.compile(r'x'), '><'),] @internationalizeDocstring def leet(self, irc, msg, args, text): """ Returns the l33tspeak version of """ for (r, sub) in self._leetres: text = re.sub(r, sub, text) text = self._leettrans(text) irc.reply(text) leet = wrap(leet, ['text']) _supaleetreplacers = [('xX', '><'), ('kK', '|<'), ('rR', '|2'), ('hH', '|-|'), ('L', '|_'), ('uU', '|_|'), ('O', '()'), ('nN', '|\\|'), ('mM', '/\\/\\'), ('G', '6'), ('Ss', '$'), ('i', ';'), ('aA', '/-\\'), ('eE', '3'), ('t', '+'), ('T', '7'), ('l', '1'), ('D', '|)'), ('B', '|3'), ('I', ']['), ('Vv', '\\/'), ('wW', '\\/\\/'), ('d', 'c|'), ('b', '|>'), ('c', '<'), ('h', '|n'),] @internationalizeDocstring def supa1337(self, irc, msg, args, text): """ Replies with an especially k-rad translation of . """ for (r, sub) in self._leetres: text = re.sub(r, sub, text) for (letters, replacement) in self._supaleetreplacers: for letter in letters: text = text.replace(letter, replacement) irc.reply(text) supa1337 = wrap(supa1337, ['text']) _scrambleRe = re.compile(r'(?:\b|(?![a-zA-Z]))([a-zA-Z])([a-zA-Z]*)' r'([a-zA-Z])(?:\b|(?![a-zA-Z]))') @internationalizeDocstring def scramble(self, irc, msg, args, text): """ Replies with a string where each word is scrambled; i.e., each internal letter (that is, all letters but the first and last) are shuffled. """ def _subber(m): L = list(m.group(2)) random.shuffle(L) return '%s%s%s' % (m.group(1), ''.join(L), m.group(3)) s = self._scrambleRe.sub(_subber, text) irc.reply(s) scramble = wrap(scramble, ['text']) _morseCode = { "A" : ".-", "B" : "-...", "C" : "-.-.", "D" : "-..", "E" : ".", "F" : "..-.", "G" : "--.", "H" : "....", "I" : "..", "J" : ".---", "K" : "-.-", "L" : ".-..", "M" : "--", "N" : "-.", "O" : "---", "P" : ".--.", "Q" : "--.-", "R" : ".-.", "S" : "...", "T" : "-", "U" : "..-", "V" : "...-", "W" : ".--", "X" : "-..-", "Y" : "-.--", "Z" : "--..", "0" : "-----", "1" : ".----", "2" : "..---", "3" : "...--", "4" : "....-", "5" : ".....", "6" : "-....", "7" : "--...", "8" : "---..", "9" : "----.", "." : ".-.-.-", "," : "--..--", ":" : "---...", "?" : "..--..", "'" : ".----.", "-" : "-....-", "/" : "-..-.", '"' : ".-..-.", "@" : ".--.-.", "=" : "-...-" } _revMorseCode = dict([(y, x) for (x, y) in _morseCode.items()]) _unmorsere = re.compile('([.-]+)') @internationalizeDocstring def unmorse(self, irc, msg, args, text): """ Does the reverse of the morse command. """ text = text.replace('_', '-') def morseToLetter(m): s = m.group(1) return self._revMorseCode.get(s, s) text = self._unmorsere.sub(morseToLetter, text) text = text.replace(' ', '\x00') text = text.replace(' ', '') text = text.replace('\x00', ' ') irc.reply(text) unmorse = wrap(unmorse, ['text']) @internationalizeDocstring def morse(self, irc, msg, args, text): """ Gives the Morse code equivalent of a given string. """ L = [] for c in text.upper(): L.append(self._morseCode.get(c, c)) irc.reply(' '.join(L)) morse = wrap(morse, ['text']) @internationalizeDocstring def reverse(self, irc, msg, args, text): """ Reverses . """ irc.reply(text[::-1]) reverse = wrap(reverse, ['text']) @internationalizeDocstring def _color(self, c, fg=None): if c == ' ': return c if fg is None: fg = random.randint(2, 15) fg = str(fg).zfill(2) return '\x03%s%s' % (fg, c) @internationalizeDocstring def colorize(self, irc, msg, args, text): """ Returns with each character randomly colorized. """ if minisix.PY2: text = text.decode('utf-8') text = ircutils.stripColor(text) L = [self._color(c) for c in text] if minisix.PY2: L = [c.encode('utf-8') for c in L] irc.reply('%s%s' % (''.join(L), '\x03')) colorize = wrap(colorize, ['text']) @internationalizeDocstring def rainbow(self, irc, msg, args, text): """ Returns colorized like a rainbow. """ if minisix.PY2: text = text.decode('utf-8') text = ircutils.stripColor(text) colors = utils.iter.cycle(['05', '04', '07', '08', '09', '03', '11', '10', '12', '02', '06', '13']) L = [self._color(c, fg=next(colors)) for c in text] if minisix.PY2: L = [c.encode('utf-8') for c in L] irc.reply(''.join(L) + '\x03') rainbow = wrap(rainbow, ['text']) @wrap(['text']) def stripformatting(self, irc, msg, args, text): """ Strips bold, underline, and colors from .""" irc.reply(ircutils.stripFormatting(text)) @internationalizeDocstring def stripcolor(self, irc, msg, args, text): """ Returns stripped of all color codes. """ irc.reply(ircutils.stripColor(text)) stripcolor = wrap(stripcolor, ['text']) @internationalizeDocstring def aol(self, irc, msg, args, text): """ Returns as if an AOL user had said it. """ text = text.replace(' you ', ' u ') text = text.replace(' are ', ' r ') text = text.replace(' love ', ' <3 ') text = text.replace(' luv ', ' <3 ') text = text.replace(' too ', ' 2 ') text = text.replace(' to ', ' 2 ') text = text.replace(' two ', ' 2 ') text = text.replace('fore', '4') text = text.replace(' for ', ' 4 ') text = text.replace('be', 'b') text = text.replace('four', ' 4 ') text = text.replace(' their ', ' there ') text = text.replace(', ', ' ') text = text.replace(',', ' ') text = text.replace("'", '') text = text.replace('one', '1') smiley = utils.iter.choice(['<3', ':)', ':-)', ':D', ':-D']) text += smiley*3 irc.reply(text) aol = wrap(aol, ['text']) @internationalizeDocstring def jeffk(self, irc, msg, args, text): """ Returns as if JeffK had said it himself. """ def randomlyPick(L): return utils.iter.choice(L) def quoteOrNothing(m): return randomlyPick(['"', '']).join(m.groups()) def randomlyReplace(s, probability=0.5): def f(m): if random.random() < probability: return m.expand(s) else: return m.group(0) return f def randomExclaims(m): if random.random() < 0.85: return ('!' * random.randrange(1, 5)) + m.group(1) else: return '.' + m.group(1) def randomlyShuffle(m): L = list(m.groups()) random.shuffle(L) return ''.join(L) def lessRandomlyShuffle(m): L = list(m.groups()) if random.random() < .4: random.shuffle(L) return ''.join(L) def randomlyLaugh(text, probability=.3): if random.random() < probability: if random.random() < .5: insult = utils.iter.choice([' fagot1', ' fagorts', ' jerks', 'fagot' ' jerk', 'dumbshoes', ' dumbshoe']) else: insult = '' laugh1 = utils.iter.choice(['ha', 'hah', 'lol', 'l0l', 'ahh']) laugh2 = utils.iter.choice(['ha', 'hah', 'lol', 'l0l', 'ahh']) laugh1 = laugh1 * random.randrange(1, 5) laugh2 = laugh2 * random.randrange(1, 5) exclaim = utils.iter.choice(['!', '~', '!~', '~!!~~', '!!~', '~~~!']) exclaim += utils.iter.choice(['!', '~', '!~', '~!!~~', '!!~', '~~~!']) if random.random() < 0.5: exclaim += utils.iter.choice(['!', '~', '!~', '~!!~~', '!!~', '~~~!']) laugh = ''.join([' ', laugh1, laugh2, insult, exclaim]) text += laugh return text if random.random() < .03: irc.reply(randomlyLaugh('NO YUO', probability=1)) return alwaysInsertions = { r'er\b': 'ar', r'\bthe\b': 'teh', r'\byou\b': 'yuo', r'\bis\b': 'si', r'\blike\b': 'liek', r'[^e]ing\b': 'eing', } for (r, s) in alwaysInsertions.items(): text = re.sub(r, s, text) randomInsertions = { r'i': 'ui', r'le\b': 'al', r'i': 'io', r'l': 'll', r'to': 'too', r'that': 'taht', r'[^s]c([ei])': r'sci\1', r'ed\b': r'e', r'\band\b': 'adn', r'\bhere\b': 'hear', r'\bthey\'re': 'their', r'\bthere\b': 'they\'re', r'\btheir\b': 'there', r'[^e]y': 'ey', } for (r, s) in randomInsertions.items(): text = re.sub(r, randomlyReplace(s), text) text = re.sub(r'(\w)\'(\w)', quoteOrNothing, text) text = re.sub(r'\.(\s+|$)', randomExclaims, text) text = re.sub(r'([aeiou])([aeiou])', randomlyShuffle, text) text = re.sub(r'([bcdfghkjlmnpqrstvwxyz])([bcdfghkjlmnpqrstvwxyz])', lessRandomlyShuffle, text) text = randomlyLaugh(text) if random.random() < .4: text = text.upper() irc.reply(text) jeffk = wrap(jeffk, ['text']) # Keeping these separate so people can just replace the alphabets for # whatever their language of choice _spellLetters = { 'a': _('ay'), 'b': _('bee'), 'c': _('see'), 'd': _('dee'), 'e': _('ee'), 'f': _('eff'), 'g': _('gee'), 'h': _('aych'), 'i': _('eye'), 'j': _('jay'), 'k': _('kay'), 'l': _('ell'), 'm': _('em'), 'n': _('en'), 'o': _('oh'), 'p': _('pee'), 'q': _('cue'), 'r': _('arr'), 's': _('ess'), 't': _('tee'), 'u': _('you'), 'v': _('vee'), 'w': _('double-you'), 'x': _('ecks'), 'y': _('why'), 'z': _('zee') } for (k, v) in list(_spellLetters.items()): _spellLetters[k.upper()] = v _spellPunctuation = { '!': _('exclamation point'), '"': _('quote'), '#': _('pound'), '$': _('dollar sign'), '%': _('percent'), '&': _('ampersand'), '\'': _('single quote'), '(': _('left paren'), ')': _('right paren'), '*': _('asterisk'), '+': _('plus'), ',': _('comma'), '-': _('minus'), '.': _('period'), '/': _('slash'), ':': _('colon'), ';': _('semicolon'), '<': _('less than'), '=': _('equals'), '>': _('greater than'), '?': _('question mark'), '@': _('at'), '[': _('left bracket'), '\\': _('backslash'), ']': _('right bracket'), '^': _('caret'), '_': _('underscore'), '`': _('backtick'), '{': _('left brace'), '|': _('pipe'), '}': _('right brace'), '~': _('tilde') } _spellNumbers = { '0': _('zero'), '1': _('one'), '2': _('two'), '3': _('three'), '4': _('four'), '5': _('five'), '6': _('six'), '7': _('seven'), '8': _('eight'), '9': _('nine') } @internationalizeDocstring def spellit(self, irc, msg, args, text): """ Returns , phonetically spelled out. """ d = {} if self.registryValue('spellit.replaceLetters'): d.update(self._spellLetters) if self.registryValue('spellit.replaceNumbers'): d.update(self._spellNumbers) if self.registryValue('spellit.replacePunctuation'): d.update(self._spellPunctuation) # A bug in unicode on OSX prevents me from testing this. ## dd = {} ## for (c, v) in d.items(): ## dd[ord(c)] = unicode(v + ' ') ## irc.reply(unicode(text).translate(dd)) out = minisix.io.StringIO() write = out.write for c in text: try: c = d[c] write(' ') except KeyError: pass write(c) irc.reply(out.getvalue().strip()) spellit = wrap(spellit, ['text']) @internationalizeDocstring def gnu(self, irc, msg, args, text): """ Returns as GNU/RMS would say it. """ irc.reply(' '.join(['GNU/' + s for s in text.split()])) gnu = wrap(gnu, ['text']) @internationalizeDocstring def shrink(self, irc, msg, args, text): """ Returns with each word longer than supybot.plugins.Filter.shrink.minimum being shrunken (i.e., like "internationalization" becomes "i18n"). """ L = [] minimum = self.registryValue('shrink.minimum', msg.channel, irc.network) r = re.compile(r'[A-Za-z]{%s,}' % minimum) def shrink(m): s = m.group(0) return ''.join((s[0], str(len(s)-2), s[-1])) text = r.sub(shrink, text) irc.reply(text) shrink = wrap(shrink, ['text']) # TODO: 2,4,; # XXX suckiest: B,K,P,Q,T # alternatives: 3: U+2107 _uniudMap = { ' ': ' ', '0': '0', '@': '@', '!': '\u00a1', '1': '1', 'A': '\u2200', '"': '\u201e', '2': '\u2681', 'B': 'q', '#': '#', '3': '\u0190', 'C': '\u0186', '$': '$', '4': '\u2683', 'D': '\u15e1', '%': '%', '5': '\u1515', 'E': '\u018e', '&': '\u214b', '6': '9', 'F': '\u2132', "'": '\u0375', '7': 'L', 'G': '\u2141', '(': ')', '8': '8', 'H': 'H', ')': '(', '9': '6', 'I': 'I', '*': '*', ':': ':', 'J': '\u148b', '+': '+', ';': ';', 'K': '\u029e', ',': '\u2018', '<': '>', 'L': '\u2142', '-': '-', '=': '=', 'M': '\u019c', '.': '\u02d9', '>': '<', 'N': 'N', '/': '/', '?': '\u00bf', 'O': 'O', 'P': 'd', '`': '\u02ce', 'p': 'd', 'Q': 'b', 'a': '\u0250', 'q': 'b', 'R': '\u1d1a', 'b': 'q', 'r': '\u0279', 'S': 'S', 'c': '\u0254', 's': 's', 'T': '\u22a5', 'd': 'p', 't': '\u0287', 'U': '\u144e', 'e': '\u01dd', '': 'n', 'V': '\u039b', 'f': '\u214e', 'v': '\u028c', 'W': 'M', 'g': '\u0253', 'w': '\u028d', 'X': 'X', 'h': '\u0265', 'x': 'x', 'Y': '\u2144', 'i': '\u1d09', 'y': '\u028e', 'Z': 'Z', 'j': '\u027f', 'z': 'z', '[': ']', 'k': '\u029e', '{': '}', '\\': '\\', 'l': '\u05df', '|': '|', ']': '[', 'm': '\u026f', '}': '{', '^': '\u2335', 'n': '', '~': '~', '_': '\u203e', 'o': 'o', } @internationalizeDocstring def uniud(self, irc, msg, args, text): """ Returns rotated 180 degrees. Only really works for ASCII printable characters. """ turned = [] tlen = 0 for c in text: if c in self._uniudMap: tmp = self._uniudMap[c] if not len(tmp): tmp = '\ufffd' turned.append(tmp) tlen += 1 elif c == '\t': tablen = 8 - tlen % 8 turned.append(' ' * tablen) tlen += tablen elif ord(c) >= 32: turned.append(c) tlen += 1 s = '%s \x02 \x02' % ''.join(reversed(turned)) irc.reply(s) uniud = wrap(uniud, ['text']) def capwords(self, irc, msg, args, text): """ Capitalises the first letter of each word. """ text = string.capwords(text) irc.reply(text) capwords = wrap(capwords, ['text']) def caps(self, irc, msg, args, text): """ EVERYONE LOVES CAPS LOCK. """ irc.reply(text.upper()) caps = wrap(caps, ['text']) _vowelrottrans = utils.str.MultipleReplacer(dict(list(zip('aeiouAEIOU', 'eiouaEIOUA')))) def vowelrot(self, irc, msg, args, text): """ Returns with vowels rotated """ text = self._vowelrottrans(text) irc.reply(text) vowelrot = wrap(vowelrot, ['text']) _uwutrans = utils.str.MultipleReplacer(dict(list(zip('lrLR', 'wwWW')))) def uwu(self, irc, msg, args, text): """ Returns in uwu-speak. """ text = self._uwutrans(text) text += random.choice([''] * 10 + [' uwu', ' UwU', ' owo', ' OwO']) irc.reply(text) uwu = wrap(uwu, ['text']) Filter = internationalizeDocstring(Filter) Class = Filter # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Filter/test.py0000644000175000017500000001725014535072470016364 0ustar00valval# -*- coding: utf8 -*- ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 __future__ import unicode_literals from supybot.test import * import re import codecs import supybot.utils as utils import supybot.callbacks as callbacks from supybot.utils.minisix import u class FilterTest(ChannelPluginTestCase): plugins = ('Filter', 'Utilities', 'Reply') def testNoErrors(self): self.assertNotError('leet foobar') self.assertNotError('supa1337 foobar') self.assertNotError('aol I\'m too legit to quit.') def testDisabledCommandsCannotFilter(self): self.assertNotError('outfilter rot13') self.assertResponse('echo foo', 'sbb') self.assertNotError('outfilter') try: self.assertNotError('disable rot13') self.assertError('outfilter rot13') self.assertNotError('enable rot13') self.assertNotError('outfilter rot13') finally: try: callbacks.Plugin._disabled.remove('rot13') except KeyError: pass def testHebrew(self): self.assertResponse('hebrew The quick brown fox ' 'jumps over the lazy dog.', 'Th qck brwn fx jmps vr th lzy dg.') def testJeffk(self): for i in range(100): self.assertNotError('jeffk the quick brown fox is ghetto') def testSquish(self): self.assertResponse('squish foo bar baz', 'foobarbaz') self.assertResponse('squish "foo bar baz"', 'foobarbaz') def testUndup(self): self.assertResponse('undup foo bar baz quux', 'fo bar baz qux') self.assertResponse('undup aaaaaaaaaa', 'a') def testMorse(self): self.assertResponse('unmorse [morse jemfinch]', 'JEMFINCH') def testReverse(self): for s in map(str, range(1000, 1010)): self.assertResponse('reverse %s' % s, s[::-1]) def testBinary(self): self.assertResponse('binary A', '01000001') def testUnbinary(self): self.assertResponse('unbinary 011011010110111101101111', 'moo') self.assertError('unbinary moo') self.assertResponse('unbinary 01101101 01101111 01101111', 'moo') def testRot13(self): for s in map(str, range(1000, 1010)): self.assertResponse('rot13 [rot13 %s]' % s, s) def testRot13HandlesNonAsciiStuff(self): self.assertNotError('rot13 é') def testHexlifyUnhexlify(self): for s in map(str, range(1000, 1010)): self.assertResponse('unhexlify [hexlify %s]' % s, s) self.assertNotError('unhexlify ff') def testScramble(self): s = 'the recalcitrant jamessan tests his scramble function' self.assertNotRegexp('scramble %s' % s, s) s = 'the recalc1trant jam3ssan tests his scramble fun>` to configure the output format for all 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Format/config.py0000644000175000017500000000473214535072470016656 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Format') 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('Format', True) Format = conf.registerPlugin('Format') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Format, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/Format/locales/0000755000175000017500000000000014535072535016455 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Format/locales/fi.po0000644000175000017500000001512614535072470017416 0ustar00valval# Format plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Format plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11:40+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:42 msgid "" "Provides some commands for formatting text, such as making text bold or\n" " capitalized." msgstr "" "Tarjoaa komentoja tekstin muotoiluun, vaikkapa tekstin korostamiseen tai\n" " kirjainkoon muuttamiseen." #: plugin.py:46 msgid "" "\n" "\n" " Returns bolded.\n" " " msgstr "" "\n" "\n" " Palautttaa korostettuna.\n" " " #: plugin.py:55 msgid "" "\n" "\n" " Returns in reverse-video.\n" " " msgstr "" "\n" "\n" " Palauttaa käänteis-videona..\n" " " #: plugin.py:64 msgid "" "\n" "\n" " Returns underlined.\n" " " msgstr "" "\n" "\n" " Palauttaa alleviivattuna.\n" " " #: plugin.py:73 msgid "" " [] \n" "\n" " Returns with foreground color and background " "color\n" " (if given)\n" " " msgstr "" " [] \n" "\n" " Palauttaa etuala värillä ja tausta värillä\n" " (jos annettu)\n" " " #: plugin.py:83 msgid "" " [ ...]\n" "\n" " Joins all the arguments together with .\n" " " msgstr "" " [ ...]\n" "\n" " Yhdistää kaikki parametrit yhteen .\n" " " #: plugin.py:92 msgid "" " \n" "\n" " Replaces with in\n" " . The first and second arguments must necessarily be the " "same\n" " length.\n" " " msgstr "" " \n" "\n" " Korvaa \n" " . Ensinmäisen ja viimeisen parametrin täytyy olla " "täsmälleen saman\n" " pituisia.\n" " " #: plugin.py:99 msgid "" " must be the same length as ." msgstr "" " täytyy olla saman pituisia, kuin ." #: plugin.py:106 msgid "" " \n" "\n" " Replaces all non-overlapping occurrences of \n" " with in .\n" " " msgstr "" " \n" "\n" " Korvaa kaikki ei-ylilyövät sattumat \n" " .\n" " " #: plugin.py:115 msgid "" "\n" "\n" " Returns uppercased.\n" " " msgstr "" "\n" "\n" " Palauttaa isoilla kirjaimilla.\n" " " #: plugin.py:124 msgid "" "\n" "\n" " Returns lowercased.\n" " " msgstr "" "\n" "\n" " Palauttaa pienillä kirjaimilla.\n" " " #: plugin.py:133 msgid "" "\n" "\n" " Returns capitalized.\n" " " msgstr "" "\n" "\n" " Palauttaa aktivoituna.\n" " " #: plugin.py:142 msgid "" "\n" "\n" " Returns titlecased.\n" " " msgstr "" "\n" "\n" " Palauttaa otsikoituna.\n" " " #: plugin.py:151 msgid "" "\n" "\n" " Returns surrounded by double quotes.\n" " " msgstr "" "\n" "\n" " Palauttaa tekstin kahden lainausmerkin sisällä.\n" " " #: plugin.py:160 msgid "" " \n" "\n" " Concatenates two strings. Do keep in mind that this is *not* the " "same\n" " thing as join \"\", since if contains spaces, they won't " "be\n" " removed by concat.\n" " " msgstr "" " \n" "\n" " Liittää merkkiketjut yhteen. Pidä mielessä että tämä *ei* ole sama\n" " asia kuin liitä \"\", koska jos sisältää " "välilyöntejä, ne eivät tule\n" " liitoksen poistamiksi.\n" " " #: plugin.py:171 msgid "" " \n" "\n" " Cuts down to by chopping off the rightmost characters " "in\n" " excess of . If is a negative number, it chops that " "many\n" " characters off the end of .\n" " " msgstr "" " \n" "\n" " Leikkaa katkaisemalla oikeimmat\n" " pääsyt. Jos on negatiivinen numero, se leikkaa niin " "monta\n" " merkkiä lopusta.\n" " " #: plugin.py:182 msgid "" " \n" "\n" " Returns the th space-separated field of . I.e., if " "text\n" " is \"foo bar baz\" and is 2, \"bar\" is returned.\n" " " msgstr "" " \n" "\n" " Palauttaa :nen välilyönnillä erotetut osan . " "Esim. jos teksti\n" " on \"foo bar baz\" ja on 2, palauttaa \"bar\" :in.\".\n" " " #: plugin.py:195 msgid "" " [ ...]\n" "\n" " Expands a Python-style format string using the remaining args. Just " "be\n" " sure always to use %s, not %d or %f or whatever, because all the " "args\n" " are strings.\n" " " msgstr "" " [ ...]\n" "\n" " Laajentaa Python-tyylistä merkkiketjua käyttämällä jäljellä olevia " "parametrejä. Ole varma, että käytät\n" " aina vain %s:ää, etkä %d:tä tai %f:ää tai mitä tahansa, koska kaikki " "parametrit ovat\n" " merkkiketjuja.\n" " " #: plugin.py:209 msgid "Not enough arguments for the format string." msgstr "Ei tarpeeksi parametrejä formaatti merkkiketjulle." #~ msgid "" #~ "\n" #~ "\n" #~ " Strips bold, underline, and colors from ." #~ msgstr "" #~ "\n" #~ "\n" #~ " Riisuu korostuksen, alleviivauksen ja värit." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Format/locales/fr.po0000644000175000017500000001355614535072470017434 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:42 msgid "" "Provides some commands for formatting text, such as making text bold or\n" " capitalized." msgstr "" #: plugin.py:46 msgid "" "\n" "\n" " Returns bolded.\n" " " msgstr "" "\n" "\n" "Retourne le texte, en gras" #: plugin.py:55 msgid "" "\n" "\n" " Returns in reverse-video.\n" " " msgstr "" "\n" "\n" "Retourne le texte, inversé" #: plugin.py:64 msgid "" "\n" "\n" " Returns underlined.\n" " " msgstr "" "\n" "\n" "Retourne le texte, souligné" #: plugin.py:73 msgid "" " [] \n" "\n" " Returns with foreground color and background " "color\n" " (if given)\n" " " msgstr "" " [] \n" "\n" "Retourne le avec les couleurs de et de l' (si donné)." #: plugin.py:83 msgid "" " [ ...]\n" "\n" " Joins all the arguments together with .\n" " " msgstr "" " [ ...]\n" "\n" "Joint tous les arguments en utilisant le ." #: plugin.py:92 msgid "" " \n" "\n" " Replaces with in\n" " . The first and second arguments must necessarily be the " "same\n" " length.\n" " " msgstr "" " \n" "\n" "Remplacer des caractères par d'autres. Le premier et le second argument " "doivent obligatoirement être de la même taille." #: plugin.py:99 msgid "" " must be the same length as ." msgstr "" " doit être de la même taille que " #: plugin.py:106 msgid "" " \n" "\n" " Replaces all non-overlapping occurrences of \n" " with in .\n" " " msgstr "" " \n" "\n" "Replace toutes les occurences de (à condition " "qu'elles n'entrent pas en conflit) par les " "dans le ." #: plugin.py:115 msgid "" "\n" "\n" " Returns uppercased.\n" " " msgstr "" "\n" "\n" "Retourne le texte, en majuscules" #: plugin.py:124 msgid "" "\n" "\n" " Returns lowercased.\n" " " msgstr "" "\n" "\n" "Retourne le texte, en minuscules" #: plugin.py:133 msgid "" "\n" "\n" " Returns capitalized.\n" " " msgstr "" "\n" "\n" "Retourne le texte, capitalisé" #: plugin.py:142 msgid "" "\n" "\n" " Returns titlecased.\n" " " msgstr "" "\n" "\n" "Retourne le texte, mis en majuscules de titre." #: plugin.py:151 msgid "" "\n" "\n" " Returns surrounded by double quotes.\n" " " msgstr "" "\n" "\n" "Retourne le texte, entouré de guillemets doubles." #: plugin.py:160 msgid "" " \n" "\n" " Concatenates two strings. Do keep in mind that this is *not* the " "same\n" " thing as join \"\", since if contains spaces, they won't " "be\n" " removed by concat.\n" " " msgstr "" " \n" "\n" "Concatène les deux chaînes. Notez que ce n'est pas la même chose que de les " "joindre avec \"\", car, si contient des espaces, ils ne seront " "pas supprimés par la concaténation." #: plugin.py:171 msgid "" " \n" "\n" " Cuts down to by chopping off the rightmost characters " "in\n" " excess of . If is a negative number, it chops that " "many\n" " characters off the end of .\n" " " msgstr "" " \n" "\n" "Coup le en morceaux de , en découpant les caractères " "dépassant la . Si la est un nombre négatif, il coupe en " "comptant à partir de la fin du texte." #: plugin.py:182 msgid "" " \n" "\n" " Returns the th space-separated field of . I.e., if " "text\n" " is \"foo bar baz\" and is 2, \"bar\" is returned.\n" " " msgstr "" " \n" "\n" "Retourne le -ième élément (séparé par des espaces) du . C'est " "à dire que si le texte est \"foo bar baz\" et que est 2, \"bar\" " "sera retourné." #: plugin.py:195 msgid "" " [ ...]\n" "\n" " Expands a Python-style format string using the remaining args. Just " "be\n" " sure always to use %s, not %d or %f or whatever, because all the " "args\n" " are strings.\n" " " msgstr "" " [ ...]\n" "\n" "Étant une chaine de formattage dans le style de Python avec les arguments " "restants.Assurez-vous seulement de toujours utiliser %s, et pas %d, %f, ou " "quoi que ce soit d’autre, puisque tous les arguments sont des chaines de " "caractères." #: plugin.py:209 msgid "Not enough arguments for the format string." msgstr "Pas assez d'arguments pour formatter la chaîne." #~ msgid "" #~ "\n" #~ "\n" #~ " Strips bold, underline, and colors from ." #~ msgstr "" #~ "\n" #~ "\n" #~ "Retire le le gras, le soulignement, et les couleurs du ." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Format/locales/it.po0000644000175000017500000001374314535072470017437 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-29 14:11+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:42 msgid "" "Provides some commands for formatting text, such as making text bold or\n" " capitalized." msgstr "" #: plugin.py:46 msgid "" "\n" "\n" " Returns bolded.\n" " " msgstr "" "\n" "\n" " Restituisce in grassetto.\n" " " #: plugin.py:55 msgid "" "\n" "\n" " Returns in reverse-video.\n" " " msgstr "" "\n" "\n" " Restituisce in negativo.\n" " " #: plugin.py:64 msgid "" "\n" "\n" " Returns underlined.\n" " " msgstr "" "\n" "\n" " Restituisce sottolineato.\n" " " #: plugin.py:73 msgid "" " [] \n" "\n" " Returns with foreground color and background " "color\n" " (if given)\n" " " msgstr "" " [] \n" "\n" " Restituisce con colore di e colore di " "(se specificato)\n" " " #: plugin.py:83 msgid "" " [ ...]\n" "\n" " Joins all the arguments together with .\n" " " msgstr "" " [ ...]\n" "\n" " Unisce tutti gli argomenti con .\n" " " #: plugin.py:92 msgid "" " \n" "\n" " Replaces with in\n" " . The first and second arguments must necessarily be the " "same\n" " length.\n" " " msgstr "" " \n" "\n" " Sostituisce con in .\n" " Il primo e il secondo argomento devono essere obbligatoriamente " "della stessa lunghezza.\n" " " #: plugin.py:99 msgid "" " must be the same length as ." msgstr "" " deve essere della stessa lunghezza di ." #: plugin.py:106 msgid "" " \n" "\n" " Replaces all non-overlapping occurrences of \n" " with in .\n" " " msgstr "" " \n" "\n" " Sostituisce tutte le occorrenze di (a " "condizione\n" " che non entrino in conflitto) con in " ".\n" " " #: plugin.py:115 msgid "" "\n" "\n" " Returns uppercased.\n" " " msgstr "" "\n" "\n" " Restituisce tutto maiuscolo.\n" " " #: plugin.py:124 msgid "" "\n" "\n" " Returns lowercased.\n" " " msgstr "" "\n" "\n" " Restituisce minuscolo.\n" " " #: plugin.py:133 msgid "" "\n" "\n" " Returns capitalized.\n" " " msgstr "" "\n" "\n" " Restituisce maiuscolo.\n" " " #: plugin.py:142 msgid "" "\n" "\n" " Returns titlecased.\n" " " msgstr "" "\n" "\n" " Restituisce con tutte le prime lettere delle parole in " "maiuscolo.\n" " " #: plugin.py:151 msgid "" "\n" "\n" " Returns surrounded by double quotes.\n" " " msgstr "" "\n" "\n" " Restituisce tra virgolette doppie.\n" " " #: plugin.py:160 msgid "" " \n" "\n" " Concatenates two strings. Do keep in mind that this is *not* the " "same\n" " thing as join \"\", since if contains spaces, they won't " "be\n" " removed by concat.\n" " " msgstr "" " \n" "\n" " Concatena due stringhe. Nota che non è come unire con \"\", dal " "momento\n" " che se contiene spazi questi non verranno rimossi da " "concat.\n" " " #: plugin.py:171 msgid "" " \n" "\n" " Cuts down to by chopping off the rightmost characters " "in\n" " excess of . If is a negative number, it chops that " "many\n" " characters off the end of .\n" " " msgstr "" " \n" "\n" " Taglia a rimuovendo i caratteri in eccesso più " "a\n" " destra di . Se è un numero negativo, " "rimuove \n" " i caratteri alla fine di .\n" " " #: plugin.py:182 msgid "" " \n" "\n" " Returns the th space-separated field of . I.e., if " "text\n" " is \"foo bar baz\" and is 2, \"bar\" is returned.\n" " " msgstr "" " \n" "\n" " Riporta i campi (separati da spazi) di . Ovvero se " "il\n" " testo è \"foo bar baz\" e è 2, verrà mostrato \"bar\".\n" " " #: plugin.py:195 msgid "" " [ ...]\n" "\n" " Expands a Python-style format string using the remaining args. Just " "be\n" " sure always to use %s, not %d or %f or whatever, because all the " "args\n" " are strings.\n" " " msgstr "" " [ ...]\n" "\n" " Espande un formato di stringa in stile Python utilizzando gli " "argomenti\n" " restanti. Assicurati di usare sempre %s, non %d o %f o altro, in " "quanto\n" " tutti gli argomenti sono stringhe.\n" " " #: plugin.py:209 msgid "Not enough arguments for the format string." msgstr "Argomenti non sufficienti per il formato della stringa." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Format/plugin.py0000644000175000017500000001643414535072470016711 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 string import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Format') class Format(callbacks.Plugin): """Provides some commands for formatting text, such as making text bold or capitalized.""" @internationalizeDocstring def bold(self, irc, msg, args, text): """ Returns bolded. """ irc.reply(ircutils.bold(text)) bold = wrap(bold, ['text']) @internationalizeDocstring def reverse(self, irc, msg, args, text): """ Returns in reverse-video. """ irc.reply(ircutils.reverse(text)) reverse = wrap(reverse, ['text']) @internationalizeDocstring def underline(self, irc, msg, args, text): """ Returns underlined. """ irc.reply(ircutils.underline(text)) underline = wrap(underline, ['text']) @internationalizeDocstring def color(self, irc, msg, args, fg, bg, text): """ [] Returns with foreground color and background color (if given) """ irc.reply(ircutils.mircColor(text, fg=fg, bg=bg)) color = wrap(color, ['color', optional('color'), 'text']) @internationalizeDocstring def join(self, irc, msg, args, sep): """ [ ...] Joins all the arguments together with . """ irc.reply(sep.join(args)) join = wrap(join, ['anything'], allowExtra=True) @internationalizeDocstring def translate(self, irc, msg, args, bad, good, text): """ Replaces with in . The first and second arguments must necessarily be the same length. """ if len(bad) != len(good): irc.error(_(' must be the same length as ' '.'), Raise=True) irc.reply(utils.str.MultipleReplacer(dict(list(zip(bad, good))))(text)) translate = wrap(translate, ['something', 'something', 'text']) @internationalizeDocstring def replace(self, irc, msg, args, bad, good, text): """ Replaces all non-overlapping occurrences of with in . """ irc.reply(text.replace(bad, good)) replace = wrap(replace, ['anything', 'anything', 'text']) def upper(self, irc, msg, args, text): """ Returns uppercased. """ irc.reply(text.upper()) upper = wrap(upper, ['text']) @internationalizeDocstring def lower(self, irc, msg, args, text): """ Returns lowercased. """ irc.reply(text.lower()) lower = wrap(lower, ['text']) @internationalizeDocstring def capitalize(self, irc, msg, args, text): """ Returns capitalized. """ irc.reply(text.capitalize()) capitalize = wrap(capitalize, ['text']) @internationalizeDocstring def title(self, irc, msg, args, text): """ Returns titlecased. """ irc.reply(string.capwords(text, " ")) title = wrap(title, ['text']) @internationalizeDocstring def repr(self, irc, msg, args, text): """ Returns surrounded by double quotes. """ irc.reply(utils.str.dqrepr(text)) repr = wrap(repr, ['text']) @internationalizeDocstring def concat(self, irc, msg, args, first, second): """ Concatenates two strings. Do keep in mind that this is *not* the same thing as join "", since if contains spaces, they won't be removed by concat. """ irc.reply(first+second) concat = wrap(concat, ['something', 'text']) @internationalizeDocstring def cut(self, irc, msg, args, size, text): """ Cuts down to by chopping off the rightmost characters in excess of . If is a negative number, it chops that many characters off the end of . """ irc.reply(text[:size]) cut = wrap(cut, ['int', 'text']) @internationalizeDocstring def field(self, irc, msg, args, index, text): """ Returns the th space-separated field of . I.e., if text is "foo bar baz" and is 2, "bar" is returned. """ try: irc.reply(text.split()[index]) except IndexError: irc.errorInvalid('field') field = wrap(field, ['index', 'text']) @internationalizeDocstring def format(self, irc, msg, args): """ [ ...] Expands a Python-style format string using the remaining args. Just be sure always to use %s, not %d or %f or whatever, because all the args are strings. """ if not args: raise callbacks.ArgumentError s = args.pop(0) try: s %= tuple(args) irc.reply(s) except TypeError as e: self.log.debug(utils.exnToString(e)) irc.error(_('Not enough arguments for the format string.'), Raise=True) Class = Format # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Format/test.py0000644000175000017500000000775614535072470016401 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class FormatTestCase(PluginTestCase): plugins = ('Format',) def testBold(self): self.assertResponse('bold foobar', '\x02foobar\x02') def testUnderline(self): self.assertResponse('underline foobar', '\x1ffoobar\x1f') def testReverse(self): self.assertResponse('reverse foobar', '\x16foobar\x16') def testFormat(self): self.assertResponse('format %s foo', 'foo') self.assertResponse('format %s%s foo bar', 'foobar') self.assertResponse('format "%sbaz%s" "foo bar" 1', 'foo barbaz1') self.assertError('format %s foo bar') self.assertError('format %s%s foo') def testJoin(self): self.assertResponse('join + foo bar baz', 'foo+bar+baz') self.assertResponse('join "" foo bar baz', 'foobarbaz') def testTranslate(self): self.assertResponse('translate 123 456 1234567890', '4564567890') self.assertError('translate 123 1234 123125151') def testReplace(self): self.assertResponse('replace # %23 bla#foo', 'bla%23foo') self.assertResponse('replace "/" "" t/e/s/t', 'test') self.assertResponse('replace "" :) hello', ':)h:)e:)l:)l:)o:)') self.assertResponse('replace de "d e" a b c de f ', 'a b c d e f') def testUpper(self): self.assertResponse('upper foo', 'FOO') self.assertResponse('upper FOO', 'FOO') def testLower(self): self.assertResponse('lower foo', 'foo') self.assertResponse('lower FOO', 'foo') def testCapitalize(self): self.assertResponse('capitalize foo', 'Foo') self.assertResponse('capitalize foo bar', 'Foo bar') def testTitle(self): self.assertResponse('title foo', 'Foo') self.assertResponse('title foo bar', 'Foo Bar') self.assertResponse('title foo\'s bar', 'Foo\'s Bar') def testRepr(self): self.assertResponse('repr foo bar baz', '"foo bar baz"') def testConcat(self): self.assertResponse('concat foo bar baz', 'foobar baz') def testCut(self): self.assertResponse('cut 5 abcdefgh', 'abcde') self.assertResponse('cut 5 abcd', 'abcd') self.assertResponse('cut -1 abcde', 'abcd') def testField(self): self.assertResponse('field 2 foo bar baz', 'bar') self.assertResponse('field -1 foo bar baz', 'baz') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3377547 limnoria-2023.11.18/plugins/GPG/0000755000175000017500000000000014535072535014220 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/GPG/__init__.py0000644000175000017500000000505614535072470016335 0ustar00valval### # Copyright (c) 2015-2021, 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. ### """ GPG: Provides authentication based on GPG keys. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.progval __maintainer__ = supybot.authors.limnoria_core # 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 importlib import reload from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/GPG/config.py0000644000175000017500000000566414535072470016050 0ustar00valval### # Copyright (c) 2015-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('GPG') 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('GPG', True) GPG = conf.registerPlugin('GPG') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(GPG, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerGroup(GPG, 'auth') conf.registerGroup(GPG.auth, 'sign') conf.registerGlobalValue(GPG.auth.sign, 'enable', registry.Boolean(True, """Determines whether or not users are allowed to use GPG signing for authentication.""")) conf.registerGlobalValue(GPG.auth.sign, 'TokenTimeout', registry.PositiveInteger(60*10, """Determines the lifetime of a GPG signature authentication token (in seconds).""")) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/GPG/plugin.py0000644000175000017500000002253614535072470016076 0ustar00valval### # Copyright (c) 2015-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import sys import time import uuid import functools import supybot.gpg as gpg import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb 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 if minisix.PY3: import http.client as http_client else: import httplib as http_client try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('GPG') except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x def check_gpg_available(f): if gpg.available: return f else: if not gpg.found_gnupg_lib: def newf(self, irc, *args): irc.error(_('gnupg features are not available because ' 'the python-gnupg library is not installed.')) elif not gpg.found_gnupg_bin: def newf(self, irc, *args): irc.error(_('gnupg features are not available because ' 'the gnupg executable is not installed.')) else: # This case should never happen. def newf(self, irc, *args): irc.error(_('gnupg features are not available.')) newf.__doc__ = f.__doc__ newf.__name__ = f.__name__ return newf if hasattr(http_client, '_MAXHEADERS'): safe_getUrl = utils.web.getUrl else: def safe_getUrl(url): try: return commands.process(utils.web.getUrl, url, timeout=10, heap_size=10*1024*1024, pn='GPG') except (commands.ProcessTimeoutError, MemoryError): raise utils.web.Error(_('Page is too big or the server took ' 'too much time to answer the request.')) class GPG(callbacks.Plugin): """Provides authentication based on GPG keys.""" class key(callbacks.Commands): @check_gpg_available def add(self, irc, msg, args, user, keyid, keyserver): """ Add a GPG key to your account.""" if keyid in user.gpgkeys: irc.error(_('This key is already associated with your ' 'account.')) return result = gpg.keyring.recv_keys(keyserver, keyid) reply = format(_('%n imported, %i unchanged, %i not imported.'), (result.imported, _('key')), result.unchanged, result.not_imported, [x['fingerprint'] for x in result.results]) if result.imported == 1: user.gpgkeys.append(keyid) irc.reply(reply) else: irc.error(reply) add = wrap(add, ['user', ('somethingWithoutSpaces', _('You must give a valid key id')), ('somethingWithoutSpaces', _('You must give a valid key server'))]) @check_gpg_available def remove(self, irc, msg, args, user, fingerprint): """ Remove a GPG key from your account.""" try: keyids = [x['keyid'] for x in gpg.keyring.list_keys() if x['fingerprint'] == fingerprint] if len(keyids) == 0: raise ValueError for keyid in keyids: try: user.gpgkeys.remove(keyid) except ValueError: user.gpgkeys.remove('0x' + keyid) gpg.keyring.delete_keys(fingerprint) irc.replySuccess() except ValueError: irc.error(_('GPG key not associated with your account.')) remove = wrap(remove, ['user', 'somethingWithoutSpaces']) @check_gpg_available def list(self, irc, msg, args, user): """takes no arguments List your GPG keys.""" keyids = user.gpgkeys if len(keyids) == 0: irc.reply(_('No key is associated with your account.')) else: irc.reply(format('%L', keyids)) list = wrap(list, ['user']) class signing(callbacks.Commands): def __init__(self, *args): super(GPG.signing, self).__init__(*args) self._tokens = {} def _expire_tokens(self): now = time.time() self._tokens = dict(filter(lambda x_y: x_y[1][1]>now, self._tokens.items())) @check_gpg_available def gettoken(self, irc, msg, args): """takes no arguments Send you a token that you'll have to sign with your key.""" self._expire_tokens() token = '{%s}' % str(uuid.uuid4()) lifetime = conf.supybot.plugins.GPG.auth.sign.TokenTimeout() self._tokens.update({token: (msg.prefix, time.time()+lifetime)}) irc.reply(_('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.') % token) gettoken = wrap(gettoken, []) _auth_re = re.compile(r'-----BEGIN PGP SIGNED MESSAGE-----\r?\n' r'Hash: .*\r?\n\r?\n' r'\s*({[0-9a-z-]+})\s*\r?\n' r'-----BEGIN PGP SIGNATURE-----\r?\n.*' r'\r?\n-----END PGP SIGNATURE-----', re.S) @check_gpg_available def auth(self, irc, msg, args, url): """ Check the GPG signature at the and authenticates you if the key used is associated to a user.""" self._expire_tokens() content = safe_getUrl(url) if minisix.PY3 and isinstance(content, bytes): content = content.decode() match = self._auth_re.search(content) if not match: irc.error(_('Signature or token not found.'), Raise=True) data = match.group(0) token = match.group(1) if token not in self._tokens: irc.error(_('Unknown token. It may have expired before you ' 'submit it.'), Raise=True) if self._tokens[token][0] != msg.prefix: irc.error(_('Your hostname/nick changed in the process. ' 'Authentication aborted.'), Raise=True) verified = gpg.keyring.verify(data) if verified and verified.valid: keyid = verified.pubkey_fingerprint[-16:] prefix, expiry = self._tokens.pop(token) found = False for (id, user) in ircdb.users.items(): if keyid in [x[-len(keyid):] for x in user.gpgkeys]: try: user.addAuth(msg.prefix) except ValueError: irc.error(_('Your secure flag is true and your ' 'hostmask doesn\'t match any of your ' 'known hostmasks.'), Raise=True) ircdb.users.setUser(user, flush=False) irc.reply(_('You are now authenticated as %s.') % user.name) return irc.error(_('Unknown GPG key.'), Raise=True) else: irc.error(_('Signature could not be verified. Make sure ' 'this is a valid GPG signature and the URL is valid.')) auth = wrap(auth, ['url']) Class = GPG # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/GPG/test.py0000644000175000017500000001327114535072470015553 0ustar00valval### # Copyright (c) 2015-2021, 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 * import supybot.utils.minisix as minisix import supybot.gpg as gpg PRIVATE_KEY = """ -----BEGIN PGP PRIVATE KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) lQHYBFD7GxQBBACeu7bj/wgnnv5NkfHImZJVJLaq2cwKYc3rErv7pqLXpxXZbDOI jP+5eSmTLhPUK67aRD6gG0wQ9iAhYR03weOmyjDGh0eF7kLYhu/4Il56Y/YbB8ll Imz/pep/Hi72ShcW8AtifDup/KeHjaWa1yF2WThHbX/0N2ghSxbJnatpBwARAQAB AAP6Arf7le7FD3ZhGZvIBkPr25qca6i0Qxb5XpOinV7jLcoycZriJ9Xofmhda9UO xhNVppMvs/ofI/m0umnR4GLKtRKnJSc8Edxi4YKyqLehfBTF20R/kBYPZ772FkNW Kzo5yCpP1jpOc0+QqBuU7OmrG4QhQzTLXIUgw4XheORncEECAMGkvR47PslJqzbY VRIzWEv297r1Jxqy6qgcuCJn3RWYJbEZ/qdTYy+MgHGmaNFQ7yhfIzkBueq0RWZp Z4PfJn8CANHZGj6AJZcvb+VclNtc5VNfnKjYD+qQOh2IS8NhE/0umGMKz3frH1TH yCbh2LlPR89cqNcd4QvbHKA/UmzISXkB/37MbUnxXTpS9Y4HNpQCh/6SYlB0lucV QN0cgjfhd6nBrb6uO6+u40nBzgynWcEpPMNfN0AtQeA4Dx+WrnK6kZqfd7QMU3Vw eWJvdCB0ZXN0iLgEEwECACIFAlD7GxQCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B AheAAAoJEMnTMjwgrwErV3AD/0kRq8UWPlkc6nyiIR6qiT3EoBNHKIi4cz68Wa1u F2M6einrRR0HolrxonynTGsdr1u2f3egOS4fNfGhTNAowSefYR9q5kIYiYE2DL5G YnjJKNfmnRxZM9YqmEnN50rgu2cifSRehp61fXdTtmOAR3js+9wb73dwbYzr3kIc 3WH1 =UBcd -----END PGP PRIVATE KEY BLOCK----- """ WRONG_TOKEN_SIGNATURE = """ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1 {a95dc112-780e-47f7-a83a-c6f3820d7dc3} -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.12 (GNU/Linux) iJwEAQECAAYFAlD7Jb0ACgkQydMyPCCvASv9HgQAhQf/oFMWcKwGncH0hjXC3QYz 7ck3chgL3S1pPAvS69viz6i2bwYZYD8fhzHNJ/qtw/rx6thO6PwT4SpdhKerap+I kdem3LjM4fAGHRunHZYP39obNKMn1xv+f26mEAAWxdv/W/BLAFqxi3RijJywRkXm zo5GUl844kpnV+uk0Xk= =z2Cz -----END PGP SIGNATURE----- """ FINGERPRINT = '2CF3E41500218D30F0B654F5C9D3323C20AF012B' class GPGTestCase(PluginTestCase): plugins = ('GPG', 'User') def setUp(self): super(GPGTestCase, self).setUp() gpg.loadKeyring() if gpg.available and network: def testGpgAddRemove(self): self.assertNotError('register foo bar') self.assertError('gpg key add 51E516F0B0C5CE6A pgp.mit.edu') self.assertResponse('gpg key add EB17F1E0CEB63930 pgp.mit.edu', '1 key imported, 0 unchanged, 0 not imported.') self.assertNotError( 'gpg key remove F88ECDE235846FA8652DAF5FEB17F1E0CEB63930') self.assertResponse('gpg key add EB17F1E0CEB63930 pgp.mit.edu', '1 key imported, 0 unchanged, 0 not imported.') self.assertResponse('gpg key add EB17F1E0CEB63930 pgp.mit.edu', 'Error: This key is already associated with your account.') if gpg.available: def testGpgAuth(self): self.assertNotError('register spam egg') gpg.keyring.import_keys(PRIVATE_KEY).__dict__ (id, user) = list(ircdb.users.items())[0] user.gpgkeys.append(FINGERPRINT) msg = self.getMsg('gpg signing gettoken').args[-1] match = re.search('is: ({.*}).', msg) assert match, repr(msg) token = match.group(1) def fakeGetUrlFd(*args, **kwargs): fd.geturl = lambda :None return fd (utils.web.getUrlFd, realGetUrlFd) = (fakeGetUrlFd, utils.web.getUrlFd) fd = minisix.io.StringIO() fd.write('foo') fd.seek(0) self.assertResponse('gpg signing auth http://foo.bar/baz.gpg', 'Error: Signature or token not found.') fd = minisix.io.StringIO() fd.write(token) fd.seek(0) self.assertResponse('gpg signing auth http://foo.bar/baz.gpg', 'Error: Signature or token not found.') fd = minisix.io.StringIO() fd.write(WRONG_TOKEN_SIGNATURE) fd.seek(0) self.assertRegexp('gpg signing auth http://foo.bar/baz.gpg', 'Error: Unknown token.*') fd = minisix.io.StringIO() fd.write(str(gpg.keyring.sign(token))) fd.seek(0) self.assertResponse('gpg signing auth http://foo.bar/baz.gpg', 'You are now authenticated as spam.') utils.web.getUrlFd = realGetUrlFd # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Games/0000755000175000017500000000000014535072535014637 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/__init__.py0000644000175000017500000000514414535072470016752 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This plugin provides some fun games like (Russian) roulette, 8ball, monologue which tells you how many lines you have spoken without anyone interrupting you, coin and dice. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/config.py0000644000175000017500000000441114535072470016454 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Games') 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('Games', True) Games = conf.registerPlugin('Games') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Games/locales/0000755000175000017500000000000014535072535016261 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/locales/de.po0000644000175000017500000001130114535072470017203 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-11-10 23:19+0100\n" "Last-Translator: Florian Besser \n" "Language-Team: DE \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "X-Poedit-Language: German\n" #: plugin.py:46 msgid "" "This plugin provides some small games like (Russian) roulette,\n" " eightball, monologue, coin and dice." msgstr "" #: plugin.py:50 msgid "" "takes no arguments\n" "\n" " Flips a coin and returns the result.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Wirft eine Münze und gibt das Ergebnis aus." #: plugin.py:55 msgid "heads" msgstr "Kopf" #: plugin.py:57 msgid "tails" msgstr "Zahl" #: plugin.py:62 msgid "" "d\n" "\n" " Rolls a die with number of sides times.\n" " For example, 2d6 will roll 2 six-sided dice; 10d10 will roll 10\n" " ten-sided dice.\n" " " msgstr "" "d\n" "\n" "Wirft einen Würfel mit Anzahl der Seiten, mal. z.B. 2d6 " "wirft 2 Würfel mit 6 Seiten; 10d10 wirft 10 mal einen 10 seitigen Würfel." #: plugin.py:70 msgid "You can't roll more than 1000 dice." msgstr "Du kannst nicht mehr wie 1000 Würfel werfen." #: plugin.py:72 msgid "Dice can't have more than 100 sides." msgstr "Würfel kann nicht mehr als 100 Seiten haben." #: plugin.py:74 msgid "Dice can't have fewer than 3 sides." msgstr "Würfel können nicht weniger wie 3 Seiten haben." #: plugin.py:82 msgid "Dice must be of the form d" msgstr "Würfel muss mit d angegeben werden" #: plugin.py:86 msgid "" "It is possible.|Yes!|Of course.|Naturally.|Obviously.|It shall be.|The " "outlook is good.|It is so.|One would be wise to think so.|The answer is " "certainly yes." msgstr "" "Es ist möglich.| Ja!|Natürlich.|Natürlicher.|Ist doch klar.|So soll es sein.|" "Es sieht gut aus.|So ist es.| Es wäre gut so zu denken.|Die Antwort ist " "sicherlich ja." #: plugin.py:90 msgid "" "In your dreams.|I doubt it very much.|No chance.|The outlook is poor.|" "Unlikely.|About as likely as pigs flying.|You're kidding, right?|NO!|NO.|No.|" "The answer is a resounding no." msgstr "" "In deinen Träumen.|Ich zweifel das stark an.| Keine Chance.|Es sieht " "schlecht aus.|Unwahrscheinlich.|So wahrscheinlich wie fliegende Schweine.| " "Du machst Witze, oder?|NEIN!|NEIN.|Nein.| Die Antwort ist ein klares Nein." #: plugin.py:94 msgid "" "Maybe...|No clue.|_I_ don't know.|The outlook is hazy, please ask again " "later.|What are you asking me for?|Come again?|You know the answer better " "than I.|The answer is def-- oooh! shiny thing!" msgstr "" "Vielleicht...| Keine Ahnung.| Ich weiß nicht.|Die Aussicht ist unklar, frag " "später nochmal.|Nach was fragst du mich?|Kommst du wieder?|Du weißt die " "Antwort besser als ich.|Die Antwort ist defin---- OHH! Da glänzt was!" #: plugin.py:111 msgid "" "[]\n" "\n" " Ask a question and the answer shall be provided.\n" " " msgstr "" "[]\n" "\n" "Frage deine Frage und dir wird eine Antwort gegeben." #: plugin.py:125 msgid "" "[spin]\n" "\n" " Fires the revolver. If the bullet was in the chamber, you're dead.\n" " Tell me to spin the chambers and I will.\n" " " msgstr "" "[spin]\n" "\n" "Feuert den Revolver. Falls eine Patrone in der Kammer war, bist du tod. Sag " "mir das ich die Kammern drehen soll, dann werde ich das tun." #: plugin.py:132 msgid "*SPIN* Are you feeling lucky?" msgstr "*DREHE* Denkst du du hast Glück?" #: plugin.py:142 msgid "*BANG* Hey, who put a blank in here?!" msgstr "*BANG* Hey, wer hat hier eine leere Patrone reingetan?!" #: plugin.py:144 msgid "reloads and spins the chambers." msgstr "läd nach und dreht die Kammer." #: plugin.py:146 msgid "*click*" msgstr "*klick*" #: plugin.py:153 msgid "" "[]\n" "\n" " Returns the number of consecutive lines you've sent in \n" " without being interrupted by someone else (i.e. how long your " "current\n" " 'monologue' is). is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Gibt die Anzahl der aufeinderfolgenen Zeilen an, die du im gesendet " "hast, ohne von jemand anderem unterbrochen worden zu sein (z.B. wie lang " "dein momentaner Monolog ist). ist nur nötig falls die Nachricht " "nicht im Kanal selbst gesendet wurde." #: plugin.py:172 msgid "Your current monologue is at least %n long." msgstr "Dein momentaner Monolog ist mindestens %n lang." #: plugin.py:173 msgid "line" msgstr "Zeile" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/locales/fi.po0000644000175000017500000001223214535072470017215 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Games plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11:36+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" #: plugin.py:46 msgid "" "This plugin provides some small games like (Russian) roulette,\n" " eightball, monologue, coin and dice." msgstr "" "Tämä plugini sisältää joitakin pieniä pelejä, kuten (venäläisen) ruletin,\n" " kasipalllon, monologin, kolikon heittäminen ja nopan heittäminen." #: plugin.py:50 msgid "" "takes no arguments\n" "\n" " Flips a coin and returns the result.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Heittää rahan ja palauttaa tuloksen.\n" " " #: plugin.py:55 msgid "heads" msgstr "kruuna" #: plugin.py:57 msgid "tails" msgstr "klaava" #: plugin.py:62 msgid "" "d\n" "\n" " Rolls a die with number of sides times.\n" " For example, 2d6 will roll 2 six-sided dice; 10d10 will roll 10\n" " ten-sided dice.\n" " " msgstr "" "d\n" "\n" " Heittää noppaa sivujen lukumäärä kertaa.\n" " Esimerkiksi, 2d6 heittää 2 kuusisivuista noppaa; 10d10 heittää 10\n" " kymmenen-sivuista noppaa.\n" " " #: plugin.py:70 msgid "You can't roll more than 1000 dice." msgstr "Et voi heittää useampaa kuin tuhatta noppaa." #: plugin.py:72 msgid "Dice can't have more than 100 sides." msgstr "Nopalla ei voi olla useampaa kuin sataa sivua." #: plugin.py:74 msgid "Dice can't have fewer than 3 sides." msgstr "Nopalla ei voi olla vähempää kuin kolmea sivua." #: plugin.py:82 msgid "Dice must be of the form d" msgstr "Nopan täytyy olla muodossa d." #: plugin.py:86 msgid "" "It is possible.|Yes!|Of course.|Naturally.|Obviously.|It shall be.|The " "outlook is good.|It is so.|One would be wise to think so.|The answer is " "certainly yes." msgstr "" "Se on mahdollista.|Kyllä!|Tietysti.|Luonnollisesti.|Ilmeisesti.|Olkoon niin.|" "Hyvät näkymät.|Se on niin.|Erään olisi viisasta ajatella niin.|Vastaus on " "varmasti kyllä." #: plugin.py:90 msgid "" "In your dreams.|I doubt it very much.|No chance.|The outlook is poor.|" "Unlikely.|About as likely as pigs flying.|You're kidding, right?|NO!|NO.|No.|" "The answer is a resounding no." msgstr "" "Unissasi.|Minä epäilen sitä kovasti.|Ei mahdollista.|Näkymät ovat huonot.|" "Epätodennäköisesti.|Yhtä todennäköisesti, kuin siat lentävät.|Kai sinä " "pilailet?|EI!|EI.|Ei.|Vastaus on raikuva ei." #: plugin.py:94 msgid "" "Maybe...|No clue.|_I_ don't know.|The outlook is hazy, please ask again " "later.|What are you asking me for?|Come again?|You know the answer better " "than I.|The answer is def-- oooh! shiny thing!" msgstr "" "Ehkäpä...|Ei tietoa.|_Minä_ en tiedä.|Näkymä on sotkuinen, ole hyvä ja kysy " "myöhemmin uudelleen.|Miksi kysyt minulta?|Tule uudelleen?|Sinä tiedät " "vastauksen paremmin kuin minä.|Vastaus on var-- oooh! kiiltävä esine!" #: plugin.py:111 msgid "" "[]\n" "\n" " Ask a question and the answer shall be provided.\n" " " msgstr "" "[]\n" "\n" " Kysy kysymys ja vastaus annetaan.\n" " " #: plugin.py:125 msgid "" "[spin]\n" "\n" " Fires the revolver. If the bullet was in the chamber, you're dead.\n" " Tell me to spin the chambers and I will.\n" " " msgstr "" "[spin]\n" "\n" " Ampuu revolverillä. Jos luoti oli kammiossa, olet kuollut.\n" " Käske minun pyöräyttää kammiota ja minä teen sen.\n" " " #: plugin.py:132 msgid "*SPIN* Are you feeling lucky?" msgstr "*Pyörähdys* Tuntuuko sinusta onnekkaalta?" #: plugin.py:142 msgid "*BANG* Hey, who put a blank in here?!" msgstr "*PANG* Hei, kuka laittoi tuon laudan tuohon?!" #: plugin.py:144 msgid "reloads and spins the chambers." msgstr "lataa ja pyöräyttää kammioita." #: plugin.py:146 msgid "*click*" msgstr "*klick*" #: plugin.py:153 msgid "" "[]\n" "\n" " Returns the number of consecutive lines you've sent in \n" " without being interrupted by someone else (i.e. how long your " "current\n" " 'monologue' is). is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa jatkuvan määrän rivejä, jotka olet lähettänyt \n" " tulematta kenenkään muun keskeyttämäksi (esim. kuinka pitkä " "nykyinen\n" " 'monologisi' on). on vaadittu vain jos viestiä ei lähetetä " "kanavalla\n" " itsellään.\n" " " #: plugin.py:172 msgid "Your current monologue is at least %n long." msgstr "Sinun nykyinen monologisi on ainakin %n pitkä." #: plugin.py:173 msgid "line" msgstr "rivi" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/locales/fr.po0000644000175000017500000001155014535072470017230 0ustar00valval# French translations for PACKAGE package # Traductions françaises du paquet PACKAGE. # Copyright (C) 2010 ORGANIZATION # ProgVal , 2010. # msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2010-12-23 19:55+0100\n" "Last-Translator: Valentin Lorentz \n" "Language-Team: French\n" "Language: fr\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" #: plugin.py:46 msgid "" "This plugin provides some small games like (Russian) roulette,\n" " eightball, monologue, coin and dice." msgstr "" #: plugin.py:50 msgid "" "takes no arguments\n" "\n" " Flips a coin and returns the result.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Lance une pièce et retourne le résultat." #: plugin.py:55 msgid "heads" msgstr "face" #: plugin.py:57 msgid "tails" msgstr "pile" #: plugin.py:62 msgid "" "d\n" "\n" " Rolls a die with number of sides times.\n" " For example, 2d6 will roll 2 six-sided dice; 10d10 will roll 10\n" " ten-sided dice.\n" " " msgstr "" "d\n" "\n" "Fait un certain nombre de d'un dé d'un certain nombre de . " "Par exemple, 2d6 lancera deux dés à six faces, et 10d10 dix dés à dix faces." #: plugin.py:70 msgid "You can't roll more than 1000 dice." msgstr "Vous ne pouvez lancer plus de 1000 dés." #: plugin.py:72 msgid "Dice can't have more than 100 sides." msgstr "Vous ne pouvez pas avoir plus de 100 faces." #: plugin.py:74 msgid "Dice can't have fewer than 3 sides." msgstr "Vous ne pouvez pas avoir moins de trois faces." #: plugin.py:82 msgid "Dice must be of the form d" msgstr "Les dés doivent être de la forme d" #: plugin.py:86 msgid "" "It is possible.|Yes!|Of course.|Naturally.|Obviously.|It shall be.|The " "outlook is good.|It is so.|One would be wise to think so.|The answer is " "certainly yes." msgstr "" "C'est possible.|Oui !|Bien sûr.|Naturellement.|Évidemment.|Ce doit être ça.|" "Ce n'est pas impossible.|C'est le cas.|C'est ce que l'on peut penser.|La " "réponse est certainement oui." #: plugin.py:90 msgid "" "In your dreams.|I doubt it very much.|No chance.|The outlook is poor.|" "Unlikely.|About as likely as pigs flying.|You're kidding, right?|NO!|NO.|No.|" "The answer is a resounding no." msgstr "" "Dans tes rêves.|J'en doute.|Impossible.|Ça m'étonnerait|Improbable.|Quand " "les poules auront des dents|La semaine des quatre jeudis.|Tu rigoles ?|NON !|" "NON.|Non.|La réponse semble être non." #: plugin.py:94 msgid "" "Maybe...|No clue.|_I_ don't know.|The outlook is hazy, please ask again " "later.|What are you asking me for?|Come again?|You know the answer better " "than I.|The answer is def-- oooh! shiny thing!" msgstr "" "Peut-être...|Aucune idée|*Je* ne sais pas|Je n'en sais rien, veuillez " "réessayer.|Qu'est-ce que vous me demander ?|Revenez plus tard.|Vous " "connaissez la réponse mieux que moi.|La réponse est... oooh ! un truc qui " "brille !" #: plugin.py:111 msgid "" "[]\n" "\n" " Ask a question and the answer shall be provided.\n" " " msgstr "" "[]\n" "\n" "Posez une question, et la réponse devrait vous être donnée." #: plugin.py:125 msgid "" "[spin]\n" "\n" " Fires the revolver. If the bullet was in the chamber, you're dead.\n" " Tell me to spin the chambers and I will.\n" " " msgstr "" "[spin]\n" "\n" "Tire avec le revolver. Si la balle était dans la chambre, vous être mort. " "Dites-moi de recharger les chambres (en indiquant 'spin') et je le ferais." #: plugin.py:132 msgid "*SPIN* Are you feeling lucky?" msgstr "*FAIT TOURNER LE BARILLET* Prêt à mettre votre chance à l'épreuve ?" #: plugin.py:142 msgid "*BANG* Hey, who put a blank in here?!" msgstr "*BANG* Eh, qui a fait un trou ici ?" #: plugin.py:144 msgid "reloads and spins the chambers." msgstr "recharge et fait tourner les chambres" #: plugin.py:146 msgid "*click*" msgstr "*clic*" #: plugin.py:153 msgid "" "[]\n" "\n" " Returns the number of consecutive lines you've sent in \n" " without being interrupted by someone else (i.e. how long your " "current\n" " 'monologue' is). is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne le nombre de lignes consécutives que vous avez écrites sur le " " sans être interrompu par qui que ce soit. n'est nécessaire " "que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:172 msgid "Your current monologue is at least %n long." msgstr "Votre monologue actuel est au moins long de %n." #: plugin.py:173 msgid "line" msgstr "ligne" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/locales/it.po0000644000175000017500000001117414535072470017237 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-11-20 21:27+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" #: plugin.py:46 msgid "" "This plugin provides some small games like (Russian) roulette,\n" " eightball, monologue, coin and dice." msgstr "" #: plugin.py:50 msgid "" "takes no arguments\n" "\n" " Flips a coin and returns the result.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Lancia una moneta e restituisce il risultato.\n" " " #: plugin.py:55 msgid "heads" msgstr "testa" #: plugin.py:57 msgid "tails" msgstr "croce" #: plugin.py:62 msgid "" "d\n" "\n" " Rolls a die with number of sides times.\n" " For example, 2d6 will roll 2 six-sided dice; 10d10 will roll 10\n" " ten-sided dice.\n" " " msgstr "" "d\n" "\n" " Tira un certo numero di dadi con un certo numero di facce.\n" " Ad esempio, 2d6 tirerà due dadi da sei facce; 10d10 dieci dadi da " "dieci facce.\n" " " #: plugin.py:70 msgid "You can't roll more than 1000 dice." msgstr "Non è possibile tirare più di 1000 dadi." #: plugin.py:72 msgid "Dice can't have more than 100 sides." msgstr "I dadi non possono avere più di 100 facce." #: plugin.py:74 msgid "Dice can't have fewer than 3 sides." msgstr "I dadi non possono avere meno di tre facce." #: plugin.py:82 msgid "Dice must be of the form d" msgstr "I dadi vanno espressi nella forma d" #: plugin.py:86 msgid "" "It is possible.|Yes!|Of course.|Naturally.|Obviously.|It shall be.|The " "outlook is good.|It is so.|One would be wise to think so.|The answer is " "certainly yes." msgstr "" "È possibile.|Sì!|Certamente.|Naturalmente.|Ovviamente.|Deve essere così.|La " "prospettiva è promettente.|È così.|Sarebbe saggio pensarlo.|La risposta è " "certamente sì." #: plugin.py:90 msgid "" "In your dreams.|I doubt it very much.|No chance.|The outlook is poor.|" "Unlikely.|About as likely as pigs flying.|You're kidding, right?|NO!|NO.|No.|" "The answer is a resounding no." msgstr "" "Nei tuoi sogni.|Ne dubito fortemente.|Nessuna possibilità.|La prospettiva è " "scarsa.|Improbabile.|Quando gli asini voleranno.|Stai scherzando, vero?|NO!|" "NO.|No.|La risposta è un clamoroso no." #: plugin.py:94 msgid "" "Maybe...|No clue.|_I_ don't know.|The outlook is hazy, please ask again " "later.|What are you asking me for?|Come again?|You know the answer better " "than I.|The answer is def-- oooh! shiny thing!" msgstr "" "Forse...|Non ne ho idea.|Non lo so.|La prospettiva è confusa, riprova più " "tardi.|Cosa mi stai chiedendo?|Torna più tardi.|Conosci la risposta meglio " "di me.|La risposta è... oooh! brillante!" #: plugin.py:111 msgid "" "[]\n" "\n" " Ask a question and the answer shall be provided.\n" " " msgstr "" "[]\n" "\n" " Poni una domanda e otterrai una risposta.\n" " " #: plugin.py:125 msgid "" "[spin]\n" "\n" " Fires the revolver. If the bullet was in the chamber, you're dead.\n" " Tell me to spin the chambers and I will.\n" " " msgstr "" "[spin]\n" "\n" " Spara con il revolver. Se il proiettile era in camera, sei morto.\n" " Dimmi di girare il tamburo (aggiungendo \"spin\") e lo farò.\n" " " #: plugin.py:132 msgid "*SPIN* Are you feeling lucky?" msgstr "*GIRA* Pronto a mettere alla prova la fortuna?" #: plugin.py:142 msgid "*BANG* Hey, who put a blank in here?!" msgstr "*BANG* Hei, chi ha messo una cartuccia a salve qui?!" #: plugin.py:144 msgid "reloads and spins the chambers." msgstr "ricarica e gira il tamburo." #: plugin.py:146 msgid "*click*" msgstr "*clic*" #: plugin.py:153 msgid "" "[]\n" "\n" " Returns the number of consecutive lines you've sent in \n" " without being interrupted by someone else (i.e. how long your " "current\n" " 'monologue' is). is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Restituisce il numero di righe consecutive inviate in senza " "essere\n" " stati interrotti da qualcun altro. è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso.\n" " " #: plugin.py:172 msgid "Your current monologue is at least %n long." msgstr "Il tuo attuale monologo è lungo almeno %n." #: plugin.py:173 msgid "line" msgstr "riga" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/plugin.py0000644000175000017500000001612014535072470016505 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import random import supybot.utils as utils 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('Games') class Games(callbacks.Plugin): """This plugin provides some small games like (Russian) roulette, eightball, monologue, coin and dice.""" @internationalizeDocstring def coin(self, irc, msg, args): """takes no arguments Flips a coin and returns the result. """ if random.randrange(0, 2): irc.reply(_('heads')) else: irc.reply(_('tails')) coin = wrap(coin) @internationalizeDocstring def dice(self, irc, msg, args, m): """d Rolls a die with number of sides times. For example, 2d6 will roll 2 six-sided dice; 10d10 will roll 10 ten-sided dice. """ (dice, sides) = list(map(int, m.groups())) if dice > 1000: irc.error(_('You can\'t roll more than 1000 dice.')) elif sides > 100: irc.error(_('Dice can\'t have more than 100 sides.')) elif sides < 3: irc.error(_('Dice can\'t have fewer than 3 sides.')) else: L = [0] * dice for i in range(dice): L[i] = random.randrange(1, sides+1) irc.reply(format('%L', [str(x) for x in L])) _dicere = re.compile(r'^(\d+)d(\d+)$') dice = wrap(dice, [('matches', _dicere, _('Dice must be of the form d'))]) # The list of words and algorithm are pulled straight the mozbot # MagicEightBall.bm module: http://tinyurl.com/7ytg7 _positive = _('It is possible.|Yes!|Of course.|Naturally.|Obviously.|' 'It shall be.|The outlook is good.|It is so.|' 'One would be wise to think so.|' 'The answer is certainly yes.') _negative = _('In your dreams.|I doubt it very much.|No chance.|' 'The outlook is poor.|Unlikely.|' 'About as likely as pigs flying.|You\'re kidding, right?|' 'NO!|NO.|No.|The answer is a resounding no.') _unknown = _('Maybe...|No clue.|_I_ don\'t know.|' 'The outlook is hazy, please ask again later.|' 'What are you asking me for?|Come again?|' 'You know the answer better than I.|' 'The answer is def-- oooh! shiny thing!') def _checkTheBall(self, questionLength): if questionLength % 3 == 0: catalog = self._positive elif questionLength % 3 == 1: catalog = self._negative else: catalog = self._unknown return utils.iter.choice(catalog.split('|')) @internationalizeDocstring def eightball(self, irc, msg, args, text): """[] Ask a question and the answer shall be provided. """ if text: irc.reply(self._checkTheBall(len(text))) else: irc.reply(self._checkTheBall(random.randint(0, 2))) eightball = wrap(eightball, [additional('text')]) _rouletteChamber = random.randrange(0, 6) _rouletteBullet = random.randrange(0, 6) @internationalizeDocstring def roulette(self, irc, msg, args, spin): """[spin] Fires the revolver. If the bullet was in the chamber, you're dead. Tell me to spin the chambers and I will. """ if spin: self._rouletteBullet = random.randrange(0, 6) irc.reply(_('*SPIN* Are you feeling lucky?'), prefixNick=False) return channel = msg.channel if self._rouletteChamber == self._rouletteBullet: self._rouletteBullet = random.randrange(0, 6) self._rouletteChamber = random.randrange(0, 6) if irc.nick in irc.state.channels[channel].ops or \ irc.nick in irc.state.channels[channel].halfops: irc.queueMsg(ircmsgs.kick(channel, msg.nick, 'BANG!')) else: irc.reply(_('*BANG* Hey, who put a blank in here?!'), prefixNick=False) irc.reply(_('reloads and spins the chambers.'), action=True) else: irc.reply(_('*click*')) self._rouletteChamber += 1 self._rouletteChamber %= 6 roulette = wrap(roulette, ['public', additional(('literal', 'spin'))]) @internationalizeDocstring def monologue(self, irc, msg, args, channel): """[] Returns the number of consecutive lines you've sent in without being interrupted by someone else (i.e. how long your current 'monologue' is). is only necessary if the message isn't sent in the channel itself. """ i = 0 for m in reversed(irc.state.history): if m.command != 'PRIVMSG': continue if not m.prefix: continue if not ircutils.strEqual(m.args[0], channel): continue if msg.prefix == m.prefix: i += 1 else: break irc.reply(format(_('Your current monologue is at least %n long.'), (i, _('line')))) monologue = wrap(monologue, ['channel']) Class = Games # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Games/test.py0000644000175000017500000000544614535072470016177 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class GamesTestCase(ChannelPluginTestCase): plugins = ('Games',) _nonKickRe = re.compile(r'bang|click|spin', re.I) def testRoulette(self): self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick)) sawKick = False for i in range(100): m = self.getMsg('roulette', frm='someoneElse!foo@bar') if m.command == 'PRIVMSG': self.assertTrue(self._nonKickRe.search(m.args[1]), 'Got a msg without bang|click|spin: %r' % m) elif m.command == 'KICK': sawKick = True self.assertIn( 'bang', m.args[2].lower(), 'Got a KICK without bang in it.' ) else: self.fail('Got something other than a kick or a privmsg.') self.assertTrue(sawKick, 'Didn\'t get a kick in %s iterations!' % i) def testEightball(self): self.assertNotError('eightball') self.assertNotError('eightball a') self.assertNotError('eightball ab') self.assertNotError('eightball abc') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Geography/0000755000175000017500000000000014535072535015530 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Geography/__init__.py0000644000175000017500000000502214535072470017636 0ustar00valval### # Copyright (c) 2021, 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. ### """ Geography: Provides geography facts, such as timezones. """ import sys import supybot from supybot import world # Use this for the version of this plugin. __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 if sys.version_info >= (3, 4): from importlib import reload else: 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Geography/common.py0000644000175000017500000000362014535072470017371 0ustar00valval### # Copyright (c) 2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.utils as utils def headers(): headers = utils.web.defaultHeaders.copy() # Comply with https://meta.wikimedia.org/wiki/User-Agent_policy # and https://operations.osmfoundation.org/policies/nominatim/ headers[ "User-agent" ] += " https://github.com/progval/Limnoria/ - Geography plugin" return headers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Geography/config.py0000644000175000017500000000461114535072470017347 0ustar00valval### # Copyright (c) 2021, 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 import conf, registry from supybot.i18n import PluginInternationalization _ = PluginInternationalization("Geography") 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("Geography", True) Geography = conf.registerPlugin("Geography") # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Geography, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Geography/nominatim.py0000644000175000017500000000555414535072470020104 0ustar00valval### # Copyright (c) 2021, 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 json import time import threading import urllib.parse import supybot.utils as utils from .common import headers NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org" _QUERY_LOCK = threading.Lock() _LAST_QUERY_TIME = 0 def _wait_before_query(): """Should be called before any API access. Blocks the current thread in order to follow the rate limit: https://operations.osmfoundation.org/policies/nominatim/""" global _LAST_QUERY_TIME min_time_between_queries = 1.0 with _QUERY_LOCK: time_since_last_query = _LAST_QUERY_TIME - time.time() if time_since_last_query >= min_time_between_queries: time.sleep(min_time_between_queries - time_since_last_query) _LAST_QUERY_TIME = time.time() def _query_nominatim(path, params): url = NOMINATIM_BASE_URL + path + "?" + urllib.parse.urlencode(params) _wait_before_query() content = utils.web.getUrlContent(url, headers=headers()) return json.loads(content) def search_osmids(query): """Queries nominatim's search endpoint and returns a list of OSM ids See https://nominatim.org/release-docs/develop/api/Search/ for details on the query format""" data = _query_nominatim("/search", {"format": "json", "q": query}) return [item["osm_id"] for item in data if item.get("osm_id")] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Geography/plugin.py0000644000175000017500000001460014535072470017377 0ustar00valval### # Copyright (c) 2021, 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 datetime import re from supybot import conf, utils, plugins, ircutils, callbacks from supybot.commands import * from supybot.i18n import PluginInternationalization from . import nominatim from . import wikidata _ = PluginInternationalization("Geography") def timezone_from_uri(irc, uri): try: return wikidata.timezone_from_uri(uri) except utils.time.UnknownTimeZone as e: irc.error( format(_("Could not understand timezone: %s"), e.args[0]), Raise=True, ) except utils.time.MissingTimezoneLibrary: irc.error( _( "Timezone-related commands are not available. " "Your administrator need to either upgrade Python to " "version 3.9 or greater, or install pytz." ), Raise=True, ) except utils.time.TimezoneException as e: irc.error(e.args[0], Raise=True) class Geography(callbacks.Plugin): """Provides geography facts, such as timezones. This plugin uses data from `Wikidata `_ and `OSM/Nominatim `. """ threaded = True @wrap(["text"]) def localtime(self, irc, msg, args, query): """ Returns the current used in the given location. For example, the name could be "Paris" or "Paris, France". The response is formatted according to supybot.reply.format.time This uses data from Wikidata and Nominatim.""" osmids = nominatim.search_osmids(query) if not osmids: irc.error(_("Could not find the location"), Raise=True) for osmid in osmids: uri = wikidata.uri_from_osmid(osmid) if not uri: continue # Get the timezone object (and handle various errors) timezone = timezone_from_uri(irc, uri) if timezone is None: continue # Get the local time now = datetime.datetime.now(tz=timezone) format_ = conf.supybot.reply.format.time.getSpecific( channel=msg.channel, network=irc.network )() # Return it irc.reply(now.strftime(format_)) return irc.error( _("Could not find the timezone of this location."), Raise=True ) def _format_utc_offset(self, offset_seconds): sign = "+" if offset_seconds >= 0 else "-" # Make modulos work as expected offset_seconds = abs(offset_seconds) (offset_minutes, offset_seconds) = divmod(offset_seconds, 60) (offset_hours, offset_minutes) = divmod(offset_minutes, 60) offset = f"{offset_hours}:{offset_minutes:02}:{offset_seconds:02}" # hide seconds and minutes if they are zero offset = re.sub("(:00)+$", "", offset) return f"UTC{sign}{offset}" @wrap(["text"]) def timezone(self, irc, msg, args, query): """ Returns the timezone used in the given location. For example, the name could be "Paris" or "Paris, France". This uses data from Wikidata and Nominatim.""" osmids = nominatim.search_osmids(query) if not osmids: irc.error(_("Could not find the location"), Raise=True) now = datetime.datetime.now(tz=datetime.timezone.utc) for osmid in osmids: uri = wikidata.uri_from_osmid(osmid) if not uri: continue # Get the timezone object (and handle various errors) timezone = timezone_from_uri(irc, uri) if timezone is None: continue offset_seconds = int( datetime.datetime.now(tz=timezone).utcoffset().total_seconds() ) offset = self._format_utc_offset(offset_seconds) # Extract a human-friendly name, depending on the type of # the timezone object: if hasattr(timezone, "key"): # instance of zoneinfo.ZoneInfo irc.reply(format("%s (currently %s)", timezone.key, offset)) return elif hasattr(timezone, "zone"): # instance of pytz.timezone irc.reply(format("%s (currently %s)", timezone.zone, offset)) return else: # probably datetime.timezone built from a constant offset try: offset = int(timezone.utcoffset(now).total_seconds()) except NotImplementedError: continue irc.reply(self._format_utc_offset(offset)) return irc.error( _("Could not find the timezone of this location."), Raise=True ) Class = Geography # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Geography/test.py0000644000175000017500000002412514535072477017072 0ustar00valval### # Copyright (c) 2021, 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 datetime import functools import contextlib from unittest import skipIf from unittest.mock import patch try: import pytz except ImportError: pytz = None try: import zoneinfo except ImportError: zoneinfo = None from supybot.test import * from supybot import utils from . import wikidata from . import nominatim def mock(f): @functools.wraps(f) def newf(self): with patch.object(wikidata, "uri_from_osmid", return_value="foo"): with patch.object(nominatim, "search_osmids", return_value=[42]): f(self) return newf class GeographyTimezoneTestCase(PluginTestCase): plugins = ("Geography",) @skipIf(not pytz, "pytz is not available") @mock def testTimezonePytz(self): tz = pytz.timezone("Europe/Paris") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone Foo Bar", r"Europe/Paris \(currently UTC\+[12]\)" ) tz = pytz.timezone("America/New_York") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone New York", r"America/New_York \(currently UTC-[45]\)" ) tz = pytz.timezone("America/St_Johns") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone Newfoundland", r"America/St_Johns \(currently UTC-[23]:30\)", ) tz = pytz.timezone("Asia/Kolkata") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone Delhi", r"Asia/Kolkata \(currently UTC\+5:30\)" ) @skipIf(not zoneinfo, "Python is older than 3.9") @mock def testTimezoneZoneinfo(self): tz = zoneinfo.ZoneInfo("Europe/Paris") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone Foo Bar", r"Europe/Paris \(currently UTC\+[12]\)" ) tz = zoneinfo.ZoneInfo("America/New_York") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone New York", r"America/New_York \(currently UTC-[45]\)" ) tz = zoneinfo.ZoneInfo("America/St_Johns") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone Newfoundland", r"America/St_Johns \(currently UTC-[23]:30\)", ) tz = zoneinfo.ZoneInfo("Asia/Kolkata") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp( "timezone Delhi", r"Asia/Kolkata \(currently UTC\+5:30\)" ) @skipIf(not zoneinfo, "Python is older than 3.9") @mock def testTimezoneAbsolute(self): tz = datetime.timezone(datetime.timedelta(hours=4)) with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertResponse("timezone Foo Bar", "UTC+4") tz = datetime.timezone(datetime.timedelta(hours=4, minutes=30)) with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertResponse("timezone Foo Bar", "UTC+4:30") tz = datetime.timezone(datetime.timedelta(hours=-4, minutes=30)) with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertResponse("timezone Foo Bar", "UTC-3:30") @skipIf(not network, "Network test") def testTimezoneIntegration(self): self.assertRegexp( "timezone Metz, France", r"Europe/Paris \(currently UTC\+[12]\)" ) self.assertResponse("timezone Saint-Denis, La Réunion", "UTC+4") self.assertRegexp( "timezone Delhi", r"Asia/Kolkata \(currently UTC\+5:30\)" ) self.assertRegexp("timezone Newfoundland", r"UTC-[23]:30") class GeographyLocaltimeTestCase(PluginTestCase): plugins = ("Geography",) @skipIf(not pytz, "pytz is not available") @mock def testLocaltimePytz(self): tz = pytz.timezone("Europe/Paris") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp("localtime Foo Bar", r".*\+0[12]00$") @skipIf(not zoneinfo, "Python is older than 3.9") @mock def testLocaltimeZoneinfo(self): tz = zoneinfo.ZoneInfo("Europe/Paris") with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp("localtime Foo Bar", r".*\+0[12]00$") @skipIf(not zoneinfo, "Python is older than 3.9") @mock def testLocaltimeAbsolute(self): tz = datetime.timezone(datetime.timedelta(hours=4)) with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp("localtime Foo Bar", r".*\+0400$") tz = datetime.timezone(datetime.timedelta(hours=4, minutes=30)) with patch.object(wikidata, "timezone_from_uri", return_value=tz): self.assertRegexp("localtime Foo Bar", r".*\+0430$") @skipIf(not network, "Network test") def testLocaltimeIntegration(self): self.assertRegexp("localtime Metz, France", r".*\+0[12]00$") self.assertRegexp("localtime Saint-Denis, La Réunion", r".*\+0400$") class GeographyWikidataTestCase(SupyTestCase): @skipIf(not network, "Network test") def testRelationOsmidToTimezone(self): self.assertEqual( wikidata.uri_from_osmid(450381), "http://www.wikidata.org/entity/Q22690", ) self.assertEqual( wikidata.uri_from_osmid(192468), "http://www.wikidata.org/entity/Q47045", ) @skipIf(not network, "Network test") def testNodeOsmidToTimezone(self): self.assertEqual( wikidata.uri_from_osmid(436012592), "http://www.wikidata.org/entity/Q933", ) @skipIf(not network, "Network test") def testDirect(self): # The queried object directly has a timezone property self.assertEqual( # New York wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q1384"), utils.time.iana_timezone("America/New_York"), ) @skipIf(not network, "Network test") def testParent(self): # The queried object does not have a TZ property but it is part # of an object that does self.assertEqual( # Metz, France wikidata.timezone_from_uri( "http://www.wikidata.org/entity/Q22690" ), utils.time.iana_timezone("Europe/Paris"), ) @skipIf(not network, "Network test") def testParentAndIgnoreSelf(self): # The queried object has a TZ property, but it's useless to us; # however it is part of an object that has a useful one. self.assertEqual( # New York City, NY wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q60"), utils.time.iana_timezone("America/New_York"), ) self.assertEqual( # Paris, France wikidata.timezone_from_uri("http://www.wikidata.org/entity/Q90"), utils.time.iana_timezone("Europe/Paris"), ) @skipIf(not network, "Network test") def testParentQualifiedIgnorePreferred(self): # The queried object does not have a TZ property, # and is part of an object that does. # However, this parent's 'preferred' timezone is not the # right one, so we must make sure to select the right one # based on P518 ('applies to part'). # La Réunion is a French region, but in UTC+4. # France has a bunch of timezone statements, and 'Europe/Paris' # is marked as Preferred because it is the time of metropolitan # France. However, it is not valid for La Réunion. self.assertEqual( # La Réunion wikidata.timezone_from_uri( "http://www.wikidata.org/entity/Q17070" ), datetime.timezone(datetime.timedelta(hours=4)), ) class GeographyNominatimTestCase(SupyTestCase): @skipIf(not network, "Network test") def testSearch(self): self.assertIn(450381, nominatim.search_osmids("Metz")) results = nominatim.search_osmids("Metz, France") self.assertEqual(results[0], 450381, results) results = nominatim.search_osmids("Saint-Denis, La Réunion") self.assertEqual(results[0], 192468, results) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Geography/wikidata.py0000644000175000017500000001261214535072477017706 0ustar00valval### # Copyright (c) 2021, 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 json import string import datetime import urllib.parse import supybot.utils as utils from .common import headers SPARQL_URL = "https://query.wikidata.org/sparql" TIMEZONE_QUERY = string.Template( """ SELECT ?item ?itemLabel ?rank ?endtime ?appliestopart ?utcoffset ?tzid (MIN(?area) AS ?min_area) WHERE { # find all ?item entities that the subject is part of, recursively; <$subject> (wdt:P131*) ?item. # Get all timezones (returns a superset of "?item wdt:P421 ?timezone", as it does not filter on rank) ?item p:P421 ?statement. ?statement ps:P421 ?timezone. # TODO: order the final result based on the rank? ?statement wikibase:rank ?rank. # fetch the end of validity of the given statement (TODO: check it) OPTIONAL { ?statement pq:P582 ?endtime. } { # filter out statements that apply only to a part of ?item... FILTER NOT EXISTS { ?statement pq:P518 ?appliestopart. } } UNION { # ... unless it applies to a part that contains what we are interested in ?statement pq:P518 ?appliestopart. <$subject> (wdt:P131*) ?appliestopart. } # Filter out values only valid in certain periods of the year (DST vs normal time) FILTER NOT EXISTS { ?statement pq:P1264 ?validinperiod. } # store the identifier of the object the statement applies to BIND(IF(BOUND(?appliestopart),?appliestopart,?item) AS ?statementsubject). # Get the area, will be used to order by specificity OPTIONAL { ?statementsubject wdt:P2046 ?area. } # Require that ?timezone be an instance of... ?timezone (wdt:P31/wdt:P279*) <$tztype>. { # Get either an IANA timezone ID... ?timezone wdt:P6687 ?tzid. } UNION { # ... or an absolute UTC offset ?timezone p:P2907 ?utcoffset_statement. ?utcoffset_statement ps:P2907 ?utcoffset. # unless it is only valid in certain periods of the year (DST vs normal time) FILTER NOT EXISTS { ?utcoffset_statement pq:P1264 ?utcoffset_validinperiod. } } SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } } # Deduplicate in case there is more than one ?area statement GROUP BY ?item ?itemLabel ?rank ?endtime ?appliestopart ?utcoffset ?tzid # Get the smallest entities first. As they are more specific, # they are more likely to be correct. ORDER BY ?min_area DESC(?tzid) LIMIT 1 """ ) OSMID_QUERY = string.Template( """ SELECT ?item WHERE { { ?item wdt:P402 "$osmid". # OSM relation ID } UNION { ?item wdt:P11693 "$osmid". # OSM node ID } } LIMIT 1 """ ) def _query_sparql(query): params = {"format": "json", "query": query} url = SPARQL_URL + "?" + urllib.parse.urlencode(params) content = utils.web.getUrlContent(url, headers=headers()) return json.loads(content) def timezone_from_uri(location_uri): """Returns a :class:datetime.tzinfo object, given a Wikidata Q-ID. eg. ``"Q60"`` for New York City.""" for tztype in [ "http://www.wikidata.org/entity/Q17272692", # IANA timezones first "http://www.wikidata.org/entity/Q12143", # any timezone as a fallback ]: data = _query_sparql( TIMEZONE_QUERY.substitute(subject=location_uri, tztype=tztype) ) results = data["results"]["bindings"] for result in results: if "tzid" in result: return utils.time.iana_timezone(result["tzid"]["value"]) else: assert "utcoffset" in result utc_offset = float(result["utcoffset"]["value"]) return datetime.timezone(datetime.timedelta(hours=utc_offset)) return None def uri_from_osmid(location_osmid): """Returns the wikidata Q-id from an OpenStreetMap ID.""" data = _query_sparql(OSMID_QUERY.substitute(osmid=location_osmid)) results = data["results"]["bindings"] for result in results: return result["item"]["value"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Google/0000755000175000017500000000000014535072535015017 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/__init__.py0000644000175000017500000000471314535072470017133 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Accesses Google for various things. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/config.py0000644000175000017500000001560214535072470016640 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2008-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Google') def configure(advanced): from supybot.questions import output, yn conf.registerPlugin('Google', True) output(_("""The Google plugin has the functionality to watch for URLs that match a specific pattern. (We call this a snarfer) When supybot sees such a URL, it will parse the web page for information and reply with the results.""")) if yn(_('Do you want the Google search snarfer enabled by default?')): conf.supybot.plugins.Google.searchSnarfer.setValue(True) class Language(registry.OnlySomeStrings): transLangs = {'Afrikaans': 'af', 'Albanian': 'sq', 'Amharic': 'am', 'Arabic': 'ar', 'Armenian': 'hy', 'Azerbaijani': 'az', 'Basque': 'eu', 'Belarusian': 'be', 'Bengali': 'bn', 'Bulgarian': 'bg', 'Burmese': 'my', 'Catalan': 'ca', 'Chinese': 'zh', 'Chinese_simplified': 'zh-CN', 'Chinese_traditional': 'zh-TW', 'Croatian': 'hr', 'Czech': 'cs', 'Danish': 'da', 'Dhivehi': 'dv', 'Dutch': 'nl', 'English': 'en', 'Esperanto': 'eo', 'Estonian': 'et', 'Filipino': 'tl', 'Finnish': 'fi', 'French': 'fr', 'Galician': 'gl', 'Georgian': 'ka', 'German': 'de', 'Greek': 'el', 'Gujarati': 'gu', 'Hebrew': 'iw', 'Hindi': 'hi', 'Hungarian': 'hu', 'Icelandic': 'is', 'Indonesian': 'id', 'Inuktitut': 'iu', 'Italian': 'it', 'Japanese': 'ja', 'Kannada': 'kn', 'Kazakh': 'kk', 'Khmer': 'km', 'Korean': 'ko', 'Kurdish': 'ku', 'Kyrgyz': 'ky', 'Laothian': 'lo', 'Latvian': 'lv', 'Lithuanian': 'lt', 'Macedonian': 'mk', 'Malay': 'ms', 'Malayalam': 'ml', 'Maltese': 'mt', 'Marathi': 'mr', 'Mongolian': 'mn', 'Nepali': 'ne', 'Norwegian': 'no', 'Oriya': 'or', 'Pashto': 'ps', 'Persian': 'fa', 'Polish': 'pl', 'Portuguese': 'pt-PT', 'Punjabi': 'pa', 'Romanian': 'ro', 'Russian': 'ru', 'Sanskrit': 'sa', 'Serbian': 'sr', 'Sindhi': 'sd', 'Sinhalese': 'si', 'Slovak': 'sk', 'Slovenian': 'sl', 'Spanish': 'es', 'Swedish': 'sv', 'Tajik': 'tg', 'Tamil': 'ta', 'Tagalog': 'tl', 'Telugu': 'te', 'Thai': 'th', 'Tibetan': 'bo', 'Turkish': 'tr', 'Ukranian': 'uk', 'Urdu': 'ur', 'Uzbek': 'uz', 'Uighur': 'ug', 'Vietnamese': 'vi', 'Detect language': 'auto'} validStrings = ['lang_' + s for s in transLangs.values()] validStrings.append('') def normalize(self, s): if s and not s.startswith('lang_'): s = 'lang_' + s if not s.endswith('CN') or s.endswith('TW') or s.endswith('PT'): s = s.lower() else: s = s.lower()[:-2] + s[-2:] return s class NumSearchResults(registry.PositiveInteger): """Value must be 1 <= n <= 8""" def setValue(self, v): if v > 8: self.error() super(self.__class__, self).setValue(v) class SafeSearch(registry.OnlySomeStrings): validStrings = ['active', 'moderate', 'off'] Google = conf.registerPlugin('Google') conf.registerGlobalValue(Google, 'referer', registry.String('', _("""Determines the URL that will be sent to Google for the Referer field of the search requests. If this value is empty, a Referer will be generated in the following format: http://$server/$botName"""))) conf.registerChannelValue(Google, 'baseUrl', registry.String('google.com', _("""Determines the base URL used for requests."""))) conf.registerChannelValue(Google, 'searchSnarfer', registry.Boolean(False, _("""Determines whether the search snarfer is enabled. If so, messages (even unaddressed ones) beginning with the word 'google' will result in the first URL Google returns being sent to the channel."""))) conf.registerChannelValue(Google, 'colorfulFilter', registry.Boolean(False, _("""Determines whether the word 'google' in the bot's output will be made colorful (like Google's logo)."""))) conf.registerChannelValue(Google, 'bold', registry.Boolean(True, _("""Determines whether results are bolded."""))) conf.registerChannelValue(Google, 'oneToOne', registry.Boolean(False, _("""Determines whether results are sent in different lines or all in the same one."""))) conf.registerChannelValue(Google, 'maximumResults', NumSearchResults(3, _("""Determines the maximum number of results returned from the google command."""))) conf.registerChannelValue(Google, 'defaultLanguage', Language('lang_'+ _('en'), _("""Determines what default language is used in searches. If left empty, no specific language will be requested."""))) conf.registerChannelValue(Google, 'searchFilter', SafeSearch('moderate', _("""Determines what level of search filtering to use by default. 'active' - most filtering, 'moderate' - default filtering, 'off' - no filtering"""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Google/locales/0000755000175000017500000000000014535072535016441 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/locales/fi.po0000644000175000017500000002257314535072470017406 0ustar00valval# Google plugin in Limnoria. # Copyright (C) 2012 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Google plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14: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:40 msgid "" "The Google plugin has the functionality to watch for URLs\n" " that match a specific pattern. (We call this a snarfer)\n" " When supybot sees such a URL, it will parse the web page\n" " for information and reply with the results." msgstr "" "Google lisäosalla on toiminnallisuus vahtia URL-osoitteita, jotka\n" " täsmäävät tiettyyn kuvioon. (Me kutsumme tätä kaappaajaksi)\n" " Kun supybot näkee sellaisen URL-osoitteen, se jäsentää verkkosivun\n" " tiedot ja vastaa tuloksilla." #: config.py:44 msgid "Do you want the Google search snarfer enabled by default?" msgstr "Tahdotko Google haku kaappaajan olevan oletuksenä käytössä?" #: config.py:90 msgid "Value must be 1 <= n <= 8" msgstr "Arvon täytyy olla 1 <= n <= 8" #: config.py:101 msgid "" "Determines the URL that will be sent to Google for\n" " the Referer field of the search requests. If this value is empty, a\n" " Referer will be generated in the following format:\n" " http://$server/$botName" msgstr "" "Määrittää URLin, joka lähetetään Googlelle\n" " hakupyyntöjen Referer kenttään. Jos tämä arvo on tyhjä, \n" " Referer luodaan seuraavassa muodossa:\n" " http://$palvelin/$botinNimi" #: config.py:106 msgid "" "Determines the base URL used for\n" " requests." msgstr "" "Märittää perus URL-osoitteen, jota käytetään\n" " hakupyynnöille." #: config.py:109 msgid "" "Determines whether the search snarfer is\n" " enabled. If so, messages (even unaddressed ones) beginning with the " "word\n" " 'google' will result in the first URL Google returns being sent to the\n" " channel." msgstr "" "Määrittää onko haku kaappaaja\n" " käytössä. Jos niin, viestit (jopa botille osoittamattomat), jotka " "alkavat sanalla\n" " 'google' tuovat ensinmäisen URLin jonka Google haku palauttaa\n" " kanavalle." #: config.py:114 msgid "" "Determines whether the word 'google' in the\n" " bot's output will be made colorful (like Google's logo)." msgstr "" "Määrittää tehdäänkö sana 'google'\n" " botin ulostulossa värikkääksi (kuten Googlen logo)." #: config.py:117 msgid "Determines whether results are bolded." msgstr "Määrittää korostetaanko viestit." #: config.py:119 msgid "" "Determines whether results are sent in\n" " different lines or all in the same one." msgstr "Määrittää lähetetäänkö tulokset eri vai samoilla riveillä." #: config.py:122 msgid "" "Determines the maximum number of results returned\n" " from the google command." msgstr "" "Määrittää maksimi numeron tuloksia, jotka palaavat\n" " Google komennolla." #: config.py:125 msgid "" "Determines what default language is used in\n" " searches. If left empty, no specific language will be requested." msgstr "" "Määrittää mitä oletuskieltä käytetään hauissa.\n" " jos jätetty tyhjäksi, yhtään tiettyä kieltä ei pyydetä." #: config.py:125 msgid "en" msgstr "en" #: config.py:128 msgid "" "Determines what level of search filtering to use\n" " by default. 'active' - most filtering, 'moderate' - default filtering,\n" " 'off' - no filtering" msgstr "" "Määrittää minkälaista suodatusta käytetään\n" " oletuksena. 'active' - eniten suodatusta, 'moderate' - oletus " "suodatus,\n" " 'off' - ei suodatusta." #: plugin.py:50 msgid "" "\n" " This is a simple plugin to provide access to the Google services we\n" " all know and love from our favorite IRC bot.\n" "\n" " 1. google\n" "\n" " Searches for a string and gives you 3 results from Google search\n" " ``!google something``\n" "\n" " 2. lucky\n" "\n" " Return the first result (Google's \"I'm Feeling Lucky\" search)\n" " ``!lucky something``\n" "\n" " 3. calc\n" "\n" " Does mathematic calculations\n" " ``!calc 5+4``\n" "\n" " 4. translate\n" "\n" " Translates a string\n" " ``!translate en ar test``\n" "\n" " Check: `Supported language codes`_\n" "\n" " .. _Supported language codes: `\n" " " msgstr "" #: plugin.py:116 #, fuzzy msgid "" "search(\"search phrase\", options={})\n" "\n" " Valid options are:\n" " smallsearch - True/False (Default: False)\n" " filter - {active,moderate,off} (Default: \"moderate\")\n" " language - Restrict search to documents in the given language\n" " (Default: \"lang_en\")\n" " " msgstr "" "Tee jaku käyttämällä Googlen AJAX APIa.\n" " haky(\"hakusana\", asetukset={})\n" "\n" " Kelvolliset asetukset ovat:\n" " smallsearch - True/False (Oletus: False)\n" " filter - {active,moderate,off} (Oletus: \"moderate\")\n" " language - Rajoita haku documentteihin annetulla kielellä.\n" " (Oletus: \"lang_en\")\n" " " #: plugin.py:179 msgid "No matches found." msgstr "Osumia ei löytynyt." #: plugin.py:187 msgid "" "[--snippet] \n" "\n" " Does a google search, but only returns the first result.\n" " If option --snippet is given, returns also the page text snippet.\n" " " msgstr "" "[--snippet] \n" "\n" " Tekee google haun, mutta palauttaa vain ensinmäisen tuloksen.\n" " Jos --snippet on annettu, palauttaa myös pätkän sivun tekstistä.\n" " " #: plugin.py:206 msgid "Google found nothing." msgstr "Google ei löytänyt mitään." #: plugin.py:211 msgid "" " [--{filter,language} ]\n" "\n" " Searches google.com for the given string. As many results as can " "fit\n" " are included. --language accepts a language abbreviation; --filter\n" " accepts a filtering level ('active', 'moderate', 'off').\n" " " msgstr "" " [--{suodatin,kieli} ]\n" "\n" " Hakee google.com :ista annetulla merkkiketjulla. Niin monta " "tulosta, kuin mahtuu\n" " sisällytetään. --language hyväksyy kieli lyhenteen; --filter\n" " hyväksyy suodatus tason ('active', 'moderate', 'off').\n" " " #: plugin.py:236 msgid "" "\n" "\n" " Returns a link to the cached version of if it is available.\n" " " msgstr "" "\n" "\n" " Palauttaa linkin välimuistissa olevaan versioon, jos se on " "saatavilla.\n" " " #: plugin.py:247 msgid "Google seems to have no cache for that site." msgstr "Googlella ei näytä olevan välimuistia tuolle sivulle." #: plugin.py:253 msgid "" " [ ...]\n" "\n" " Returns the results of each search, in order, from greatest number\n" " of results to least.\n" " " msgstr "" " [ ...]\n" "\n" " Palauttaa tulokset jokaiselle haulle järjestyksessä suurimmasta " "numerosta\n" " pienimpään.\n" " " #: plugin.py:309 #, fuzzy msgid "No translations found." msgstr "Osumia ei löytynyt." #: plugin.py:313 #, fuzzy msgid "" " [to] \n" "\n" " Returns translated from into . and take language\n" " codes (not language names), which are listed here:\n" " https://cloud.google.com/translate/docs/languages\n" " " msgstr "" " [to] \n" "\n" " Kääntää .\n" " " #: plugin.py:325 msgid "^google\\s+(.*)$" msgstr "^google\\s+(.*)$" #: plugin.py:347 msgid "" "\n" "\n" " Uses Google's calculator to calculate the value of .\n" " " msgstr "" "\n" "\n" " Käyttää Googlen laskinta laskeakseen arvon.\n" " " #: plugin.py:378 msgid "" "\n" "\n" " Looks up on Google.\n" " " msgstr "" "\n" "\n" " Etsii Googlesta.\n" " " #: plugin.py:392 msgid "Google's phonebook didn't come up with anything." msgstr "Googlen puhelinluettelo ei keksinyt mitään." #~ msgid "" #~ "This is a simple plugin to provide access to the Google services we\n" #~ " all know and love from our favorite IRC bot." #~ msgstr "" #~ "Tämä on yksinkertainen plugini tarjoamaan pääsyn Googlen palveluihin, " #~ "jotka me kaikki tunnemme, ja joita me kaikki\n" #~ " rakastamme suosikki IRC-botistamme." #~ msgid "We broke The Google!" #~ msgstr "Me hajotimme Googlen!" #~ msgid "Google says: Error: %s." #~ msgstr "Google sanoo: Virhe: %s." #~ msgid "from language" #~ msgstr "kielestä" #~ msgid "Valid languages are: %L" #~ msgstr "Kelvolliset kielet ovat: %L" #~ msgid "to language" #~ msgstr "kielelle" #~ msgid "Google's calculator didn't come up with anything." #~ msgstr "Googlen laskin ei keksinyt mitään." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/locales/fr.po0000644000175000017500000002145314535072470017413 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:40 msgid "" "The Google plugin has the functionality to watch for URLs\n" " that match a specific pattern. (We call this a snarfer)\n" " When supybot sees such a URL, it will parse the web page\n" " for information and reply with the results." msgstr "" "Le plugin Google sert à lire des URLs qui correspondent à un masque " "spécifique (nous appelons ça un 'snarfer'). Lorsque Supybot voit une telle " "URL, il parse la page web pour trouver des informations et répond avec les " "résultats." #: config.py:44 msgid "Do you want the Google search snarfer enabled by default?" msgstr "Voulez-vous activer le snarfer de recherche Google par défaut ?" #: config.py:90 msgid "Value must be 1 <= n <= 8" msgstr "La valeur doit être comprise entre 1 et 8 (inclus)" #: config.py:101 msgid "" "Determines the URL that will be sent to Google for\n" " the Referer field of the search requests. If this value is empty, a\n" " Referer will be generated in the following format:\n" " http://$server/$botName" msgstr "" "Détermine l'URL qui sera envoyée à Google comme Referer pour les recerches. " "Si la valeur est vide, un Refere sera automatiquement généré dans le " "format : http://$server/$botNamr" #: config.py:106 msgid "" "Determines the base URL used for\n" " requests." msgstr "Détermine l’URL de base utilisée pour les requêtes." #: config.py:109 msgid "" "Determines whether the search snarfer is\n" " enabled. If so, messages (even unaddressed ones) beginning with the " "word\n" " 'google' will result in the first URL Google returns being sent to the\n" " channel." msgstr "" "Détermine si le snarger de recherche est activé. Si c'est le cas, les " "messages (même non adressés) commençant par 'google' seront répondus par la " "première URL que Google donne pour cette recherche." #: config.py:114 msgid "" "Determines whether the word 'google' in the\n" " bot's output will be made colorful (like Google's logo)." msgstr "" "Détermine si le mot 'google' dans la sortie du bot sera coloré (comme le " "logo de Google)" #: config.py:117 msgid "Determines whether results are bolded." msgstr "Détermine si les résultats sont mis en gras." #: config.py:119 msgid "" "Determines whether results are sent in\n" " different lines or all in the same one." msgstr "" "Détermine si les résultats sont envoyés sur différentes lignes, ou tous sur " "la même." #: config.py:122 msgid "" "Determines the maximum number of results returned\n" " from the google command." msgstr "" "Détermine le nombre maximum de résultats retournés par la commande google." #: config.py:125 msgid "" "Determines what default language is used in\n" " searches. If left empty, no specific language will be requested." msgstr "" "Détermine quand langue par défaut est utilisée dans les recherches. Si " "laissé vide, aucune langue spécifique ne sera demandée." #: config.py:125 msgid "en" msgstr "fr" #: config.py:128 msgid "" "Determines what level of search filtering to use\n" " by default. 'active' - most filtering, 'moderate' - default filtering,\n" " 'off' - no filtering" msgstr "" "Détermine le niveau de filtrage à utiliser par défaut. 'active' filtre tout, " "'moderate' est le filtre par défaut, et 'off' désactive le filtrage." #: plugin.py:50 msgid "" "\n" " This is a simple plugin to provide access to the Google services we\n" " all know and love from our favorite IRC bot.\n" "\n" " 1. google\n" "\n" " Searches for a string and gives you 3 results from Google search\n" " ``!google something``\n" "\n" " 2. lucky\n" "\n" " Return the first result (Google's \"I'm Feeling Lucky\" search)\n" " ``!lucky something``\n" "\n" " 3. calc\n" "\n" " Does mathematic calculations\n" " ``!calc 5+4``\n" "\n" " 4. translate\n" "\n" " Translates a string\n" " ``!translate en ar test``\n" "\n" " Check: `Supported language codes`_\n" "\n" " .. _Supported language codes: `\n" " " msgstr "" #: plugin.py:116 #, fuzzy msgid "" "search(\"search phrase\", options={})\n" "\n" " Valid options are:\n" " smallsearch - True/False (Default: False)\n" " filter - {active,moderate,off} (Default: \"moderate\")\n" " language - Restrict search to documents in the given language\n" " (Default: \"lang_en\")\n" " " msgstr "" "Perform a search using Google's AJAX API.\n" " search(\"search phrase\", options={})\n" "\n" " Valid options are:\n" " smallsearch - True/False (Default: False)\n" " filter - {active,moderate,off} (Default: \"moderate\")\n" " language - Restrict search to documents in the given language\n" " (Default: \"lang_en\")\n" " " #: plugin.py:179 msgid "No matches found." msgstr "Aucune correspondance." #: plugin.py:187 msgid "" "[--snippet] \n" "\n" " Does a google search, but only returns the first result.\n" " If option --snippet is given, returns also the page text snippet.\n" " " msgstr "" "[--snippet] \n" "\n" "Effectue une recherche google, mais ne retourne que le premier résultat. Si " "l'option --snippet est donnée, retournera également un fragment de la page." #: plugin.py:206 msgid "Google found nothing." msgstr "Google n'a rien trouvé." #: plugin.py:211 msgid "" " [--{filter,language} ]\n" "\n" " Searches google.com for the given string. As many results as can " "fit\n" " are included. --language accepts a language abbreviation; --filter\n" " accepts a filtering level ('active', 'moderate', 'off').\n" " " msgstr "" " [--filter ] [--language ]\n" "\n" "Rercherche la chaîne donnée sur Google. Autant de résultats que possible " "sont donnés. --language accepte une abbréviation de langue ; --filter " "accepte un niveau de filtrage ('active', 'moderate', 'off')." #: plugin.py:236 msgid "" "\n" "\n" " Returns a link to the cached version of if it is available.\n" " " msgstr "" "\n" "\n" "Retourne un lien vers la version en cache de l', si elle est disponible." #: plugin.py:247 msgid "Google seems to have no cache for that site." msgstr "Google semble ne pas avoir de cache pour ce site." #: plugin.py:253 msgid "" " [ ...]\n" "\n" " Returns the results of each search, in order, from greatest number\n" " of results to least.\n" " " msgstr "" " [ ...]\n" "\n" "Retourne les résultats de chaque recherche, dans l'ordre, par ordre " "croissant du nombre de résultats." #: plugin.py:309 #, fuzzy msgid "No translations found." msgstr "Aucune correspondance." #: plugin.py:313 #, fuzzy msgid "" " [to] \n" "\n" " Returns translated from into . and take language\n" " codes (not language names), which are listed here:\n" " https://cloud.google.com/translate/docs/languages\n" " " msgstr "" " [to] \n" "\n" "Retourne le , traduire de la vers la ." #: plugin.py:325 msgid "^google\\s+(.*)$" msgstr "^google\\s+(.*)$" #: plugin.py:347 msgid "" "\n" "\n" " Uses Google's calculator to calculate the value of .\n" " " msgstr "" "\n" "\n" "Utilise la calculatrice Google pour calculer la valeur de l'." #: plugin.py:378 msgid "" "\n" "\n" " Looks up on Google.\n" " " msgstr "" "\n" "\n" "Recherche le sur Google." #: plugin.py:392 msgid "Google's phonebook didn't come up with anything." msgstr "L'annuaire téléphonique de Google ne donne aucun résultat." #~ msgid "We broke The Google!" #~ msgstr "Google est toukassay !" #~ msgid "Google says: Error: %s." #~ msgstr "Google dit : Erreur : %s." #~ msgid "from language" #~ msgstr "de la langue" #~ msgid "Valid languages are: %L" #~ msgstr "Les langues valides sont : %L" #~ msgid "to language" #~ msgstr "vers la langue" #~ msgid "Google's calculator didn't come up with anything." #~ msgstr "La calculatrice Google ne donne aucun résultat." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/locales/it.po0000644000175000017500000002145014535072470017415 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-15 20:55+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:40 msgid "" "The Google plugin has the functionality to watch for URLs\n" " that match a specific pattern. (We call this a snarfer)\n" " When supybot sees such a URL, it will parse the web page\n" " for information and reply with the results." msgstr "" "Il plugin Google ha la funzione di leggere gli URL che corrispondono ad\n" " un modello specifico (viene chiamato snarfer).\n" " Quando supybot vede un URL, analizza la pagina web per\n" " ottenere informazioni e rispondere con dei risultati." #: config.py:44 msgid "Do you want the Google search snarfer enabled by default?" msgstr "Vuoi che lo snarfer per la ricerca in Google sia abilitato di default?" #: config.py:90 msgid "Value must be 1 <= n <= 8" msgstr "Il valore deve essere compreso tra 1 e 8 (incluso)" #: config.py:101 msgid "" "Determines the URL that will be sent to Google for\n" " the Referer field of the search requests. If this value is empty, a\n" " Referer will be generated in the following format:\n" " http://$server/$botName" msgstr "" "Determina l'URL che sarà inviato a Google come Referer per le ricerche.\n" " Se questo valore è vuoto, verrà generato un Referer nel seguente " "formato:\n" " http://$server/$botName" #: config.py:106 msgid "" "Determines the base URL used for\n" " requests." msgstr "" #: config.py:109 msgid "" "Determines whether the search snarfer is\n" " enabled. If so, messages (even unaddressed ones) beginning with the " "word\n" " 'google' will result in the first URL Google returns being sent to the\n" " channel." msgstr "" "Determina se lo snarfer per la ricerca è abilitato. In caso lo sia, i " "messaggi\n" " (anche quelli non diretti) che iniziano con la parola \"google\" " "invieranno\n" " in canale il primo URL che Google restituisce." #: config.py:114 msgid "" "Determines whether the word 'google' in the\n" " bot's output will be made colorful (like Google's logo)." msgstr "" "Determina se la parola \"google\" nell'output del bot sarà colorata (come il " "logo di Google)." #: config.py:117 msgid "Determines whether results are bolded." msgstr "Determina se i risultati saranno in grassetto." #: config.py:119 msgid "" "Determines whether results are sent in\n" " different lines or all in the same one." msgstr "" #: config.py:122 msgid "" "Determines the maximum number of results returned\n" " from the google command." msgstr "" "Determina il numero massimo di risultati restituiti dal comando google." #: config.py:125 msgid "" "Determines what default language is used in\n" " searches. If left empty, no specific language will be requested." msgstr "" "Determina la lingua predefinita usata nelle ricerche. Se lasciata vuota,\n" " non sarà richiesta nessuna lingua specifica." #: config.py:125 msgid "en" msgstr "it" #: config.py:128 msgid "" "Determines what level of search filtering to use\n" " by default. 'active' - most filtering, 'moderate' - default filtering,\n" " 'off' - no filtering" msgstr "" "Determina quale livello di filtraggio usare in modo predefinito. \"active" "\":\n" " filtra tutto; \"moderate\": filtro predefinito; \"off\": filtro " "disattivato." #: plugin.py:50 msgid "" "\n" " This is a simple plugin to provide access to the Google services we\n" " all know and love from our favorite IRC bot.\n" "\n" " 1. google\n" "\n" " Searches for a string and gives you 3 results from Google search\n" " ``!google something``\n" "\n" " 2. lucky\n" "\n" " Return the first result (Google's \"I'm Feeling Lucky\" search)\n" " ``!lucky something``\n" "\n" " 3. calc\n" "\n" " Does mathematic calculations\n" " ``!calc 5+4``\n" "\n" " 4. translate\n" "\n" " Translates a string\n" " ``!translate en ar test``\n" "\n" " Check: `Supported language codes`_\n" "\n" " .. _Supported language codes: `\n" " " msgstr "" #: plugin.py:116 #, fuzzy msgid "" "search(\"search phrase\", options={})\n" "\n" " Valid options are:\n" " smallsearch - True/False (Default: False)\n" " filter - {active,moderate,off} (Default: \"moderate\")\n" " language - Restrict search to documents in the given language\n" " (Default: \"lang_en\")\n" " " msgstr "" "Esegue una ricerca usando l'API AJAX di Google.\n" " search(\"search phrase\", options={})\n" "\n" " Le opzioni valide sono:\n" " smallsearch - True/False (Default: False)\n" " filter - {active,moderate,off} (Default: \"moderate\")\n" " language - Restringe la ricerca alla lingua specificata\n" " (Default: \"lang_en\")\n" " " #: plugin.py:179 msgid "No matches found." msgstr "Nessun risultato trovato." #: plugin.py:187 msgid "" "[--snippet] \n" "\n" " Does a google search, but only returns the first result.\n" " If option --snippet is given, returns also the page text snippet.\n" " " msgstr "" "[--snippet] \n" "\n" " Effettua una ricerca su Google restituendo solo il primo risultato.\n" " Se l'opzione --snippet è specificata, riporta anche un frammento di " "testo della pagina.\n" " " #: plugin.py:206 msgid "Google found nothing." msgstr "Google non ha trovato nulla." #: plugin.py:211 msgid "" " [--{filter,language} ]\n" "\n" " Searches google.com for the given string. As many results as can " "fit\n" " are included. --language accepts a language abbreviation; --filter\n" " accepts a filtering level ('active', 'moderate', 'off').\n" " " msgstr "" " [--{filter,language} ]\n" "\n" " Ricerca la data stringa su google.com fornendo tutti i risultati " "possibili.\n" " --language accetta un'abbreviazione di lingua; --filter un livello " "di filtraggio\n" " ('active', 'moderate', 'off').\n" " " #: plugin.py:236 msgid "" "\n" "\n" " Returns a link to the cached version of if it is available.\n" " " msgstr "" "\n" "\n" " Restituisce un link della versione in cache di , se " "disponibile.\n" " " #: plugin.py:247 msgid "Google seems to have no cache for that site." msgstr "Google sembra non avere cache per questo sito." #: plugin.py:253 msgid "" " [ ...]\n" "\n" " Returns the results of each search, in order, from greatest number\n" " of results to least.\n" " " msgstr "" " [ ...]\n" "\n" " Restituisce i risultati di ogni ricerca in ordine decrescente in " "base al numero di risultati.\n" " " #: plugin.py:309 #, fuzzy msgid "No translations found." msgstr "Nessun risultato trovato." #: plugin.py:313 #, fuzzy msgid "" " [to] \n" "\n" " Returns translated from into . and take language\n" " codes (not language names), which are listed here:\n" " https://cloud.google.com/translate/docs/languages\n" " " msgstr "" " [to] \n" "\n" " Restituisce tradotto . Attenzione\n" " che tradurre da o verso lingue che usano caratteri multibyte\n" " può produrre risultati molto strani.\n" " " #: plugin.py:325 msgid "^google\\s+(.*)$" msgstr "^google\\s+(.*)$" #: plugin.py:347 msgid "" "\n" "\n" " Uses Google's calculator to calculate the value of .\n" " " msgstr "" "\n" "\n" " Utilizza la calcolatrice di Google per calcolare il valore di " ".\n" " " #: plugin.py:378 msgid "" "\n" "\n" " Looks up on Google.\n" " " msgstr "" "\n" "\n" " Cerca su Google.\n" " " #: plugin.py:392 msgid "Google's phonebook didn't come up with anything." msgstr "La rubrica di Google non ha fornito alcun risultato." #~ msgid "We broke The Google!" #~ msgstr "Abbiamo rotto Google!" #~ msgid "from language" #~ msgstr "da lingua" #~ msgid "Valid languages are: %L" #~ msgstr "Le lingue valide sono: %L" #~ msgid "to language" #~ msgstr "a lingua" #~ msgid "Google says: Error: %s." #~ msgstr "Google dice: errore: %s." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/parser.py0000644000175000017500000001055414535072470016670 0ustar00valval### # Copyright (c) 2020-2021, 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 enum import collections from html.parser import HTMLParser import supybot.utils as utils result = collections.namedtuple('result', 'link title snippet') @enum.unique class ParserState(enum.Enum): OUTSIDE = 0 IN_LINK = 1 IN_TITLE = 2 TITLE_PARSED = 3 BREADCRUMBS_PARSED = 5 LINK_PARSED = 6 @enum.unique class DomMark(enum.Enum): """A mark on an element in the stack, to know when to change state when poping the element from the stack.""" HEADING = 1 BREADCRUMBS = 2 STACKED_TAGS = ('div', 'span', 'a') class GoogleHTMLParser(HTMLParser): def __init__(self): super().__init__() self.stack = [] self.results = [] self.reset_current_result() def reset_current_result(self): self.state = ParserState.OUTSIDE self.current_link = None self.current_title = None self.current_snippet = None def handle_starttag(self, tag, attrs): attrs = dict(attrs) classes = attrs.get('class', '').split() if tag in STACKED_TAGS: self.stack.append(tag) if tag == 'a' and attrs['href'].startswith('/url?q=') \ and self.state == ParserState.OUTSIDE: self.state = ParserState.IN_LINK href = attrs['href'][len('/url?q='):] self.current_link = utils.web.urlunquote(href.split('&sa')[0]) elif tag == 'h3' and 'a' in self.stack and self.state == ParserState.IN_LINK: self.state = ParserState.IN_TITLE mark = DomMark.HEADING def handle_endtag(self, tag): if tag in STACKED_TAGS: item = self.stack.pop() assert item == tag, (item, tag) if tag == 'a' and self.state in ( ParserState.IN_LINK, ParserState.IN_TITLE, ParserState.BREADCRUMBS_PARSED): if self.current_title is None: # That wasn't a result self.state = ParserState.OUTSIDE else: self.state = ParserState.LINK_PARSED def handle_data(self, data): if self.state == ParserState.IN_TITLE: self.current_title = data self.state = ParserState.TITLE_PARSED elif self.state == ParserState.TITLE_PARSED: self.state = ParserState.BREADCRUMBS_PARSED elif self.state == ParserState.LINK_PARSED: self.current_snippet = data self.state = ParserState.OUTSIDE self.build_result() def build_result(self): self.results.append(result( link=self.current_link, title=self.current_title, snippet=self.current_snippet, )) self.reset_current_result() if __name__ == '__main__': parser = GoogleHTMLParser() with open('google.html') as fd: parser.feed(fd.read()) print(parser.results) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/plugin.py0000644000175000017500000002665514535072470016703 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import sys import json import supybot.conf as conf import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.utils.minisix as minisix import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Google') from .parser import GoogleHTMLParser class Google(callbacks.PluginRegexp): """ This is a simple plugin to provide access to the Google services we all know and love from our favorite IRC bot. 1. google Searches for a string and gives you 3 results from Google search ``!google something`` 2. lucky Return the first result (Google's "I'm Feeling Lucky" search) ``!lucky something`` 3. calc Does mathematic calculations ``!calc 5+4`` 4. translate Translates a string ``!translate en ar test`` Check: `Supported language codes`_ .. _Supported language codes: ` """ threaded = True callBefore = ['Web'] regexps = ['googleSnarfer'] _colorGoogles = {} def _getColorGoogle(self, m): s = m.group(1) ret = self._colorGoogles.get(s) if not ret: L = list(s) L[0] = ircutils.mircColor(L[0], 'blue')[:-1] L[1] = ircutils.mircColor(L[1], 'red')[:-1] L[2] = ircutils.mircColor(L[2], 'yellow')[:-1] L[3] = ircutils.mircColor(L[3], 'blue')[:-1] L[4] = ircutils.mircColor(L[4], 'green')[:-1] L[5] = ircutils.mircColor(L[5], 'red') ret = ''.join(L) self._colorGoogles[s] = ret return ircutils.bold(ret) _googleRe = re.compile(r'\b(google)\b', re.I) def outFilter(self, irc, msg): if msg.command == 'PRIVMSG' and \ self.registryValue('colorfulFilter', msg.channel, irc.network): s = msg.args[1] s = re.sub(self._googleRe, self._getColorGoogle, s) msg = ircmsgs.privmsg(msg.args[0], s, msg=msg) return msg @classmethod def decode(cls, text): parser = GoogleHTMLParser() parser.feed(text) return parser.results _gsearchUrl = 'https://www.google.com/search' def search(self, query, channel, network, options={}): """search("search phrase", options={}) Valid options are: smallsearch - True/False (Default: False) filter - {active,moderate,off} (Default: "moderate") language - Restrict search to documents in the given language (Default: "lang_en") """ self.log.warning('The Google plugin search is deprecated since ' 'Google closed their public API and will be removed in a ' 'future release. Please consider switching to an other ' 'plugin for your searches, like DDG (built-in)') ref = self.registryValue('referer') if not ref: ref = 'http://%s/%s' % (dynamic.irc.server, dynamic.irc.nick) headers = dict(utils.web.defaultHeaders) headers['Referer'] = ref headers['User-agent'] = 'Mozilla/5.0 (compatible; utils.web python module)' opts = {'q': query, 'gbv': '2'} for (k, v) in options.items(): if k == 'smallsearch': if v: opts['rsz'] = 'small' else: opts['rsz'] = 'large' elif k == 'filter': opts['safe'] = v elif k == 'language': opts['hl'] = v defLang = self.registryValue('defaultLanguage', channel, network) if 'hl' not in opts and defLang: opts['hl'] = defLang.strip('lang_') if 'safe' not in opts: opts['safe'] = self.registryValue('searchFilter', channel, network) if 'rsz' not in opts: opts['rsz'] = 'large' text = utils.web.getUrl('%s?%s' % (self._gsearchUrl, utils.web.urlencode(opts)), headers=headers).decode('utf8') return text def formatData(self, data, bold=True, max=0, onetoone=False): data = self.decode(data) results = [] if max: data = data[:max] for result in data: title = utils.web.htmlToText(result.title.encode('utf-8')) url = result.link if minisix.PY2: url = url.encode('utf-8') if title: if bold: title = ircutils.bold(title) results.append(format('%s: %u', title, url)) else: results.append(url) if minisix.PY2: repl = lambda x:x if isinstance(x, unicode) else unicode(x, 'utf8') results = list(map(repl, results)) if not results: return [_('No matches found.')] elif onetoone: return results else: return [minisix.u('; ').join(results)] @internationalizeDocstring def lucky(self, irc, msg, args, opts, text): """[--snippet] Does a google search, but only returns the first result. If option --snippet is given, returns also the page text snippet. """ opts = dict(opts) data = self.search(text, msg.channel, irc.network, {'smallsearch': True}) data = self.decode(data) if data: url = data[0].link if 'snippet' in opts: snippet = data[0].snippet snippet = " | " + utils.web.htmlToText(snippet, tagReplace='') else: snippet = "" result = url + snippet irc.reply(result) else: irc.reply(_('Google found nothing.')) lucky = wrap(lucky, [getopts({'snippet':'',}), 'text']) @internationalizeDocstring def google(self, irc, msg, args, optlist, text): """ [--{filter,language} ] Searches google.com for the given string. As many results as can fit are included. --language accepts a language abbreviation; --filter accepts a filtering level ('active', 'moderate', 'off'). """ if 'language' in optlist and optlist['language'].lower() not in \ conf.supybot.plugins.Google.safesearch.validStrings: irc.errorInvalid('language') data = self.search(text, msg.channel, irc.network, dict(optlist)) bold = self.registryValue('bold', msg.channel, irc.network) max = self.registryValue('maximumResults', msg.channel, irc.network) # We don't use supybot.reply.oneToOne here, because you generally # do not want @google to echo ~20 lines of results, even if you # have reply.oneToOne enabled. onetoone = self.registryValue('oneToOne', msg.channel, irc.network) for result in self.formatData(data, bold=bold, max=max, onetoone=onetoone): irc.reply(result) google = wrap(google, [getopts({'language':'something', 'filter':''}), 'text']) def _translate(self, sourceLang, targetLang, text): headers = dict(utils.web.defaultHeaders) headers['User-agent'] = ('Mozilla/5.0 (X11; U; Linux i686) ' 'Gecko/20071127 Firefox/2.0.0.11') sourceLang = utils.web.urlquote(sourceLang) targetLang = utils.web.urlquote(targetLang) text = utils.web.urlquote(text) result = utils.web.getUrlFd('http://translate.googleapis.com/translate_a/single' '?client=gtx&dt=t&sl=%s&tl=%s&q=' '%s' % (sourceLang, targetLang, text), headers).read().decode('utf8') while ',,' in result: result = result.replace(',,', ',null,') while '[,' in result: result = result.replace('[,', '[') data = json.loads(result) try: language = data[2] except: language = 'unknown' if data[0]: return (''.join(x[0] for x in data[0]), language) else: return (_('No translations found.'), language) @internationalizeDocstring def translate(self, irc, msg, args, sourceLang, targetLang, text): """ [to] Returns translated from into . and take language codes (not language names), which are listed here: https://cloud.google.com/translate/docs/languages """ (text, language) = self._translate(sourceLang, targetLang, text) irc.reply(text, language) translate = wrap(translate, ['something', 'to', 'something', 'text']) def googleSnarfer(self, irc, msg, match): r"^google\s+(.*)$" if not self.registryValue('searchSnarfer', msg.channel, irc.network): return searchString = match.group(1) data = self.search(searchString, msg.channel, irc.network, {'smallsearch': True}) if data['responseData']['results']: url = data['responseData']['results'][0]['unescapedUrl'] irc.reply(url, prefixNick=False) googleSnarfer = urlSnarfer(googleSnarfer) def _googleUrl(self, s, channel, network): s = utils.web.urlquote_plus(s) url = r'http://%s/search?q=%s' % \ (self.registryValue('baseUrl', channel, network), s) return url Class = Google # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Google/test.py0000644000175000017500000000602714535072470016353 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008-2009, James Vega # Copyright (c) 2010-2021, 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 * class GoogleTestCase(ChannelPluginTestCase): plugins = ('Google', 'Config') if network: def testSearch(self): self.assertNotError('google foo') self.assertRegexp('google dupa', r'dupa') # Unicode check self.assertNotError('google ae') def testUrlDecode(self): self.assertRegexp( 'google site:http://www.urbandictionary.com carajo land', '\x02carajo land - Urban Dictionary\x02: ' r'https?://www.urbandictionary.com/define.php\?term=carajo%20land') def testLucky(self): self.assertResponse('lucky Hacker News', 'https://news.ycombinator.com/') def testSearchFormat(self): self.assertRegexp('google foo', '') self.assertNotError('config reply.format.url %s') self.assertRegexp('google foo', 'https?://.*') self.assertNotRegexp('google foo', '') def testSearchOneToOne(self): self.assertRegexp('google dupa', ';') self.assertNotError('config plugins.Google.oneToOne True') self.assertNotRegexp('google dupa', ';') def testTranslate(self): self.assertRegexp('translate en es hello world', 'Hola mundo') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Hashes/0000755000175000017500000000000014535072535015016 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Hashes/__init__.py0000644000175000017500000000503114535072470017124 0ustar00valval### # Copyright (c) 2017, Ken Spencer # Copyright (c) 2017-2021, 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. ### """ Provides various hash- and encryption-related 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.Author('Ken Spencer', 'kspencer/IotaSpencer', 'ken@electrocode.net') __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Hashes/config.py0000644000175000017500000000430414535072470016634 0ustar00valval### # Copyright (c) 2017, Ken Spencer # Copyright (c) 2017-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Hashes') 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('Hashes', True) String = conf.registerPlugin('Hashes') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Hashes/plugin.py0000644000175000017500000000772114535072470016673 0ustar00valval### # Copyright (c) 2017, Ken Spencer # Copyright (c) 2017-2021, 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 hashlib import supybot.conf as conf import supybot.registry as registry import supybot.utils as utils from supybot.commands import * 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('Hashes') class Hashes(callbacks.Plugin): """Provides hash or encryption related commands""" @internationalizeDocstring def md5(self, irc, msg, args, text): """ Returns the md5 hash of a given string. """ irc.reply(hashlib.md5(text.encode('utf8')).hexdigest()) md5 = wrap(md5, ['text']) @internationalizeDocstring def sha(self, irc, msg, args, text): """ Returns the SHA1 hash of a given string. """ irc.reply(hashlib.sha1(text.encode('utf8')).hexdigest()) sha = wrap(sha, ['text']) @internationalizeDocstring def sha256(self, irc, msg, args, text): """ Returns a SHA256 hash of the given string. """ irc.reply(hashlib.sha256(text.encode('utf8')).hexdigest()) sha256 = wrap(sha256, ['text']) @internationalizeDocstring def sha512(self, irc, msg, args, text): """ Returns a SHA512 hash of the given string. """ irc.reply(hashlib.sha512(text.encode('utf8')).hexdigest()) sha512 = wrap(sha512, ['text']) @internationalizeDocstring def algorithms(self, irc, msg, args): """ Returns the list of available algorithms.""" irc.reply(utils.str.format("%L", hashlib.algorithms_available)) if hasattr(hashlib, 'algorithms_available'): algorithms = wrap(algorithms) @internationalizeDocstring def mkhash(self, irc, msg, args, algorithm, text): """ Returns TEXT after it has been hashed with ALGORITHM. See the 'algorithms' command in this plugin to return the algorithms available on this system.""" if algorithm not in hashlib.algorithms_available: irc.error("Algorithm not available.") else: irc.reply(hashlib.new(algorithm, text.encode('utf8')).hexdigest()) if hasattr(hashlib, 'algorithms_available'): mkhash = wrap(mkhash, ['something', 'text']) Class = Hashes ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Hashes/test.py0000644000175000017500000000530214535072470016345 0ustar00valval### # Copyright (c) 2017, Ken Spencer # Copyright (c) 2017-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import hashlib from supybot.test import * import supybot.utils as utils brown_fox = 'The quick brown fox jumped over the lazy dog' class HashesTestCase(PluginTestCase): plugins = ('Hashes',) def testHashes(self): self.assertResponse('md5 %s' % brown_fox, '08a008a01d498c404b0c30852b39d3b8') self.assertResponse('sha %s' % brown_fox, 'f6513640f3045e9768b239785625caa6a2588842') self.assertResponse('sha256 %s' % brown_fox, '7d38b5cd25a2baf85ad3bb5b9311383e671a8a142eb302b324d4a5fba8748c69') self.assertResponse('sha512 %s' % brown_fox, 'db25330cfa5d14eaadf11a6263371cfa0e70fcd7a63a433b91f2300ca25d45b66a7b50d2f6747995c8fa0ff365b28974792e7acd5624e1ddd0d66731f346f0e7') if hasattr(hashlib, 'algorithms_available'): def testMkhash(self): self.assertResponse('mkhash md5 %s' % brown_fox, '08a008a01d498c404b0c30852b39d3b8') self.assertError('mkhash NonExistant %s' % brown_fox) self.assertNotError('algorithms') else: print("Hashes: Skipping algorithms/mkhash tests as 'hashlib.algorithms_available' is not available.") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Herald/0000755000175000017500000000000014535072535015002 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/__init__.py0000644000175000017500000000456714535072470017125 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Greets users who join the channel with a recognized hostmask with a nice little greeting. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/config.py0000644000175000017500000000750314535072470016624 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Herald') 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('Herald', True) Herald = conf.registerPlugin('Herald') conf.registerChannelValue(Herald, 'heralding', registry.Boolean(True, _("""Determines whether messages will be sent to the channel when a recognized user joins; basically enables or disables the plugin."""))) conf.registerGlobalValue(Herald, 'requireCapability', registry.String('', _("""Determines what capability (if any) is required to add/change/remove the herald of another user."""))) conf.registerChannelValue(Herald, 'throttle', registry.PositiveInteger(600, _("""Determines the minimum number of seconds between heralds."""))) conf.registerChannelValue(Herald.throttle, 'afterPart', registry.NonNegativeInteger(0, _("""Determines the minimum number of seconds after parting that the bot will not herald the person when they rejoin."""))) conf.registerChannelValue(Herald.throttle, 'afterSplit', registry.NonNegativeInteger(60, _("""Determines the minimum number of seconds after a netsplit that the bot will not herald the users that split."""))) conf.registerChannelValue(Herald, 'default', registry.String('', _("""Sets the default herald to use. If a user has a personal herald specified, that will be used instead. If set to the empty string, the default herald will be disabled."""))) conf.registerChannelValue(Herald.default, 'notice', registry.Boolean(True, _("""Determines whether the default herald will be sent as a NOTICE instead of a PRIVMSG."""))) conf.registerChannelValue(Herald.default, 'public', registry.Boolean(False, _("""Determines whether the default herald will be sent publicly."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Herald/locales/0000755000175000017500000000000014535072535016424 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/locales/fi.po0000644000175000017500000001624514535072470017370 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Herald plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14:07+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:47 msgid "" "Determines whether messages will be sent to the\n" " channel when a recognized user joins; basically enables or disables the\n" " plugin." msgstr "" "Määrittää lähetetäänkö viestejä kanavalle, kun\n" " tunnistettu käyttäjä liittyy; perusteellisesti ottaa tai poistaa " "käytöstä\n" " tämän lisäosan." #: config.py:51 msgid "" "Determines what capability (if any) is required to\n" " add/change/remove the herald of another user." msgstr "" "Määrittää mikä valtuus (jos mikään) on vaadittu\n" " toisen käyttäjän airueen lisäämisessä/muuttamisessa/poistamisessa." #: config.py:54 msgid "" "Determines the minimum number of seconds\n" " between heralds." msgstr "" "Määrittää minimi määrän sekunteja\n" " airueiden välillä." #: config.py:57 msgid "" "Determines the minimum number of seconds\n" " after parting that the bot will not herald the person when they\n" " rejoin." msgstr "" "Määrittää minimi määrän sekunteja, jolloin\n" " henkilön poistumisen jälkeen botti ei toimi airueena, kun hän\n" " palaa." #: config.py:61 msgid "" "Determines the minimum number of seconds\n" " after a netsplit that the bot will not herald the users that split." msgstr "" "Määritt'' minimi määrän sekunteja netsplitin jälkeen\n" " jolloin botti ei toimi airueena käyttäjille, jotka splittaantuivat." #: config.py:64 msgid "" "Sets the default herald to use. If a user has a\n" " personal herald specified, that will be used instead. If set to the " "empty\n" " string, the default herald will be disabled." msgstr "" "Asettaa oletusairueen käytettäväksi. Jos käyttäjällä on oma\n" " henkilökohtainen airue määritettynä, sitä käytetään sen sijaan. Jos " "asetettu tyhjäksi\n" " merkkiketjuksi, oletus airue poistetaan käytöstä." #: config.py:68 msgid "" "Determines whether the default herald will be\n" " sent as a NOTICE instead of a PRIVMSG." msgstr "" "Määrittää lähetetäänkö oletus airue NOTICEna\n" " PRIVMSG:gen sijaan." #: config.py:71 msgid "" "Determines whether the default herald will be\n" " sent publicly." msgstr "" "Määrittää lähetetäänkö oletus airut \n" " julkisesti." #: plugin.py:59 msgid "" "This plugin allows you to set welcome messages (heralds) to people who\n" " are recognized by the bot when they join a channel." msgstr "" "Tämä plugini sallii tervetuloviestien (airueiden) asettamisen käyttäjille, " "jotka botti tunnistaa heidän liittyessään kanavalle." #: plugin.py:152 #, fuzzy msgid "" "[] [--remove|]\n" "\n" " If is given, sets the default herald to . A of " "\"\"\n" " will remove the default herald. If is not given, returns the\n" " current default herald. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] [--remove|]\n" "\n" " Jos on annettu, asettaa oletusairueen . " "\"\"\n" " poistaa oletus airueen. Jos ei anneta, palauttaa\n" " nykyisen oletusairueen. on vaadittu vain jos viestiä ei " "lähetetä kanavalla itsellään." #: plugin.py:171 msgid "I do not have a default herald set for %s." msgstr "Minulla ei ole oletus airuetta asetettuna %s:lle." #: plugin.py:179 msgid "" "[] []\n" "\n" " Returns the current herald message for (or the user\n" " is currently identified or recognized as). If " "\n" " is not given, defaults to the user giving the command. \n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Palauttaa nykyisen airue viestin (tai käyttäjälle\n" " on tunnistetty). Jos \n" " ei ole annettu, se on oletuksenena komennon antanut henkilö. " "\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:190 msgid "I have no herald for %s." msgstr "Minulla ei ole airuetta %s:lle." #: plugin.py:210 msgid "" "[] \n" "\n" " Sets the herald message for (or the user is\n" " currently identified or recognized as) to . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Asettaa airueen (tai käyttäjän, johon \n" " tällä hetkellä on tunnistautunut, tai tunnistettu) . " " on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:223 msgid "" "[] []\n" "\n" " Removes the herald message set for , or the user\n" " is currently identified or recognized as. If " "\n" " is not given, defaults to the user giving the command.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] []\n" "\n" " Poistaa airue viestin, joka on asetettu , tai\n" " joka on tällä hetkellä tunnistettu tai " "tunnistautunut. Jos \n" " ei ole annettu, se on oletuksena komennon antanut käyttäjä.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:236 msgid "I have no herald for that user." msgstr "Minulla ei ole airuetta tuolle käyttäjälle." #: plugin.py:241 msgid "" "[] [] \n" "\n" " Changes the herald message for , or the user " "is\n" " currently identified or recognized as, according to . If\n" " is not given, defaults to the calling user. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] \n" "\n" " Vaihtaa airueen, tai käyttäjän joka\n" " on tunnistautunut tai tunnistettu, . Jos\n" " ei ole annettu, se on oletuksena komennon antanut " "käyttäjä. on vaadittu vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/locales/fr.po0000644000175000017500000001506714535072470017402 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-07-05 00:11+0200\n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:47 msgid "" "Determines whether messages will be sent to the\n" " channel when a recognized user joins; basically enables or disables the\n" " plugin." msgstr "" "Détermine si les messages seront envoyés sur le canal lorsqu'un utilisateur " "reconnu arrive ; active ou désactive basiquement le plugin." #: config.py:51 msgid "" "Determines what capability (if any) is required to\n" " add/change/remove the herald of another user." msgstr "" "Détermine quelle capacité (s'il en est une) est requise pour ajouter/changer/" "supprimer le message d'annonce d'un autre utilisateur." #: config.py:54 msgid "" "Determines the minimum number of seconds\n" " between heralds." msgstr "Détermine le nombre minimum de secondes entre deux annonces." #: config.py:57 msgid "" "Determines the minimum number of seconds\n" " after parting that the bot will not herald the person when they\n" " rejoin." msgstr "" "Détermine le nombre minimum de secondes après qu'un utilisateur soit parti " "pour que le bot puisse l'annoncer à nouveau quand il revient." #: config.py:61 msgid "" "Determines the minimum number of seconds\n" " after a netsplit that the bot will not herald the users that split." msgstr "" "Détermine le nombre minimum de secondes après un netsplit durant lequel le " "bot n'annoncera pas les utilisateurs qui ont splitté." #: config.py:64 msgid "" "Sets the default herald to use. If a user has a\n" " personal herald specified, that will be used instead. If set to the " "empty\n" " string, the default herald will be disabled." msgstr "" "Définit le message d'annonce par défaut à utiliser. Si un utilisateur a un " "message d'annonce personnel spécifié, il peut être utilisé à la place. Si " "définit à une chaîne vide, le message d'annonce sera désactivé." #: config.py:68 msgid "" "Determines whether the default herald will be\n" " sent as a NOTICE instead of a PRIVMSG." msgstr "" "Détermine si le message d'annonce sera envoyé en NOTICE plutôt qu'en PRIVMSG." #: config.py:71 msgid "" "Determines whether the default herald will be\n" " sent publicly." msgstr "Détermine si le message d'annonce sera envoyé publiquement." #: plugin.py:59 msgid "" "This plugin allows you to set welcome messages (heralds) to people who\n" " are recognized by the bot when they join a channel." msgstr "" #: plugin.py:152 msgid "" "[] [--remove|]\n" "\n" " If is given, sets the default herald to . A of " "\"\"\n" " will remove the default herald. If is not given, returns the\n" " current default herald. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] [--remove|]\n" "\n" "Si le est donné, définit le message d'annonce par défaut pour être " "le . Un de la forme \"\" supprimera le message d'annonce " "par défaut. Si le n'est pas donné, retourne le message d'annonce " "par défaut actuel. n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:171 msgid "I do not have a default herald set for %s." msgstr "Je n'ai pas de message d'annonce par défaut pour %s." #: plugin.py:179 msgid "" "[] []\n" "\n" " Returns the current herald message for (or the user\n" " is currently identified or recognized as). If " "\n" " is not given, defaults to the user giving the command. \n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] ]\n" "\n" "Retourne le message d'annonce courant pour l' (ou l'utilisateur " "désigné par le ). Si l' n'est pas donné, cela vaut par " "défaut l'utilisateur donnant la commande. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:190 msgid "I have no herald for %s." msgstr "Je n'ai pas de message d'annonce pour %s." #: plugin.py:210 msgid "" "[] \n" "\n" " Sets the herald message for (or the user is\n" " currently identified or recognized as) to . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Définit le d'annonce de l' (ou la personne désignée " "par ). n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:223 msgid "" "[] []\n" "\n" " Removes the herald message set for , or the user\n" " is currently identified or recognized as. If " "\n" " is not given, defaults to the user giving the command.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] []\n" "\n" "Supprime le message d'annonce de l' (ou de l'utilisateur " "désigné par le ). n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:236 msgid "I have no herald for that user." msgstr "Je n'ai pas de message d'annonce pour cet utilisateur." #: plugin.py:241 msgid "" "[] [] \n" "\n" " Changes the herald message for , or the user " "is\n" " currently identified or recognized as, according to . If\n" " is not given, defaults to the calling user. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] []\n" "\n" "Change le message d'annonce de l' (ou de l'utilisateur désigné " "par le ), en fonction de l'. Si l' " "n'est pas donné, cea correspond par défaut de l'utilisateur appelant la " "commande. n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/locales/it.po0000644000175000017500000001472714535072470017411 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-07-05 00:06+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:47 msgid "" "Determines whether messages will be sent to the\n" " channel when a recognized user joins; basically enables or disables the\n" " plugin." msgstr "" "Determina se i messaggi verranno inviati nel canale in cui entra un utente\n" " riconosciuto; in pratica abilita o disabilita il plugin." #: config.py:51 msgid "" "Determines what capability (if any) is required to\n" " add/change/remove the herald of another user." msgstr "" "Determina quale capacità (eventuale) è richiesta per aggiungere, modificare\n" " e rimuovere l'annuncio di un altro utente." #: config.py:54 msgid "" "Determines the minimum number of seconds\n" " between heralds." msgstr "Determina il numero minimo di secondi tra un annuncio e l'altro." #: config.py:57 msgid "" "Determines the minimum number of seconds\n" " after parting that the bot will not herald the person when they\n" " rejoin." msgstr "" "Determina il numero minimo di secondi dall'uscita di un utente in cui\n" " il bot non invierà l'annuncio al suo rientro." #: config.py:61 msgid "" "Determines the minimum number of seconds\n" " after a netsplit that the bot will not herald the users that split." msgstr "" "Determina il numero minimo di secondi dopo un netsplit in cui il bot\n" " non invierà l'annuncio agli utenti coinvolti." #: config.py:64 msgid "" "Sets the default herald to use. If a user has a\n" " personal herald specified, that will be used instead. If set to the " "empty\n" " string, the default herald will be disabled." msgstr "" "Imposta l'annuncio predefinito da utilizzare. Se un utente ha un annuncio " "personalizzato,\n" " verrà usato quello. Se impostato ad una stringa vuota, il predefinito " "sarà disabilitato." #: config.py:68 msgid "" "Determines whether the default herald will be\n" " sent as a NOTICE instead of a PRIVMSG." msgstr "" "Determina se l'annuncio predefinito verrà inviato tramite NOTICE anziché " "PRIVMSG." #: config.py:71 msgid "" "Determines whether the default herald will be\n" " sent publicly." msgstr "Determina se l'annuncio predefinito verrà inviato pubblicamente." #: plugin.py:59 msgid "" "This plugin allows you to set welcome messages (heralds) to people who\n" " are recognized by the bot when they join a channel." msgstr "" #: plugin.py:152 msgid "" "[] [--remove|]\n" "\n" " If is given, sets the default herald to . A of " "\"\"\n" " will remove the default herald. If is not given, returns the\n" " current default herald. is only necessary if the message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] [--remove|]\n" "\n" " Se è fornito, imposta l'annuncio predefinito a " ";\n" " un nella forma \"\" rimuoverà il predefinito. Se " "\n" " non è specificato, restituisce l'attuale annuncio. è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:171 msgid "I do not have a default herald set for %s." msgstr "Non ho un annuncio predefinito per %s." #: plugin.py:179 msgid "" "[] []\n" "\n" " Returns the current herald message for (or the user\n" " is currently identified or recognized as). If " "\n" " is not given, defaults to the user giving the command. \n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Restituisce l'attuale annuncio per (o con " "cui\n" " è identificato al momento). Se non è specificato, passa a " "quello\n" " che ha dato il comando. è necessario solo se il messaggio " "non\n" " viene inviato nel canale stesso.\n" " " #: plugin.py:190 msgid "I have no herald for %s." msgstr "Non ho annunci per %s." #: plugin.py:210 msgid "" "[] \n" "\n" " Sets the herald message for (or the user is\n" " currently identified or recognized as) to . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Imposta l'annuncio per (o con cui è " "identificato\n" " al momento) a . è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso.\n" " " #: plugin.py:223 msgid "" "[] []\n" "\n" " Removes the herald message set for , or the user\n" " is currently identified or recognized as. If " "\n" " is not given, defaults to the user giving the command.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] []\n" "\n" " Rimuove l'annuncio per o con cui è " "identificato\n" " al momento. Se non è specificato, passa a quello che ha " "dato il comando.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:236 msgid "I have no herald for that user." msgstr "Non ho annunci per questo utente." #: plugin.py:241 msgid "" "[] [] \n" "\n" " Changes the herald message for , or the user " "is\n" " currently identified or recognized as, according to . If\n" " is not given, defaults to the calling user. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] \n" "\n" " Modifica l'annuncio per , o con cui è " "identificato\n" " al momento, in base a . Se non è specificato, passa " "a quello\n" " che ha dato il comando. è necessario solo se il messaggio " "non viene\n" " inviato nel canale stesso.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/plugin.py0000644000175000017500000002505214535072470016654 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 time import supybot.conf as conf import supybot.utils as utils import supybot.world as world import supybot.ircdb as ircdb 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.utils.structures import TimeoutQueue from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Herald') filename = conf.supybot.directories.data.dirize('Herald.db') class HeraldDB(plugins.ChannelUserDB): def serialize(self, v): return [v] def deserialize(self, channel, id, L): if len(L) != 1: raise ValueError return L[0] class Herald(callbacks.Plugin): """This plugin allows you to set welcome messages (heralds) to people who are recognized by the bot when they join a channel.""" def __init__(self, irc): self.__parent = super(Herald, self) self.__parent.__init__(irc) self.db = HeraldDB(filename) world.flushers.append(self.db.flush) self.lastParts = plugins.ChannelUserDictionary() splitTimeout = conf.supybot.plugins.Herald.throttle.afterSplit self.splitters = TimeoutQueue(splitTimeout) self.lastHerald = plugins.ChannelUserDictionary() def die(self): if self.db.flush in world.flushers: world.flushers.remove(self.db.flush) self.db.close() self.__parent.die() def doQuit(self, irc, msg): # We want to observe netsplits and keep from heralding users rejoining # after one. if ircmsgs.isSplit(msg): self.splitters.enqueue(msg.nick) try: id = ircdb.users.getUserId(msg.prefix) self.splitters.enqueue(id) except KeyError: pass def doJoin(self, irc, msg): if ircutils.strEqual(irc.nick, msg.nick): return # It's us. channel = msg.args[0] irc = callbacks.SimpleProxy(irc, msg) if self.registryValue('heralding', channel, irc.network): try: id = ircdb.users.getUserId(msg.prefix) except KeyError: id = msg.nick if id in self.splitters: self.log.debug('Not heralding id #%s, recent split.', id) return now = time.monotonic() throttle = self.registryValue('throttle', channel, irc.network) if now - self.lastHerald.get((channel, id), 0) > throttle: if (channel, id) in self.lastParts: i = self.registryValue('throttle.afterPart', channel, irc.network) if now - self.lastParts[channel, id] < i: return try: herald = self.db[channel, id] except KeyError: herald = self.registryValue('default', channel, irc.network) if herald: herald = ircutils.standardSubstitute(irc, msg, herald) msgmaker = ircmsgs.privmsg if self.registryValue('default.notice', channel, irc.network): msgmaker = ircmsgs.notice target = msg.nick if self.registryValue('default.public', channel, irc.network): target = channel self.lastHerald[channel, id] = now irc.queueMsg(msgmaker(target, herald)) return if herald: self.lastHerald[channel, id] = now herald = ircutils.standardSubstitute(irc, msg, herald) irc.reply(herald, prefixNick=False) def doPart(self, irc, msg): try: id = self._getId(irc, msg.prefix) except KeyError: id = msg.nick self.lastParts[msg.args[0], id] = time.monotonic() def _getId(self, irc, userNickHostmask): try: id = ircdb.users.getUserId(userNickHostmask) except KeyError: if not ircutils.isUserHostmask(userNickHostmask): hostmask = irc.state.nickToHostmask(userNickHostmask) id = ircdb.users.getUserId(hostmask) else: raise KeyError return id @internationalizeDocstring def default(self, irc, msg, args, channel, optlist, text): """[] [--remove|] If is given, sets the default herald to . A of "" will remove the default herald. If is not given, returns the current default herald. is only necessary if the message isn't sent in the channel itself. """ if optlist and text: raise callbacks.ArgumentError for (option, foo) in optlist: if option == 'remove': self.setRegistryValue('default', '', channel) irc.replySuccess() return if text: self.setRegistryValue('default', text, channel) irc.replySuccess() else: resp = self.registryValue('default', channel, irc.network) or \ _('I do not have a default herald set for %s.') % channel irc.reply(resp) default = wrap(default, ['channel', getopts({'remove': ''}), additional('text')]) @internationalizeDocstring def get(self, irc, msg, args, channel, user): """[] [] Returns the current herald message for (or the user is currently identified or recognized as). If is not given, defaults to the user giving the command. is only necessary if the message isn't sent in the channel itself. """ try: herald = self.db[channel, user.id] irc.reply(herald) except KeyError: irc.error(_('I have no herald for %s.') % user.name) get = wrap(get, ['channel', first('otherUser', 'user')]) def _preCheck(self, irc, msg, user): capability = self.registryValue('requireCapability') if capability: try: u = ircdb.users.getUser(msg.prefix) except KeyError: irc.errorNotRegistered(Raise=True) else: if u != user: if not ircdb.checkCapability(msg.prefix, capability): irc.errorNoCapability(capability, Raise=True) # I chose not to make optional in this command because # if it's not a valid username (e.g., if the user tyops and misspells a # username), it may be nice not to clobber the user's herald. @internationalizeDocstring def add(self, irc, msg, args, channel, user, herald): """[] Sets the herald message for (or the user is currently identified or recognized as) to . is only necessary if the message isn't sent in the channel itself. """ self._preCheck(irc, msg, user) self.db[channel, user.id] = herald irc.replySuccess() add = wrap(add, ['channel', 'otherUser', 'text']) @internationalizeDocstring def remove(self, irc, msg, args, channel, user): """[] [] Removes the herald message set for , or the user is currently identified or recognized as. If is not given, defaults to the user giving the command. is only necessary if the message isn't sent in the channel itself. """ self._preCheck(irc, msg, user) try: del self.db[channel, user.id] irc.replySuccess() except KeyError: irc.error(_('I have no herald for that user.')) remove = wrap(remove, ['channel', first('otherUser', 'user')]) @internationalizeDocstring def change(self, irc, msg, args, channel, user, changer): """[] [] Changes the herald message for , or the user is currently identified or recognized as, according to . If is not given, defaults to the calling user. is only necessary if the message isn't sent in the channel itself. """ self._preCheck(irc, msg, user) s = self.db[channel, user.id] newS = changer(s) self.db[channel, user.id] = newS irc.replySuccess() change = wrap(change, ['channel', first('otherUser', 'user'), 'regexpReplacer']) Class = Herald # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Herald/test.py0000644000175000017500000000337514535072470016341 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class HeraldTestCase(PluginTestCase): plugins = ('Herald',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Internet/0000755000175000017500000000000014535072535015373 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/__init__.py0000644000175000017500000000516414535072470017510 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This plugin provides commands to transform domains into IP addresses and IP addresses to domains. It can also search WHOIS information and return hexips. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {supybot.authors.jamessan: ['whois']} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/config.py0000644000175000017500000000475514535072470017223 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Internet') 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('Internet', True) Internet = conf.registerPlugin('Internet') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Internet, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Internet/locales/0000755000175000017500000000000014535072535017015 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/locales/fi.po0000644000175000017500000000441114535072470017751 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Internet plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:25+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" #: plugin.py:45 #, fuzzy msgid "" "Provides commands to query DNS, search WHOIS databases,\n" " and convert IPs to hex." msgstr "" "Tarjoaa komennot DNS-kyselyihin, hakemaan WHOIS-tietokannoista ja " "muuttamaan\n" " IP-osoitteet hexoiksi." #: plugin.py:50 msgid "" "\n" "\n" " Returns the ip of or the reverse DNS hostname of .\n" " " msgstr "" "\n" "\n" " Palauttaa ip:een tai käänteisen isäntänimen.\n" " " #: plugin.py:57 plugin.py:72 msgid "Host not found." msgstr "Isäntää ei löytynyt." #: plugin.py:87 msgid "" "\n" "\n" " Returns WHOIS information on the registration of .\n" " " msgstr "" "\n" "\n" " Palauttaa WHOIS tiedot rekisteröimisestä.\n" " " #: plugin.py:149 msgid "updated %s" msgstr "päivitetty %s" #: plugin.py:152 msgid "registered %s" msgstr "rekisteröity %s" #: plugin.py:155 msgid "expires %s" msgstr "vanhenee %s" #: plugin.py:175 msgid " " msgstr " " #: plugin.py:177 msgid " " msgstr " " #: plugin.py:184 msgid "%s%s is %L." msgstr "%s%s on %L." #: plugin.py:187 msgid "I couldn't find such a domain." msgstr "En voi löytää sellaista verkkotunnusta." #: plugin.py:192 msgid "" "\n" "\n" " Returns the hexadecimal IP for that IP.\n" " " msgstr "" "\n" "\n" " Palauttaa IP:een heksadesimaalisen IP:een IP:lle.\n" " " #~ msgid "domain" #~ msgstr "verkkotunnus" #~ msgid "Add the help for \"help Internet\" here." #~ msgstr "Lisää ohje \"help Internet:ille\" tähän." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/locales/fr.po0000644000175000017500000000337714535072470017774 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:45 msgid "" "Provides commands to query DNS, search WHOIS databases,\n" " and convert IPs to hex." msgstr "" #: plugin.py:50 msgid "" "\n" "\n" " Returns the ip of or the reverse DNS hostname of .\n" " " msgstr "" "\n" "\n" "Retourne l'ip de l', ou le reverse DNS de l'" #: plugin.py:57 plugin.py:72 msgid "Host not found." msgstr "Hôte non trouvé." #: plugin.py:87 msgid "" "\n" "\n" " Returns WHOIS information on the registration of .\n" " " msgstr "" "\n" "\n" "Retourne les informations du WHOIS sur le ." #: plugin.py:149 msgid "updated %s" msgstr "mis à jour le %s" #: plugin.py:152 msgid "registered %s" msgstr "enregistré le %s" #: plugin.py:155 msgid "expires %s" msgstr "expire le %s" #: plugin.py:175 msgid " " msgstr " " #: plugin.py:177 msgid " " msgstr " " #: plugin.py:184 msgid "%s%s is %L." msgstr "%s%s est %L" #: plugin.py:187 msgid "I couldn't find such a domain." msgstr "Je ne peux trouver ce domaine." #: plugin.py:192 msgid "" "\n" "\n" " Returns the hexadecimal IP for that IP.\n" " " msgstr "" "\n" "\n" "Retourne l'IP hexadécimale pour cette IP." #~ msgid "domain" #~ msgstr "domaine" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/locales/it.po0000644000175000017500000000335114535072470017771 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 14:14+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 msgid "" "Provides commands to query DNS, search WHOIS databases,\n" " and convert IPs to hex." msgstr "" #: plugin.py:50 msgid "" "\n" "\n" " Returns the ip of or the reverse DNS hostname of .\n" " " msgstr "" "\n" "\n" " Restituisce l'ip di o il DNS inverso di .\n" " " #: plugin.py:57 plugin.py:72 msgid "Host not found." msgstr "Host non trovato." #: plugin.py:87 msgid "" "\n" "\n" " Returns WHOIS information on the registration of .\n" " " msgstr "" "\n" "\n" " Restituisce le informazioni WHOIS sulla registrazione di .\n" " " #: plugin.py:149 msgid "updated %s" msgstr "aggiornato il %s" #: plugin.py:152 msgid "registered %s" msgstr "registrato il %s" #: plugin.py:155 msgid "expires %s" msgstr "scade il %s" #: plugin.py:175 msgid " " msgstr " " #: plugin.py:177 msgid " " msgstr " " #: plugin.py:184 msgid "%s%s is %L." msgstr "%s%s è %L." #: plugin.py:187 msgid "I couldn't find such a domain." msgstr "Non riesco a trovare un dominio." #: plugin.py:192 msgid "" "\n" "\n" " Returns the hexadecimal IP for that IP.\n" " " msgstr "" "\n" "\n" " Restituisce l'IP esadecimale per questo IP.\n" " " #~ msgid "domain" #~ msgstr "dominio" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/plugin.py0000644000175000017500000002126714535072470017251 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2011, James McCoy # Copyright (c) 2010-2021, 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 time import socket import telnetlib import supybot.conf as conf import supybot.utils as utils from supybot.commands import * from supybot.utils.iter import any import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Internet') class Internet(callbacks.Plugin): """Provides commands to query DNS, search WHOIS databases, and convert IPs to hex.""" threaded = True @internationalizeDocstring def dns(self, irc, msg, args, host): """ Returns the ip of or the reverse DNS hostname of . """ if utils.net.isIP(host): hostname = socket.getfqdn(host) if hostname == host: irc.reply(_('Host not found.')) else: irc.reply(hostname) else: try: ips = socket.getaddrinfo(host, None) ips = map(lambda x:x[4][0], ips) ordered_unique_ips = [] unique_ips = set() for ip in ips: if ip not in unique_ips: ordered_unique_ips.append(ip) unique_ips.add(ip) irc.reply(format('%L', ordered_unique_ips)) except socket.error: irc.reply(_('Host not found.')) dns = wrap(dns, ['something']) _domain = ['Domain Name', 'Server Name', 'domain'] _netrange = ['NetRange', 'inetnum'] _registrar = ['Sponsoring Registrar', 'Registrar', 'source'] _netname = ['NetName', 'inetname'] _updated = ['Last Updated On', 'Domain Last Updated Date', 'Updated Date', 'Last Modified', 'changed', 'last-modified'] _created = ['Created On', 'Domain Registration Date', 'Creation Date', 'created', 'RegDate'] _expires = ['Expiration Date', 'Domain Expiration Date'] _status = ['Status', 'Domain Status', 'status'] @internationalizeDocstring def whois(self, irc, msg, args, domain): """ Returns WHOIS information on the registration of . """ if utils.net.isIP(domain): whois_server = 'whois.arin.net' usertld = 'ipaddress' elif '.' in domain: usertld = domain.split('.')[-1] whois_server = '%s.whois-servers.net' % usertld else: usertld = None whois_server = 'whois.iana.org' try: sock = utils.net.getSocket(whois_server, vhost=conf.supybot.protocols.irc.vhost(), vhostv6=conf.supybot.protocols.irc.vhostv6(), ) sock.connect((whois_server, 43)) except socket.error as e: irc.error(str(e)) return sock.settimeout(5) if usertld == 'com': sock.send(b'=') elif usertld == 'ipaddress': sock.send(b'n + ') sock.send(domain.encode('ascii')) sock.send(b'\r\n') s = b'' end_time = time.time() + 5 try: while end_time>time.time(): time.sleep(0.1) s += sock.recv(4096) except socket.error: pass sock.close() server = netrange = netname = registrar = updated = created = expires = status = '' for line in s.splitlines(): line = line.decode('utf8').strip() if not line or ':' not in line: continue if not server and any(line.startswith, self._domain): server = ':'.join(line.split(':')[1:]).strip().lower() # Let's add this check so that we don't respond with info for # a different domain. E.g., doing a whois for microsoft.com # and replying with the info for microsoft.com.wanadoodoo.com if server != domain: server = '' continue if not netrange and any(line.startswith, self._netrange): netrange = ':'.join(line.split(':')[1:]).strip() if not server and not netrange: continue if not registrar and any(line.startswith, self._registrar): registrar = ':'.join(line.split(':')[1:]).strip() elif not netname and any(line.startswith, self._netname): netname = ':'.join(line.split(':')[1:]).strip() elif not updated and any(line.startswith, self._updated): s = ':'.join(line.split(':')[1:]).strip() updated = _('updated %s') % s elif not created and any(line.startswith, self._created): s = ':'.join(line.split(':')[1:]).strip() created = _('registered %s') % s elif not expires and any(line.startswith, self._expires): s = ':'.join(line.split(':')[1:]).strip() expires = _('expires %s') % s elif not status and any(line.startswith, self._status): status = ':'.join(line.split(':')[1:]).strip().lower() if not status: status = 'unknown' try: t = telnetlib.Telnet('whois.pir.org', 43) except socket.error as e: irc.error(str(e)) return t.write(b'registrar ') t.write(registrar.split('(')[0].strip().encode('ascii')) t.write(b'\n') s = t.read_all() url = '' for line in s.splitlines(): line = line.decode('ascii').strip() if not line: continue if line.startswith('Email'): url = _(' ') % line.split('@')[-1] elif line.startswith('Registrar Organization:'): url = _(' ') % line.split(':')[1].strip() elif line == 'Not a valid ID pattern': url = '' if (server or netrange) and status: entity = server or 'Net range %s%s' % \ (netrange, ' (%s)' % netname if netname else '') info = filter(None, [status, created, updated, expires]) s = format(_('%s%s is %L.'), entity, url, info) irc.reply(s) else: irc.error(_('I couldn\'t find such a domain.')) whois = wrap(whois, ['lowered']) @internationalizeDocstring def hexip(self, irc, msg, args, ip): """ Returns the hexadecimal IP for that IP. """ ret = "" if utils.net.isIPV4(ip): quads = ip.split('.') for quad in quads: i = int(quad) ret += '%02X' % i else: octets = ip.split(':') for octet in octets: if octet: i = int(octet, 16) ret += '%04X' % i else: missing = (8 - len(octets)) * 4 ret += '0' * missing irc.reply(ret) hexip = wrap(hexip, ['ip']) Internet = internationalizeDocstring(Internet) Class = Internet # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Internet/test.py0000644000175000017500000000456414535072470016733 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, 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 * class InternetTestCase(PluginTestCase): plugins = ('Internet',) if network: def testDns(self): self.assertNotError('dns slashdot.org') self.assertResponse('dns does.not.exist.', 'Host not found.') def testWhois(self): self.assertNotError('internet whois ohio-state.edu') self.assertNotError('internet whois microsoft.com') self.assertNotError('internet whois inria.fr') self.assertNotError('internet whois slime.com.au') self.assertNotError('internet whois 8.8.8.8') self.assertNotError('internet whois net') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Karma/0000755000175000017500000000000014535072535014636 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/__init__.py0000644000175000017500000000475414535072470016757 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Plugin for keeping track of Karma for users and things in a channel. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/config.py0000644000175000017500000000753114535072470016461 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Karma') 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('Karma', True) Karma = conf.registerPlugin('Karma') conf.registerChannelValue(Karma, 'simpleOutput', registry.Boolean(False, _("""Determines whether the bot will output shorter versions of the karma output when requesting a single thing's karma."""))) conf.registerChannelValue(Karma, 'incrementChars', registry.SpaceSeparatedListOfStrings(['++'], _("""A space separated list of characters to increase karma."""))) conf.registerChannelValue(Karma, 'decrementChars', registry.SpaceSeparatedListOfStrings(['--'], _("""A space separated list of characters to decrease karma."""))) conf.registerChannelValue(Karma, 'response', registry.Boolean(False, _("""Determines whether the bot will reply with a success message when something's karma is increased or decreased."""))) conf.registerChannelValue(Karma, 'rankingDisplay', registry.Integer(3, _("""Determines how many highest/lowest karma things are shown when karma is called with no arguments."""))) conf.registerChannelValue(Karma, 'mostDisplay', registry.Integer(25, _("""Determines how many karma things are shown when the most command is called."""))) conf.registerChannelValue(Karma, 'allowSelfRating', registry.Boolean(False, _("""Determines whether users can adjust the karma of their nick."""))) conf.registerChannelValue(Karma, 'allowUnaddressedKarma', registry.Boolean(True, _("""Determines whether the bot will increase/decrease karma without being addressed."""))) conf.registerChannelValue(Karma, 'onlyNicks', registry.Boolean(False, _("""Determines whether the bot will only increase/decrease karma for nicks in the current channel."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Karma/locales/0000755000175000017500000000000014535072535016260 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/locales/fi.po0000644000175000017500000001723514535072470017224 0ustar00valval# Karma plugin in Limnoria. # Copyright (C) 2011-2014 Limnoria # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: Karma plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:50+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 "" "Determines whether the bot will output shorter\n" " versions of the karma output when requesting a single thing's karma." msgstr "" "Määrittää tulostaako botti lyhyemmän version\n" " karma ulostulosta, kun pyydetään yhden asian karmaa." #: config.py:50 msgid "" "A space separated list of\n" " characters to increase karma." msgstr "Välilyönneillä eroitettu lista merkeistä, jotka nostavat karmaa." #: config.py:53 msgid "" "A space separated list of\n" " characters to decrease karma." msgstr "Välilyöneillä eroitettu lista merkeistä, jotka laskevat karmaa." #: config.py:56 msgid "" "Determines whether the bot will reply with a\n" " success message when something's karma is increased or decreased." msgstr "" "Määrittää vastaako botti onnistumisviestillä, kun\n" " jonkin karmaa on nostettu tai laskettu." #: config.py:59 msgid "" "Determines how many highest/lowest karma things\n" " are shown when karma is called with no arguments." msgstr "" "Määrittää kuinka monta korkeinta/matalinta karma asiaa\n" " näytetään, kun karma pyydetään ilman parametriä." #: config.py:62 msgid "" "Determines how many karma things are shown when\n" " the most command is called." msgstr "" "Määrittää kuinka monta karma asiaa näytetään, kun\n" " kun \"most\" komento on pyydetty.'" #: config.py:65 msgid "" "Determines whether users can adjust the karma\n" " of their nick." msgstr "" "Määrittää pystyvätkö käyttäjän määrittämään\n" " nimimerkkinsä karman." #: config.py:68 msgid "" "Determines whether the bot will\n" " increase/decrease karma without being addressed." msgstr "" "Määrittää nostaako/vähentääkö botti karmaa\n" " ilman, että sille tarkoitetaan viestejä." #: config.py:71 #, fuzzy msgid "" "Determines whether the bot will\n" " only increase/decrease karma for nicks in the current channel." msgstr "" "Määrittää nostaako/vähentääkö botti karmaa\n" " ilman, että sille tarkoitetaan viestejä." #: plugin.py:229 msgid "" "\n" " Provides a simple tracker for setting Karma (thing++, thing--).\n" " If ``config plugins.karma.allowUnaddressedKarma`` is set to ``True``\n" " (default since 2014.05.07), saying `boats++` will give 1 karma\n" " to ``boats``, and ``ships--`` will subtract 1 karma from ``ships``.\n" "\n" " However, if you use this in a sentence, like\n" " ``That deserves a ++. Kevin++``, 1 karma will be added to\n" " ``That deserves a ++. Kevin``, so you should only add or subtract karma\n" " in a line that doesn't have anything else in it.\n" " Alternatively, you can restrict karma tracking to nicks in the current\n" " channel by setting `config plugins.Karma.onlyNicks` to ``True``.\n" "\n" " If ``config plugins.karma.allowUnaddressedKarma` is set to `False``,\n" " you must address the bot with nick or prefix to add or subtract karma.\n" " " msgstr "" #: plugin.py:263 msgid "%(thing)s's karma is now %(karma)i" msgstr "%(thing)in karma on nyt %(karma)i" #: plugin.py:283 plugin.py:296 msgid "You're not allowed to adjust your own karma." msgstr "Sinä et saa määrittää omaa karmaasi." #: plugin.py:323 msgid "" "[] [ ...]\n" "\n" " Returns the karma of . If is not given, returns the " "top\n" " N karmas, where N is determined by the config variable\n" " supybot.plugins.Karma.rankingDisplay. If one is given, " "returns\n" " the details of its karma; if more than one is given, " "returns\n" " the total karma of each of the things. is only necessary\n" " if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Palauttaa karman. Jos ei ole annettu, palauttaa " "TOP\n" " N karmat, missä N on asetusarvon\n" " supybot.plugins.Karma.rankingDisplay määrittämä. Jos yksi on " "annettu, palauttaa\n" " tiedot sen karmasta; jos useampi kuin yksi on annettu, " "tarkistaa jokaisen asian\n" " yhteiskarman. on vaadittu vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:336 msgid "%s has neutral karma." msgstr "%s:llä on neutraali karma." #: plugin.py:343 msgid "" "Karma for %q has been increased %n and decreased %n for a total karma of %s." msgstr "Karma %q:lle on noussut %n ja laskenut %n yhteiskarmalle %s." #: plugin.py:345 plugin.py:346 msgid "time" msgstr "aika" #: plugin.py:359 msgid "I didn't know the karma for any of those things." msgstr "Minä en tiennyt yhtäkään noiden asioiden karmoista." #: plugin.py:368 plugin.py:398 msgid "I have no karma for this channel." msgstr "Minulla ei ole karmaa tälle kanavalle." #: plugin.py:373 msgid " You (%s) are ranked %i out of %i." msgstr "Sinä olet rankingissa (%s) %i %i:stä." #: plugin.py:377 msgid "Highest karma: %L. Lowest karma: %L.%s" msgstr "Korkein karma: %L. Alhaisin karma: %L.%s" #: plugin.py:385 msgid "" "[] {increased,decreased,active}\n" "\n" " Returns the most increased, the most decreased, or the most active\n" " (the sum of increased and decreased) karma things. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {increased,decreased,active}\n" "\n" " Palauttaa eniten nousseen (\"increased\"), eniten laskeneen " "(\"decreased\"), tai aktiivisimman (\"active\")\n" " (nousseiden ja laskeneiden) karma asiat. on vaadittu vain, " "jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:404 msgid "" "[] []\n" "\n" " Resets the karma of to 0. If is not given, resets\n" " everything.\n" " " msgstr "" "[] \n" "\n" " Asettaa karman arvoksi 0. Jos ei ole annettu, asettaa " "kaiken\n" " arvoksi 0." #: plugin.py:415 msgid "" "[] \n" "\n" " Dumps the Karma database for to in the bot's\n" " data directory. is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Tallentaa botin\n" " \"data\" hakemistoon. on vaadittu vain, jos viestiä ei " "lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:428 msgid "" "[] \n" "\n" " Loads the Karma database for from in the bot's\n" " data directory. is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Lataa Karma tietokannan botin\n" " \"data\" hakemistosta. on vaadittu vain, jos viestiä ei " "lähetetä\n" " kanavalla itsellään.\n" " " #~ msgid "Provides a simple tracker for setting Karma (thing++, thing--)." #~ msgstr "Tarjoaa yksinkertaisen Karman seuraajan (jokin++, jokin--)." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/locales/fr.po0000644000175000017500000001563614535072470017240 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:47 msgid "" "Determines whether the bot will output shorter\n" " versions of the karma output when requesting a single thing's karma." msgstr "" "Détermine si le bot donnera comme sortie une version plus courte du karma " "lorsqu'on lui demandera un seul karma." #: config.py:50 msgid "" "A space separated list of\n" " characters to increase karma." msgstr "" #: config.py:53 msgid "" "A space separated list of\n" " characters to decrease karma." msgstr "" #: config.py:56 msgid "" "Determines whether the bot will reply with a\n" " success message when something's karma is increased or decreased." msgstr "" "Détermine si le bot répondra avec un message de succès si un karma est " "augmenté ou diminué." #: config.py:59 msgid "" "Determines how many highest/lowest karma things\n" " are shown when karma is called with no arguments." msgstr "" "Détermine combien de plus haut/bas karmas sont affichés lorsque karma est " "appelé sans argument." #: config.py:62 msgid "" "Determines how many karma things are shown when\n" " the most command is called." msgstr "" "Détermine combien de karmas sont affichés lorsque la commande « most » est " "appelée." #: config.py:65 msgid "" "Determines whether users can adjust the karma\n" " of their nick." msgstr "Détermine si les utilisateurs peuvent ajuster le karma de leur nick." #: config.py:68 msgid "" "Determines whether the bot will\n" " increase/decrease karma without being addressed." msgstr "" "Détermine si le bot augmentera/diminuera le karma sans que l'on s'adresse à " "lui." #: config.py:71 #, fuzzy msgid "" "Determines whether the bot will\n" " only increase/decrease karma for nicks in the current channel." msgstr "" "Détermine si le bot augmentera/diminuera le karma sans que l'on s'adresse à " "lui." #: plugin.py:229 msgid "" "\n" " Provides a simple tracker for setting Karma (thing++, thing--).\n" " If ``config plugins.karma.allowUnaddressedKarma`` is set to ``True``\n" " (default since 2014.05.07), saying `boats++` will give 1 karma\n" " to ``boats``, and ``ships--`` will subtract 1 karma from ``ships``.\n" "\n" " However, if you use this in a sentence, like\n" " ``That deserves a ++. Kevin++``, 1 karma will be added to\n" " ``That deserves a ++. Kevin``, so you should only add or subtract karma\n" " in a line that doesn't have anything else in it.\n" " Alternatively, you can restrict karma tracking to nicks in the current\n" " channel by setting `config plugins.Karma.onlyNicks` to ``True``.\n" "\n" " If ``config plugins.karma.allowUnaddressedKarma` is set to `False``,\n" " you must address the bot with nick or prefix to add or subtract karma.\n" " " msgstr "" #: plugin.py:263 msgid "%(thing)s's karma is now %(karma)i" msgstr "Le karma de %(thing)s est maintenant %(karma)i" #: plugin.py:283 plugin.py:296 msgid "You're not allowed to adjust your own karma." msgstr "Vous n'êtes pas autorisé à modifier votre propre karma." #: plugin.py:323 msgid "" "[] [ ...]\n" "\n" " Returns the karma of . If is not given, returns the " "top\n" " N karmas, where N is determined by the config variable\n" " supybot.plugins.Karma.rankingDisplay. If one is given, " "returns\n" " the details of its karma; if more than one is given, " "returns\n" " the total karma of each of the things. is only necessary\n" " if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Retourne le karma de l'. Si l' n'est pas donné, retourne les " "trois premiers et derniers karmas. Si une est donnée, retourne les " "détails de son karma ; si plus d'une est donnée, retourne le karma " "total de chacune de ces choses. Le n'est nécessaire que si la " "commande n'est pas envoyée sur le canal lui-même." #: plugin.py:336 msgid "%s has neutral karma." msgstr "%s a un karma neutre." #: plugin.py:343 msgid "" "Karma for %q has been increased %n and decreased %n for a total karma of %s." msgstr "" "Le karma de %q a été augmenté %n fois et diminué %n fois, pour un karma " "total de %s." #: plugin.py:345 plugin.py:346 msgid "time" msgstr "" #: plugin.py:359 msgid "I didn't know the karma for any of those things." msgstr "Je ne connais le karma d'aucune de ces choses." #: plugin.py:368 plugin.py:398 msgid "I have no karma for this channel." msgstr "Je n'ai pas de karma pour ce canal." #: plugin.py:373 msgid " You (%s) are ranked %i out of %i." msgstr " Vous (%s) êtes #%i sur %i" #: plugin.py:377 msgid "Highest karma: %L. Lowest karma: %L.%s" msgstr "Plus haut karma : %L. Plus bas karma : %L.%s" #: plugin.py:385 msgid "" "[] {increased,decreased,active}\n" "\n" " Returns the most increased, the most decreased, or the most active\n" " (the sum of increased and decreased) karma things. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {increased,decreased,active}\n" "\n" "Retourne le plus augmenté (increased), le plus descendu (decreased), ou le " "plus actif (la somme des montées et descentes) des karmas. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:404 #, fuzzy msgid "" "[] []\n" "\n" " Resets the karma of to 0. If is not given, resets\n" " everything.\n" " " msgstr "" "[] \n" "\n" "Redéfinit le karma de à 0." #: plugin.py:415 msgid "" "[] \n" "\n" " Dumps the Karma database for to in the bot's\n" " data directory. is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Exporte la base de données des Karma du dans le " "dans le répertoire de données du bot. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:428 msgid "" "[] \n" "\n" " Loads the Karma database for from in the bot's\n" " data directory. is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Charge la base de données des Karma du du dans le " "répertoire de données du bot. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/locales/it.po0000644000175000017500000001574714535072470017250 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-28 10: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:47 msgid "" "Determines whether the bot will output shorter\n" " versions of the karma output when requesting a single thing's karma." msgstr "" "Determina se il bot mostrerà una versione più corta del karma\n" " quando viene richiesto per un singolo oggetto." #: config.py:50 msgid "" "A space separated list of\n" " characters to increase karma." msgstr "" #: config.py:53 msgid "" "A space separated list of\n" " characters to decrease karma." msgstr "" #: config.py:56 msgid "" "Determines whether the bot will reply with a\n" " success message when something's karma is increased or decreased." msgstr "" "Determina se il bot risponderà con un messaggio di successo quando\n" " viene aumentato o diminuito il karma di qualcosa." #: config.py:59 msgid "" "Determines how many highest/lowest karma things\n" " are shown when karma is called with no arguments." msgstr "" "Determina quanti karma più/meno vengono mostrati quando richiamato senza " "argomenti." #: config.py:62 msgid "" "Determines how many karma things are shown when\n" " the most command is called." msgstr "" "Determina quanti karma vengono mostrati richiamando il comando \"most\"." #: config.py:65 msgid "" "Determines whether users can adjust the karma\n" " of their nick." msgstr "Determina se gli utenti possano modificare il karma del loro nick." #: config.py:68 msgid "" "Determines whether the bot will\n" " increase/decrease karma without being addressed." msgstr "" "Determina se il bot aumenterà o diminuirà il karma senza essere richiamato." #: config.py:71 #, fuzzy msgid "" "Determines whether the bot will\n" " only increase/decrease karma for nicks in the current channel." msgstr "" "Determina se il bot aumenterà o diminuirà il karma senza essere richiamato." #: plugin.py:229 msgid "" "\n" " Provides a simple tracker for setting Karma (thing++, thing--).\n" " If ``config plugins.karma.allowUnaddressedKarma`` is set to ``True``\n" " (default since 2014.05.07), saying `boats++` will give 1 karma\n" " to ``boats``, and ``ships--`` will subtract 1 karma from ``ships``.\n" "\n" " However, if you use this in a sentence, like\n" " ``That deserves a ++. Kevin++``, 1 karma will be added to\n" " ``That deserves a ++. Kevin``, so you should only add or subtract karma\n" " in a line that doesn't have anything else in it.\n" " Alternatively, you can restrict karma tracking to nicks in the current\n" " channel by setting `config plugins.Karma.onlyNicks` to ``True``.\n" "\n" " If ``config plugins.karma.allowUnaddressedKarma` is set to `False``,\n" " you must address the bot with nick or prefix to add or subtract karma.\n" " " msgstr "" #: plugin.py:263 msgid "%(thing)s's karma is now %(karma)i" msgstr "" #: plugin.py:283 plugin.py:296 msgid "You're not allowed to adjust your own karma." msgstr "Non ti è permesso di modificare il tuo karma." #: plugin.py:323 msgid "" "[] [ ...]\n" "\n" " Returns the karma of . If is not given, returns the " "top\n" " N karmas, where N is determined by the config variable\n" " supybot.plugins.Karma.rankingDisplay. If one is given, " "returns\n" " the details of its karma; if more than one is given, " "returns\n" " the total karma of each of the things. is only necessary\n" " if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Riporta il karma di . Se non è fornito, " "restituisce i primi\n" " N karma, dove N è determinato dalla variabile supybot.plugins.Karma." "rankingDisplay.\n" " Se viene specificato un , riporta i dettagli del suo karma; " "se ne vengono \n" " indicati più di uno, riporta il numero totale di karma di ciascuno " "degli oggetti.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:336 msgid "%s has neutral karma." msgstr "%s ha un karma neutro." #: plugin.py:343 msgid "" "Karma for %q has been increased %n and decreased %n for a total karma of %s." msgstr "" "Il karma per %q è stato aumentato di %n e diminuito di %n per un totale di " "%s." #: plugin.py:345 plugin.py:346 msgid "time" msgstr "volta" #: plugin.py:359 msgid "I didn't know the karma for any of those things." msgstr "Non conosco il karma di nessuno di questi oggetti." #: plugin.py:368 plugin.py:398 msgid "I have no karma for this channel." msgstr "Non ho karma per questo canale." #: plugin.py:373 msgid " You (%s) are ranked %i out of %i." msgstr " %s, sei valutato %i su %i." #: plugin.py:377 msgid "Highest karma: %L. Lowest karma: %L.%s" msgstr "Karma più alto: %L. Karma più basso: %L.%s" #: plugin.py:385 msgid "" "[] {increased,decreased,active}\n" "\n" " Returns the most increased, the most decreased, or the most active\n" " (the sum of increased and decreased) karma things. is " "only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] {increased,decreased,active}\n" "\n" " Riporta il karma maggiormente aumentato (increased), diminuito " "(decreased)\n" ", o più attivo (active) (la somma di aumentato e diminuito). " "è \n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:404 #, fuzzy msgid "" "[] []\n" "\n" " Resets the karma of to 0. If is not given, resets\n" " everything.\n" " " msgstr "" "[] \n" "\n" " Azzera i karma di .\n" " " #: plugin.py:415 msgid "" "[] \n" "\n" " Dumps the Karma database for to in the bot's\n" " data directory. is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Esporta il database dei karma di in nella " "directory dei dati\n" " del bot. è necessario solo se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:428 msgid "" "[] \n" "\n" " Loads the Karma database for from in the bot's\n" " data directory. is only necessary if the message isn't " "sent\n" " in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Carica il database dei karma di da nella " "directory dei dati\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/plugin.py0000644000175000017500000004357714535072470016524 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, 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 sys import csv 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('Karma') import sqlite3 def checkAllowShell(irc): if not conf.supybot.commands.allowShell(): irc.error('This command is not available, because ' 'supybot.commands.allowShell is False.', Raise=True) class SqliteKarmaDB(object): def __init__(self, filename): self.dbs = ircutils.IrcDict() self.filename = filename def close(self): for db in self.dbs.values(): db.close() def _getDb(self, channel): filename = plugins.makeChannelFilename(self.filename, channel) if filename in self.dbs: return self.dbs[filename] if os.path.exists(filename): db = sqlite3.connect(filename, check_same_thread=False) if minisix.PY2: db.text_factory = str self.dbs[filename] = db return db db = sqlite3.connect(filename, check_same_thread=False) if minisix.PY2: db.text_factory = str self.dbs[filename] = db cursor = db.cursor() cursor.execute("""CREATE TABLE karma ( id INTEGER PRIMARY KEY, name TEXT, normalized TEXT UNIQUE ON CONFLICT IGNORE, added INTEGER, subtracted INTEGER )""") db.commit() def p(s1, s2): return int(ircutils.nickEqual(s1, s2)) db.create_function('nickeq', 2, p) return db def get(self, channel, thing): db = self._getDb(channel) thing = thing.lower() cursor = db.cursor() cursor.execute("""SELECT added, subtracted FROM karma WHERE normalized=?""", (thing,)) results = cursor.fetchall() if len(results) == 0: return None else: return list(map(int, results[0])) def gets(self, channel, things): db = self._getDb(channel) cursor = db.cursor() normalizedThings = dict(list(zip([s.lower() for s in things], things))) criteria = ' OR '.join(['normalized=?'] * len(normalizedThings)) sql = """SELECT name, added-subtracted FROM karma WHERE %s ORDER BY added-subtracted DESC""" % criteria cursor.execute(sql, list(normalizedThings.keys())) L = [(name, int(karma)) for (name, karma) in cursor.fetchall()] for (name, _) in L: del normalizedThings[name.lower()] neutrals = list(normalizedThings.values()) neutrals.sort() return (L, neutrals) def top(self, channel, limit): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT name, added-subtracted FROM karma ORDER BY added-subtracted DESC LIMIT ?""", (limit,)) return [(t[0], int(t[1])) for t in cursor.fetchall()] def bottom(self, channel, limit): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT name, added-subtracted FROM karma ORDER BY added-subtracted ASC LIMIT ?""", (limit,)) return [(t[0], int(t[1])) for t in cursor.fetchall()] def rank(self, channel, thing): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT added-subtracted FROM karma WHERE name=?""", (thing,)) results = cursor.fetchall() if len(results) == 0: return None karma = int(results[0][0]) cursor.execute("""SELECT COUNT(*) FROM karma WHERE added-subtracted > ?""", (karma,)) rank = int(cursor.fetchone()[0]) return rank+1 def size(self, channel): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT COUNT(*) FROM karma""") return int(cursor.fetchone()[0]) def increment(self, channel, name): db = self._getDb(channel) cursor = db.cursor() normalized = name.lower() cursor.execute("""INSERT INTO karma VALUES (NULL, ?, ?, 0, 0)""", (name, normalized,)) cursor.execute("""UPDATE karma SET added=added+1 WHERE normalized=?""", (normalized,)) db.commit() def decrement(self, channel, name): db = self._getDb(channel) cursor = db.cursor() normalized = name.lower() cursor.execute("""INSERT INTO karma VALUES (NULL, ?, ?, 0, 0)""", (name, normalized,)) cursor.execute("""UPDATE karma SET subtracted=subtracted+1 WHERE normalized=?""", (normalized,)) db.commit() def most(self, channel, kind, limit): if kind == 'increased': orderby = 'added' elif kind == 'decreased': orderby = 'subtracted' elif kind == 'active': orderby = 'added+subtracted' else: raise ValueError('invalid kind') sql = """SELECT name, %s FROM karma ORDER BY %s DESC LIMIT %s""" % \ (orderby, orderby, limit) db = self._getDb(channel) cursor = db.cursor() cursor.execute(sql) return [(name, int(i)) for (name, i) in cursor.fetchall()] def clear(self, channel, name=None): db = self._getDb(channel) cursor = db.cursor() if name: normalized = name.lower() cursor.execute("""DELETE FROM karma WHERE normalized=?""", (normalized,)) else: cursor.execute("""DELETE FROM karma""") db.commit() def dump(self, channel, filename): filename = conf.supybot.directories.data.dirize(filename) fd = utils.file.AtomicFile(filename) out = csv.writer(fd) db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT name, added, subtracted FROM karma""") for (name, added, subtracted) in cursor.fetchall(): out.writerow([name, added, subtracted]) fd.close() def load(self, channel, filename): filename = conf.supybot.directories.data.dirize(filename) fd = open(filename, encoding='utf8') reader = csv.reader(fd) db = self._getDb(channel) cursor = db.cursor() cursor.execute("""DELETE FROM karma""") for (name, added, subtracted) in reader: normalized = name.lower() cursor.execute("""INSERT INTO karma VALUES (NULL, ?, ?, ?, ?)""", (name, normalized, added, subtracted,)) db.commit() fd.close() KarmaDB = plugins.DB('Karma', {'sqlite3': SqliteKarmaDB}) class Karma(callbacks.Plugin): """ Provides a simple tracker for setting Karma (thing++, thing--). If ``config plugins.karma.allowUnaddressedKarma`` is set to ``True`` (default since 2014.05.07), saying `boats++` will give 1 karma to ``boats``, and ``ships--`` will subtract 1 karma from ``ships``. However, if you use this in a sentence, like ``That deserves a ++. Kevin++``, 1 karma will be added to ``That deserves a ++. Kevin``, so you should only add or subtract karma in a line that doesn't have anything else in it. Alternatively, you can restrict karma tracking to nicks in the current channel by setting `config plugins.Karma.onlyNicks` to ``True``. If ``config plugins.karma.allowUnaddressedKarma` is set to `False``, you must address the bot with nick or prefix to add or subtract karma. """ callBefore = ('Factoids', 'MoobotFactoids', 'Infobot') def __init__(self, irc): self.__parent = super(Karma, self) self.__parent.__init__(irc) self.db = KarmaDB() def die(self): self.__parent.die() self.db.close() def _normalizeThing(self, thing): assert thing if thing[0] == '(' and thing[-1] == ')': thing = thing[1:-1] return thing def _respond(self, irc, channel, thing, karma): if self.registryValue('response', channel, irc.network): irc.reply(_('%(thing)s\'s karma is now %(karma)i') % {'thing': thing, 'karma': karma}) else: irc.noReply() def _doKarma(self, irc, msg, channel, thing): inc = self.registryValue('incrementChars', channel, irc.network) dec = self.registryValue('decrementChars', channel, irc.network) onlynicks = self.registryValue('onlyNicks', channel, irc.network) karma = '' for s in inc: if thing.endswith(s): thing = thing[:-len(s)] # Don't reply if the target isn't a nick if onlynicks and thing.lower() not in map(ircutils.toLower, irc.state.channels[channel].users): return if ircutils.strEqual(thing, msg.nick) and \ not self.registryValue('allowSelfRating', channel, irc.network): irc.error(_('You\'re not allowed to adjust your own karma.')) return self.db.increment(channel, self._normalizeThing(thing)) karma = self.db.get(channel, self._normalizeThing(thing)) for s in dec: if thing.endswith(s): thing = thing[:-len(s)] if onlynicks and thing.lower() not in map(ircutils.toLower, irc.state.channels[channel].users): return if ircutils.strEqual(thing, msg.nick) and \ not self.registryValue('allowSelfRating', channel, irc.network): irc.error(_('You\'re not allowed to adjust your own karma.')) return self.db.decrement(channel, self._normalizeThing(thing)) karma = self.db.get(channel, self._normalizeThing(thing)) if karma: self._respond(irc, channel, thing, karma[0]-karma[1]) def invalidCommand(self, irc, msg, tokens): if msg.channel and tokens: thing = ' '.join(tokens) self._doKarma(irc, msg, msg.channel, thing) def doPrivmsg(self, irc, msg): # We don't handle this if we've been addressed because invalidCommand # will handle it for us. This prevents us from accessing the db twice # and therefore crashing. if not (msg.addressed or msg.repliedTo): if msg.channel and \ not ircmsgs.isCtcp(msg) and \ self.registryValue('allowUnaddressedKarma', msg.channel, irc.network): irc = callbacks.SimpleProxy(irc, msg) thing = msg.args[1].rstrip() self._doKarma(irc, msg, msg.channel, thing) @internationalizeDocstring def karma(self, irc, msg, args, channel, things): """[] [ ...] Returns the karma of . If is not given, returns the top N karmas, where N is determined by the config variable supybot.plugins.Karma.rankingDisplay. If one is given, returns the details of its karma; if more than one is given, returns the total karma of each of the things. is only necessary if the message isn't sent on the channel itself. """ if len(things) == 1: name = things[0] t = self.db.get(channel, name) if t is None: irc.reply(format(_('%s has neutral karma.'), name)) else: (added, subtracted) = t total = added - subtracted if self.registryValue('simpleOutput', channel, irc.network): s = format('%s: %i', name, total) else: s = format(_('Karma for %q has been increased %n and ' 'decreased %n for a total karma of %s.'), name, (added, _('time')), (subtracted, _('time')), total) irc.reply(s) elif len(things) > 1: (L, neutrals) = self.db.gets(channel, things) if L: s = format('%L', [format('%s: %i', *t) for t in L]) if neutrals: neutral = format('. %L %h neutral karma', neutrals, len(neutrals)) s += neutral irc.reply(s + '.') else: irc.reply(_('I didn\'t know the karma for any of those ' 'things.')) else: # No name was given. Return the top/bottom N karmas. limit = self.registryValue('rankingDisplay', channel, irc.network) highest = [format('%q (%s)', s, t) for (s, t) in self.db.top(channel, limit)] lowest = [format('%q (%s)', s, t) for (s, t) in self.db.bottom(channel, limit)] if not (highest and lowest): irc.error(_('I have no karma for this channel.')) return rank = self.db.rank(channel, msg.nick) if rank is not None: total = self.db.size(channel) rankS = format(_(' You (%s) are ranked %i out of %i.'), msg.nick, rank, total) else: rankS = '' s = format(_('Highest karma: %L. Lowest karma: %L.%s'), highest, lowest, rankS) irc.reply(s) karma = wrap(karma, ['channel', any('something')]) _mostAbbrev = utils.abbrev(['increased', 'decreased', 'active']) @internationalizeDocstring def most(self, irc, msg, args, channel, kind): """[] {increased,decreased,active} Returns the most increased, the most decreased, or the most active (the sum of increased and decreased) karma things. is only necessary if the message isn't sent in the channel itself. """ L = self.db.most(channel, kind, self.registryValue('mostDisplay', channel, irc.network)) if L: L = [format('%q: %i', name, i) for (name, i) in L] irc.reply(format('%L', L)) else: irc.error(_('I have no karma for this channel.')) most = wrap(most, ['channel', ('literal', ['increased', 'decreased', 'active'])]) @internationalizeDocstring def clear(self, irc, msg, args, channel, name): """[] [] Resets the karma of to 0. If is not given, resets everything. """ self.db.clear(channel, name or None) irc.replySuccess() clear = wrap(clear, [('checkChannelCapability', 'op'), optional('text')]) @internationalizeDocstring def dump(self, irc, msg, args, channel, filename): """[] Dumps the Karma database for to in the bot's data directory. is only necessary if the message isn't sent in the channel itself. """ checkAllowShell(irc) self.db.dump(channel, filename) irc.replySuccess() dump = wrap(dump, [('checkCapability', 'owner'), 'channeldb', 'filename']) @internationalizeDocstring def load(self, irc, msg, args, channel, filename): """[] Loads the Karma database for from in the bot's data directory. is only necessary if the message isn't sent in the channel itself. """ checkAllowShell(irc) self.db.load(channel, filename) irc.replySuccess() load = wrap(load, [('checkCapability', 'owner'), 'channeldb', 'filename']) Class = Karma # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Karma/test.py0000644000175000017500000002335214535072470016172 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import sqlite3 class KarmaTestCase(ChannelPluginTestCase): plugins = ('Karma',) def testKarma(self): self.assertError('karma') self.assertRegexp('karma foobar', 'neutral karma') try: conf.replyWhenNotCommand = True self.assertNoResponse('foobar++', 2) finally: conf.replyWhenNotCommand = False self.assertRegexp('karma foobar', 'increased 1.*total.*1') self.assertRegexp('karma FOOBAR', 'increased 1.*total.*1') self.assertNoResponse('foobar--', 2) self.assertRegexp('karma foobar', 'decreased 1.*total.*0') self.assertRegexp('karma FOOBAR', 'decreased 1.*total.*0') self.assertNoResponse('FOO++', 2) self.assertNoResponse('BAR--', 2) self.assertRegexp('karma foo bar foobar', '.*foo.*foobar.*bar.*') self.assertRegexp('karma FOO BAR FOOBAR', '.*foo.*foobar.*bar.*') self.assertRegexp('karma FOO BAR FOOBAR', '.*FOO.*foobar.*BAR.*', flags=0) self.assertRegexp('karma foo bar foobar asdfjkl', 'asdfjkl') # Test case-insensitive self.assertNoResponse('MOO++', 2) self.assertRegexp('karma moo', 'Karma for [\'"]moo[\'"].*increased 1.*total.*1') self.assertRegexp('karma MoO', 'Karma for [\'"]MoO[\'"].*increased 1.*total.*1') def testKarmaRankingDisplayConfigurable(self): try: orig = conf.supybot.plugins.Karma.response() conf.supybot.plugins.Karma.response.setValue(True) original = conf.supybot.plugins.Karma.rankingDisplay() self.assertNotError('foo++') self.assertNotError('foo++') self.assertNotError('foo++') self.assertNotError('foo++') self.assertNotError('bar++') self.assertNotError('bar++') self.assertNotError('bar++') self.assertNotError('baz++') self.assertNotError('baz++') self.assertNotError('quux++') self.assertNotError('xuuq--') self.assertNotError('zab--') self.assertNotError('zab--') self.assertNotError('rab--') self.assertNotError('rab--') self.assertNotError('rab--') self.assertNotError('oof--') self.assertNotError('oof--') self.assertNotError('oof--') self.assertNotError('oof--') self.assertRegexp('karma', 'foo.*bar.*baz.*oof.*rab.*zab') conf.supybot.plugins.Karma.rankingDisplay.setValue(4) self.assertRegexp('karma', 'foo.*bar.*baz.*quux') finally: conf.supybot.plugins.Karma.response.setValue(orig) conf.supybot.plugins.Karma.rankingDisplay.setValue(original) def testMost(self): self.assertError('most increased') self.assertError('most decreased') self.assertError('most active') self.assertHelp('most aldsfkj') self.assertNoResponse('foo++', 1) self.assertNoResponse('foo++', 1) self.assertNoResponse('bar++', 1) self.assertNoResponse('bar--', 1) self.assertNoResponse('bar--', 1) self.assertRegexp('karma most active', 'bar.*foo') self.assertRegexp('karma most increased', 'foo.*bar') self.assertRegexp('karma most decreased', 'bar.*foo') self.assertNoResponse('foo--', 1) self.assertNoResponse('foo--', 1) self.assertNoResponse('foo--', 1) self.assertNoResponse('foo--', 1) self.assertRegexp('karma most active', 'foo.*bar') self.assertRegexp('karma most increased', 'foo.*bar') self.assertRegexp('karma most decreased', 'foo.*bar') def testSimpleOutput(self): try: orig = conf.supybot.plugins.Karma.simpleOutput() conf.supybot.plugins.Karma.simpleOutput.setValue(True) self.assertNoResponse('foo++', 2) self.assertResponse('karma foo', 'foo: 1') self.assertNoResponse('bar--', 2) self.assertResponse('karma bar', 'bar: -1') finally: conf.supybot.plugins.Karma.simpleOutput.setValue(orig) def testSelfRating(self): nick = self.nick try: orig = conf.supybot.plugins.Karma.allowSelfRating() conf.supybot.plugins.Karma.allowSelfRating.setValue(False) self.assertError('%s++' % nick) self.assertResponse('karma %s' % nick, '%s has neutral karma.' % nick) conf.supybot.plugins.Karma.allowSelfRating.setValue(True) self.assertNoResponse('%s++' % nick, 2) self.assertRegexp('karma %s' % nick, 'Karma for [\'"]%s[\'"].*increased 1.*total.*1' % nick) finally: conf.supybot.plugins.Karma.allowSelfRating.setValue(orig) def testKarmaOutputConfigurable(self): self.assertNoResponse('foo++', 2) try: orig = conf.supybot.plugins.Karma.response() conf.supybot.plugins.Karma.response.setValue(True) self.assertNotError('foo++') finally: conf.supybot.plugins.Karma.response.setValue(orig) def testKarmaMostDisplayConfigurable(self): self.assertNoResponse('foo++', 1) self.assertNoResponse('foo++', 1) self.assertNoResponse('bar++', 1) self.assertNoResponse('bar--', 1) self.assertNoResponse('bar--', 1) self.assertNoResponse('foo--', 1) self.assertNoResponse('foo--', 1) self.assertNoResponse('foo--', 1) self.assertNoResponse('foo--', 1) try: orig = conf.supybot.plugins.Karma.mostDisplay() conf.supybot.plugins.Karma.mostDisplay.setValue(1) self.assertRegexp('karma most active', '(?!bar)') conf.supybot.plugins.Karma.mostDisplay.setValue(25) self.assertRegexp('karma most active', 'bar') finally: conf.supybot.plugins.Karma.mostDisplay.setValue(orig) def testIncreaseKarmaWithNickNotCallingInvalidCommand(self): self.assertSnarfNoResponse('%s: foo++' % self.irc.nick, 3) def testClear(self): self.assertNoResponse('foo++', 1) self.assertRegexp('karma foo', '1') self.assertNotError('karma clear foo') self.assertRegexp('karma foo', 'neutral') self.assertNotRegexp('karma foo', '1') # def testNoKarmaDunno(self): # self.assertNotError('load Infobot') # self.assertNoResponse('foo++') def testMultiWordKarma(self): self.assertNoResponse('(foo bar)++', 1) self.assertRegexp('karma "foo bar"', '1') def testUnaddressedKarma(self): karma = conf.supybot.plugins.Karma resp = karma.response() unaddressed = karma.allowUnaddressedKarma() try: karma.response.setValue(True) karma.allowUnaddressedKarma.setValue(True) for m in ('++', '--'): self.assertRegexp('foo%s' % m, 'is now') self.assertSnarfRegexp('foo%s' % m, 'is now') #self.assertNoResponse('foo bar%s' % m) #self.assertSnarfNoResponse('foo bar%s' % m) self.assertRegexp('(foo bar)%s' % m, 'is now') self.assertSnarfRegexp('(foo bar)%s' % m, 'is now') finally: karma.response.setValue(resp) karma.allowUnaddressedKarma.setValue(unaddressed) def testOnlyNicks(self): # We use this to join a dummy user to test upon msg = ircmsgs.join(self.channel, prefix='hello!foo@bar') self.irc.feedMsg(msg) karma = conf.supybot.plugins.Karma resp = karma.response() onlynicks = karma.onlyNicks() try: karma.onlynicks.setValue(True) karma.response.setValue(True) self.assertSnarfNoResponse('abcd++') self.assertSnarfRegexp('hello--', 'is now') self.assertSnarfNoResponse('abcd--') self.assertSnarfRegexp('hello++', 'is now') finally: karma.onlynicks.setValue(onlynicks) karma.response.setValue(resp) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Lart/0000755000175000017500000000000014535072535014505 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/__init__.py0000644000175000017500000000501314535072470016613 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ This plugin keeps a database of larts (Luser Attitude Readjustment Tool), and larts with it. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/config.py0000644000175000017500000000516714535072470016333 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Lart') 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('Lart', True) Lart = conf.registerPlugin('Lart') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Lart, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue(Lart, 'showIds', registry.Boolean(False, _("""Determines whether the bot will show the ids of a lart when the lart is given."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3417547 limnoria-2023.11.18/plugins/Lart/locales/0000755000175000017500000000000014535072535016127 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/locales/fi.po0000644000175000017500000000457314535072470017074 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen, 2011. # msgid "" msgstr "" "Project-Id-Version: Lart plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:04+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 the bot will show the ids\n" " of a lart when the lart is given." msgstr "" "Määrittää näyttääkö botti lartin id:eet\n" " , kun lart annetaan." #: plugin.py:40 msgid "" "\n" " Provides an implementation of the Luser Attitude Readjustment Tool\n" " for users.\n" "\n" " Example:\n" "\n" " * If you add ``slaps $who``.\n" " * And Someone says ``@lart ChanServ``.\n" " * ``* bot slaps ChanServ``.\n" " " msgstr "" #: plugin.py:59 msgid "Larts must contain $who." msgstr "Larttien täytyy sisältää $who." #: plugin.py:63 msgid "" "[] [] [for ]\n" "\n" " Uses the Luser Attitude Readjustment Tool on (for " ",\n" " if given). If is given, uses that specific lart. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] [for ]\n" "\n" " Käyttää \"Luser Attitude Readjustment Tool\" (for " ",\n" " jos annettu). Jos on annettu, käyttää sitä tiettyä Larttia. " " on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:77 msgid "There is no lart with id #%i." msgstr "Tuolla ID:llä ei ole larttia #%i." #: plugin.py:82 msgid "There are no larts in my database for %s." msgstr "Minun tietokannassani ei ole larttia %s:lle." #: plugin.py:88 msgid "trying to dis me" msgstr "yrittämässä dissata minua" #: plugin.py:96 msgid " for " msgstr "syystä" #, fuzzy #~ msgid "" #~ "Provides an implementation of the Luser Attitude Readjustment Tool\n" #~ " for users." #~ msgstr "Tarjoaa käyttäjille Luuserin Asenteen Uudelleenmääritys Työkalun." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/locales/fr.po0000644000175000017500000000375314535072470017104 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:50 msgid "" "Determines whether the bot will show the ids\n" " of a lart when the lart is given." msgstr "Détermine si le bot affichera les idées d'un lart lorsqu'il est donné." #: plugin.py:40 msgid "" "\n" " Provides an implementation of the Luser Attitude Readjustment Tool\n" " for users.\n" "\n" " Example:\n" "\n" " * If you add ``slaps $who``.\n" " * And Someone says ``@lart ChanServ``.\n" " * ``* bot slaps ChanServ``.\n" " " msgstr "" #: plugin.py:59 msgid "Larts must contain $who." msgstr "Les larts doivent contenir $who." #: plugin.py:63 msgid "" "[] [] [for ]\n" "\n" " Uses the Luser Attitude Readjustment Tool on (for " ",\n" " if given). If is given, uses that specific lart. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] [ [for ]\n" "\n" "Utilise le Luser Attitude Readjustment Tool sur (pour la " ", si elle est donnée). Si l' est donné, utilise un LART " "spécifique. n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:77 msgid "There is no lart with id #%i." msgstr "Il n'y a pas de lart d'id #%i." #: plugin.py:82 msgid "There are no larts in my database for %s." msgstr "Il n'y a pas de lart dans ma base de données pour %s." #: plugin.py:88 msgid "trying to dis me" msgstr "essaye de me manquer de respect" #: plugin.py:96 msgid " for " msgstr " pour " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/locales/it.po0000644000175000017500000000364414535072470017110 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 14:41+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 the bot will show the ids\n" " of a lart when the lart is given." msgstr "" "Determina se il bot mostrerà gli id\n" " di un lart quando questo viene dato." #: plugin.py:40 msgid "" "\n" " Provides an implementation of the Luser Attitude Readjustment Tool\n" " for users.\n" "\n" " Example:\n" "\n" " * If you add ``slaps $who``.\n" " * And Someone says ``@lart ChanServ``.\n" " * ``* bot slaps ChanServ``.\n" " " msgstr "" #: plugin.py:59 msgid "Larts must contain $who." msgstr "I lart devono contenere $who." #: plugin.py:63 msgid "" "[] [] [for ]\n" "\n" " Uses the Luser Attitude Readjustment Tool on (for " ",\n" " if given). If is given, uses that specific lart. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] [] [per ]\n" "\n" " Utilizza il Luser Attitude Readjustment Tool su (per " ",\n" " se fornito). Se viene dato, usa quello specifico lart. " "è\n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:77 msgid "There is no lart with id #%i." msgstr "Non c'è nessun lart con l'id #%i." #: plugin.py:82 msgid "There are no larts in my database for %s." msgstr "Non ci sono lart per %s nel mio database." #: plugin.py:88 msgid "trying to dis me" msgstr "cercando di mancarmi di rispetto" #: plugin.py:96 msgid " for " msgstr " per" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/plugin.py0000644000175000017500000001025014535072470016351 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Lart') class Lart(plugins.ChannelIdDatabasePlugin): """ Provides an implementation of the Luser Attitude Readjustment Tool for users. Example: * If you add ``slaps $who``. * And Someone says ``@lart ChanServ``. * ``* bot slaps ChanServ``. """ _meRe = re.compile(r'\bme\b', re.I) _myRe = re.compile(r'\bmy\b', re.I) def _replaceFirstPerson(self, s, nick): s = self._meRe.sub(nick, s) s = self._myRe.sub('%s\'s' % nick, s) return s def addValidator(self, irc, text): if '$who' not in text: irc.error(_('Larts must contain $who.'), Raise=True) @internationalizeDocstring def lart(self, irc, msg, args, channel, id, text): """[] [] [for ] Uses the Luser Attitude Readjustment Tool on (for , if given). If is given, uses that specific lart. is only necessary if the message isn't sent in the channel itself. """ if ' for ' in text: (target, reason) = list(map(str.strip, text.split(' for ', 1))) else: (target, reason) = (text, '') if id is not None: try: lart = self.db.get(channel, id) except KeyError: irc.error(format(_('There is no lart with id #%i.'), id)) return else: lart = self.db.random(channel) if not lart: irc.error(format(_('There are no larts in my database ' 'for %s.'), channel)) return text = lart.text if ircutils.strEqual(target, irc.nick): target = msg.nick reason = self._replaceFirstPerson(_('trying to dis me'), irc.nick) else: target = self._replaceFirstPerson(target, msg.nick) reason = self._replaceFirstPerson(reason, msg.nick) if target.endswith('.'): target = target.rstrip('.') text = text.replace('$who', target) if reason: text += _(' for ') + reason if self.registryValue('showIds', channel, irc.network): text += format(' (#%i)', lart.id) irc.reply(text, action=True) lart = wrap(lart, ['channeldb', optional('id'), 'text']) Class = Lart # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Lart/test.py0000644000175000017500000000502714535072470016040 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 * class LartTestCase(ChannelPluginTestCase): plugins = ('Lart', 'User') def setUp(self): ChannelPluginTestCase.setUp(self) # Create a valid user to use self.prefix = 'mf!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.nick, 'register tester moo', prefix=self.prefix)) m = self.irc.takeMsg() # Response to register. def testAdd(self): self.assertError('lart add foo') # needs $who self.assertNotError('lart add smacks $who') def testLart(self): self.assertError('lart foo') # no praises! self.assertNotError('lart add smacks $who') self.assertAction('lart foo', 'smacks foo') def testMeInReason(self): self.assertNotError('lart add makes $who sit by me') self.assertAction('lart foo', 'makes foo sit by me') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Later/0000755000175000017500000000000014535072535014652 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/__init__.py0000644000175000017500000000513214535072470016762 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Informal notes, mostly for compatibility with other bots. Based entirely on nicks, it's an easy way to tell users who refuse to register notes when they arrive later. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/config.py0000644000175000017500000000652514535072470016477 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Later') def configure(advanced): # This will be called by setup.py 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('Later', True) Later = conf.registerPlugin('Later') conf.registerGlobalValue(Later, 'maximum', registry.NonNegativeInteger(0, _("""Determines the maximum number of messages to be queued for a user. If this value is 0, there is no maximum. """))) conf.registerGlobalValue(Later, 'private', registry.Boolean(False, _("""Determines whether users will be notified in the first place in which they're seen, or in private."""))) conf.registerGlobalValue(Later, 'tellOnJoin', registry.Boolean(False, _("""Determines whether users will be notified upon joining any channel the bot is in, or only upon sending a message."""))) conf.registerGlobalValue(Later, 'messageExpiry', registry.NonNegativeInteger(30, _("""Determines the maximum number of days that a message will remain queued for a user. After this time elapses, the message will be deleted. If this value is 0, there is no maximum."""))) conf.registerGroup(Later, 'format') conf.registerGlobalValue(Later.format, 'senderHostname', registry.Boolean(False, _("""Determines whether senders' hostname will be shown in messages (instead of just the nick)."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Later/locales/0000755000175000017500000000000014535072535016274 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/locales/de.po0000644000175000017500000001631314535072470017226 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-11-02 00:00+0100\n" "Last-Translator: Florian Besser \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: German\n" "X-Poedit-Country: GERMANY\n" #: config.py:46 msgid "" "Determines the maximum number of\n" " messages to be queued for a user. If this value is 0, there is no " "maximum.\n" " " msgstr "" "Legt die maximalanzahl von Nachrichten fest die in einer Warteschlange " "abgelegt werden. Falls dieser Wert 0 ist, gibt es keine begrenzung." #: config.py:50 msgid "" "Determines whether users will be notified in\n" " the first place in which they're seen, or in private." msgstr "" "Legt fest ob der Benutzer dort benachrichtig wird wo er zuerst gesehen wird, " "oder privat." #: config.py:53 msgid "" "Determines whether users will be notified upon\n" " joining any channel the bot is in, or only upon sending a message." msgstr "" "Legt fest ob ein Benutzer benachrichtig werden soll sobald er einen Kanal " "des Bots betritt, oder nach dem senden einer Nachricht." #: config.py:56 msgid "" "Determines the maximum number of\n" " days that a message will remain queued for a user. After this time " "elapses,\n" " the message will be deleted. If this value is 0, there is no maximum." msgstr "" "Legt die maximal Anzahl an Tagen fest, die eine Nachricht in der " "Warteschlange bleibt. Nach dieser Zeit wird die Nachricht gelöscht. Falls " "dieser Wert 0 ist gibt es keine Begrenzung." #: config.py:62 msgid "" "Determines whether senders' hostname will be\n" " shown in messages (instead of just the nick)." msgstr "" #: plugin.py:51 msgid "" "\n" " Used to do things later; currently, it only allows the sending of\n" " nick-based notes. Do note (haha!) that these notes are *not* private\n" " and don't even pretend to be; if you want such features, consider using " "the\n" " Note plugin.\n" "\n" " Use the ``later tell`` command to leave a message to a user.\n" " If you sent the message by accident or want to cancel it,\n" " you can use the `later undo` command to remove the latest later,\n" " which you have sent.\n" "\n" " You can also see the people who have notes waiting for them by using\n" " the `later notes` command. If you specify a nickname in ``later notes``\n" " command, you will see the notes, which are waiting for the nickname.\n" "\n" " Privacy\n" " -------\n" "\n" " As you probably noticed from above, this plugin isn't private.\n" " Everyone can see notes sent by anyone and the laters are sent on " "channel\n" " by default and as the \"plugin help later\" says::\n" "\n" " Used to do things later; currently, it only allows the sending of " "nick-based notes. Do note (haha!) that these notes are *not* private and " "don't even pretend to be; if you want such features, consider using the Note " "plugin.\n" "\n" " The Note plugin identifies people by username instead of nickname\n" " and allows only users to send notes.\n" " The only people who are able to read notes are the sender, receiver,\n" " and the owner.\n" "\n" " " msgstr "" #: plugin.py:117 msgid "just now" msgstr "jetzt" #: plugin.py:164 #, fuzzy msgid "" " \n" "\n" " Tells each the next time is seen. " "can\n" " contain wildcard characters, and the first matching nick will be\n" " given the note.\n" " " msgstr "" " \n" "\n" "Sagt , sobald das nächste Mal gesehen wird. kann " "Platzhalter enthalten, und der erste Nick der darauf passt wird die Notiz " "bekommen." #: plugin.py:174 msgid "I can't send notes to myself." msgstr "Ich kann keine Notizen an mich selbst senden." #: plugin.py:185 #, fuzzy msgid "These recipients' message queue are already full: %L" msgstr "Die Warteschlange dieser person ist bereits voll." #: plugin.py:193 msgid "" "[]\n" "\n" " If is given, replies with what notes are waiting on ,\n" " otherwise, replies with the nicks that have notes waiting for them.\n" " " msgstr "" "[]\n" "\n" "Falls angegeben wird, wird mit den Notizen geantwortet die auf " "waren, andernfalls wird mit den Nicks geanwortet die Notizen in der " "Warteschlange haben." #: plugin.py:204 msgid "I have no notes for that nick." msgstr "Ich habe keine Notizen für diesen Nick." #: plugin.py:209 msgid "I currently have notes waiting for %L." msgstr "Ich habe momentan keine Notizen in der Warteschlange für %L." #: plugin.py:212 msgid "I have no notes waiting to be delivered." msgstr "Ich habe keine Notizen, die darauf warten zugestellt zu werden." #: plugin.py:217 msgid "" "\n" "\n" " Removes the notes waiting on .\n" " " msgstr "" "\n" "\n" "Entfernt die wartenden Notizen für ." #: plugin.py:226 msgid "There were no notes for %r" msgstr "Da waren keine Nachrichten für %r" #: plugin.py:231 msgid "" "\n" "\n" " Removes the latest note you sent to .\n" " " msgstr "" "\n" "\n" "Entfern die letzte Notiz die du an gesendet hast." #: plugin.py:236 msgid "There are no note waiting for %s." msgstr "Es gibt keine Notiz, die auf %s wartet." #: plugin.py:247 msgid "There are no note from you waiting for %s." msgstr "Es gibt keine Notiz von dir, die auf %s wartet." #: plugin.py:275 msgid "Sent %s: <%s> %s" msgstr "%s gesendet: <%s> %s" #~ msgid "" #~ "Used to do things later; currently, it only allows the sending of\n" #~ " nick-based notes. Do note (haha!) that these notes are *not* " #~ "private\n" #~ " and don't even pretend to be; if you want such features, consider " #~ "using the\n" #~ " Note plugin." #~ msgstr "" #~ "Wird benutzt um Sachen später zu machen, momentan kann man nur Notizen an " #~ "andere Nutzer senden. Beachte, dass diese Notizen *nicht* privat sind und " #~ "auch nicht sein wollen; falls du soetwas sucht, überlege das Note Plugin " #~ "zu benutzen." #~ msgid "%s ago" #~ msgstr "vor %s" #~ msgid "" #~ "Validate nick according to the IRC RFC 2812 spec.\n" #~ "\n" #~ " Reference: http://tools.ietf.org/rfcmarkup?" #~ "doc=2812#section-2.3.1\n" #~ "\n" #~ " Some irc clients' tab-completion feature appends 'address' " #~ "characters\n" #~ " to nick, such as ':' or ','. We try correcting for that by " #~ "trimming\n" #~ " a char off the end.\n" #~ "\n" #~ " If nick incorrigibly invalid, return False, otherwise,\n" #~ " return (possibly trimmed) nick.\n" #~ " " #~ msgstr "" #~ "Validiert den Nick nach der IRC RFC 2812 spec.\n" #~ "\n" #~ "Referenz: http://tools.ietf.org/rfcmarkup?doc=2812#section-2.3.1\n" #~ "\n" #~ "Manche Klienten fügen bei der Tabvervollständigung 'addressierungs' " #~ "Zeichen hinzu, sowie ':' oder','. Wir probieren das zu korregieren indem " #~ "wir das letzte Zeichen abschneiden.\n" #~ "\n" #~ "Falls der Nick nicht korrigiert werden kann, wird False zurückgegeben, " #~ "andererseits der (möglicherweise gekürzte) Nick." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/locales/fi.po0000644000175000017500000001734414535072470017241 0ustar00valval# Later plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-03-22 15:33+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "X-Generator: Poedit 1.5.4\n" #: config.py:46 msgid "" "Determines the maximum number of\n" " messages to be queued for a user. If this value is 0, there is no " "maximum.\n" " " msgstr "" "Määrittää maksimi määrän\n" " viestejä, jotka ovat jonossa käyttäjälle. Jos tämä arvo on 0, maksimia " "ei ole.\n" " " #: config.py:50 msgid "" "Determines whether users will be notified in\n" " the first place in which they're seen, or in private." msgstr "" "Määrittää huomautetaanko käyttäjiä heti\n" " siinä paikassa kun heidät on nähty vai yksityisesti." #: config.py:53 msgid "" "Determines whether users will be notified upon\n" " joining any channel the bot is in, or only upon sending a message." msgstr "" "Määrittää huomautetaanko käyttäjiä kun liittyessä\n" " mille tahansa kanavalle. jolla botti on vai vain viestiä lähetettäessä." #: config.py:56 msgid "" "Determines the maximum number of\n" " days that a message will remain queued for a user. After this time " "elapses,\n" " the message will be deleted. If this value is 0, there is no maximum." msgstr "" "Määrittää maksimi määrän päiviä, jonka aikana\n" " viesti on jonossa käyttäjälle. Sen jälkeen kun tämä aika on kulunut,\n" " viesti poistetaan. Jos tämä arvo on 0, maksimia ei ole." #: config.py:62 msgid "" "Determines whether senders' hostname will be\n" " shown in messages (instead of just the nick)." msgstr "" #: plugin.py:51 msgid "" "\n" " Used to do things later; currently, it only allows the sending of\n" " nick-based notes. Do note (haha!) that these notes are *not* private\n" " and don't even pretend to be; if you want such features, consider using " "the\n" " Note plugin.\n" "\n" " Use the ``later tell`` command to leave a message to a user.\n" " If you sent the message by accident or want to cancel it,\n" " you can use the `later undo` command to remove the latest later,\n" " which you have sent.\n" "\n" " You can also see the people who have notes waiting for them by using\n" " the `later notes` command. If you specify a nickname in ``later notes``\n" " command, you will see the notes, which are waiting for the nickname.\n" "\n" " Privacy\n" " -------\n" "\n" " As you probably noticed from above, this plugin isn't private.\n" " Everyone can see notes sent by anyone and the laters are sent on " "channel\n" " by default and as the \"plugin help later\" says::\n" "\n" " Used to do things later; currently, it only allows the sending of " "nick-based notes. Do note (haha!) that these notes are *not* private and " "don't even pretend to be; if you want such features, consider using the Note " "plugin.\n" "\n" " The Note plugin identifies people by username instead of nickname\n" " and allows only users to send notes.\n" " The only people who are able to read notes are the sender, receiver,\n" " and the owner.\n" "\n" " " msgstr "" #: plugin.py:117 msgid "just now" msgstr "juuri nyt" #: plugin.py:164 #, fuzzy msgid "" " \n" "\n" " Tells each the next time is seen. " "can\n" " contain wildcard characters, and the first matching nick will be\n" " given the note.\n" " " msgstr "" " \n" "\n" " Kertoo seuraavalla kerralla, kun " " nähdään. voi\n" " sisältää jokerimerkkejä ja ensimmäiselle täsmäävälle nimimerkille\n" " annetaan muistiinpano.\n" " " #: plugin.py:174 msgid "I can't send notes to myself." msgstr "En voi lähettää muistiinpanoja itselleni." #: plugin.py:185 #, fuzzy msgid "These recipients' message queue are already full: %L" msgstr "Tuon henkilön viestijono on jo täynnä." #: plugin.py:193 msgid "" "[]\n" "\n" " If is given, replies with what notes are waiting on ,\n" " otherwise, replies with the nicks that have notes waiting for them.\n" " " msgstr "" "[]\n" "\n" " Jos on annettu, vastaa niillä muistiinpanoja, jotka " "odottaavat ,\n" " muutoin, vastaa nimimerkeillä, joilla on odottavia muistiinpanoja.\n" " " #: plugin.py:204 msgid "I have no notes for that nick." msgstr "Minulla ei ole muistiinpanoja odottamassa tuota nimimerkkiä." #: plugin.py:209 msgid "I currently have notes waiting for %L." msgstr "Minulla on tällä hetkellä muistiinpanoja odottamassa %L:ää.." #: plugin.py:212 msgid "I have no notes waiting to be delivered." msgstr "Minulla ei ole muistiinpanoja odottamassa toimitetuksi tulemista." #: plugin.py:217 msgid "" "\n" "\n" " Removes the notes waiting on .\n" " " msgstr "" "\n" "\n" " Poistaa muistiinpanot, jotka odottavat .\n" " " #: plugin.py:226 msgid "There were no notes for %r" msgstr " %r:lle ei ollut muistiinpanoja." #: plugin.py:231 msgid "" "\n" "\n" " Removes the latest note you sent to .\n" " " msgstr "" "\n" "\n" " Poistaa viimeisimmän muistiinpanon, jonka olet lähettänyt " ".\n" " " #: plugin.py:236 msgid "There are no note waiting for %s." msgstr "%s:lle ei ole odottavia muistiinpanoja." #: plugin.py:247 msgid "There are no note from you waiting for %s." msgstr "Sinulla ei ole odottavia muistiinpanoja %s:lle." #: plugin.py:275 msgid "Sent %s: <%s> %s" msgstr "Lähetetty %s: <%s> %s" #~ msgid "" #~ "Used to do things later; currently, it only allows the sending of\n" #~ " nick-based notes. Do note (haha!) that these notes are *not* " #~ "private\n" #~ " and don't even pretend to be; if you want such features, consider " #~ "using the\n" #~ " Note plugin." #~ msgstr "" #~ "Tottunut tekemään asioita myöhemmin; tällä hetkellä, se sallii vain " #~ "nimimerkki-pohjaisten\n" #~ " muistiinpanojen lähettämisen. Muista (haha!), että nämä muistiimpanot " #~ "*eivät* ole yksityisiä\n" #~ " eivätkä edes esitä olevansa; jos tahdot sellaisia toimintoja, " #~ "harkitse\n" #~ " Note lisäosan käyttöä." #, fuzzy #~ msgid "" #~ "Validate nick according to the IRC RFC 2812 spec.\n" #~ "\n" #~ " Reference: http://tools.ietf.org/rfcmarkup?" #~ "doc=2812#section-2.3.1\n" #~ "\n" #~ " Some irc clients' tab-completion feature appends 'address' " #~ "characters\n" #~ " to nick, such as ':' or ','. We try correcting for that by " #~ "trimming\n" #~ " a char off the end.\n" #~ "\n" #~ " If nick incorrigibly invalid, return False, otherwise,\n" #~ " return (possibly trimmed) nick.\n" #~ " " #~ msgstr "" #~ "Varmista ninimerkki IRC RFC 2812 vaatimusten mukaan.\n" #~ "\n" #~ " Viittaukset: http://tools.ietf.org/rfcmarkup?" #~ "doc=2812#section-2.3.1\n" #~ "\n" #~ " Jotkut IRC asiakasohjelmat' tabulaattoritäyttävät toimintoja " #~ "liittyen 'osoite' merkkeihin\n" #~ " nimimerkeissä, kuten ':' tai ','. Me yritämme oikaista tämän " #~ "trimmaamalla\n" #~ " merkin lopusta.\n" #~ "\n" #~ " Jos nimimerkki on viallinen, palauttaa Falsen, muutoin palauttaa\n" #~ " (mahdollisesti trimmatun) nimimerkin.\n" #~ " " #~ msgid "%s ago" #~ msgstr "%s sitten" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/locales/fr.po0000644000175000017500000001512314535072470017243 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:46 msgid "" "Determines the maximum number of\n" " messages to be queued for a user. If this value is 0, there is no " "maximum.\n" " " msgstr "" "Détermine le nombre maximum de messages en attente d'un utilisateur. Si la " "valeur est 0, il n'y a pas de maximum" #: config.py:50 msgid "" "Determines whether users will be notified in\n" " the first place in which they're seen, or in private." msgstr "" "Détermine si les utilisateurs seront notifiés au premier endroit où ils sont " "vus, ou en privé." #: config.py:53 msgid "" "Determines whether users will be notified upon\n" " joining any channel the bot is in, or only upon sending a message." msgstr "" "Détermine si les utilisateurs seront notifiés au premier endroit où ils sont " "vus, ou seulement lorsqu'ils envoient un message." #: config.py:56 msgid "" "Determines the maximum number of\n" " days that a message will remain queued for a user. After this time " "elapses,\n" " the message will be deleted. If this value is 0, there is no maximum." msgstr "" "Détermine le nombre maximum de messages en attente d'un utilisateur. Après " "que ce temps se soit écoulé, le message sera supprimé. Si la valeur est 0, " "il n'y a pas de maximum." #: config.py:62 msgid "" "Determines whether senders' hostname will be\n" " shown in messages (instead of just the nick)." msgstr "" #: plugin.py:51 msgid "" "\n" " Used to do things later; currently, it only allows the sending of\n" " nick-based notes. Do note (haha!) that these notes are *not* private\n" " and don't even pretend to be; if you want such features, consider using " "the\n" " Note plugin.\n" "\n" " Use the ``later tell`` command to leave a message to a user.\n" " If you sent the message by accident or want to cancel it,\n" " you can use the `later undo` command to remove the latest later,\n" " which you have sent.\n" "\n" " You can also see the people who have notes waiting for them by using\n" " the `later notes` command. If you specify a nickname in ``later notes``\n" " command, you will see the notes, which are waiting for the nickname.\n" "\n" " Privacy\n" " -------\n" "\n" " As you probably noticed from above, this plugin isn't private.\n" " Everyone can see notes sent by anyone and the laters are sent on " "channel\n" " by default and as the \"plugin help later\" says::\n" "\n" " Used to do things later; currently, it only allows the sending of " "nick-based notes. Do note (haha!) that these notes are *not* private and " "don't even pretend to be; if you want such features, consider using the Note " "plugin.\n" "\n" " The Note plugin identifies people by username instead of nickname\n" " and allows only users to send notes.\n" " The only people who are able to read notes are the sender, receiver,\n" " and the owner.\n" "\n" " " msgstr "" #: plugin.py:117 msgid "just now" msgstr "à l'instant" #: plugin.py:164 #, fuzzy msgid "" " \n" "\n" " Tells each the next time is seen. " "can\n" " contain wildcard characters, and the first matching nick will be\n" " given the note.\n" " " msgstr "" " \n" "\n" "Dit le à la prochaine fois qu'iel est vu-e. peut " "contenir des jokers, et le premier nick correspondant recevra la note." #: plugin.py:174 msgid "I can't send notes to myself." msgstr "Je ne peux m'envoyer de notes à moi-même." #: plugin.py:185 #, fuzzy msgid "These recipients' message queue are already full: %L" msgstr "La file d'attente des messages de cette personne est déjà pleine." #: plugin.py:193 msgid "" "[]\n" "\n" " If is given, replies with what notes are waiting on ,\n" " otherwise, replies with the nicks that have notes waiting for them.\n" " " msgstr "" "[]\n" "\n" "Si le est donné, répond avec les notes en attente pour ; " "sinon, répond avec les nicks ayant des notes en attente." #: plugin.py:204 msgid "I have no notes for that nick." msgstr "Je n'ai pas de note pour ce nick." #: plugin.py:209 msgid "I currently have notes waiting for %L." msgstr "J'ai actuellement des notes en attente pour %L." #: plugin.py:212 msgid "I have no notes waiting to be delivered." msgstr "Je n'ai pas de note à délivrer." #: plugin.py:217 msgid "" "\n" "\n" " Removes the notes waiting on .\n" " " msgstr "" "\n" "\n" "Supprime les notes en attente pour ." #: plugin.py:226 msgid "There were no notes for %r" msgstr "Il n'y a pas de note pour %r" #: plugin.py:231 msgid "" "\n" "\n" " Removes the latest note you sent to .\n" " " msgstr "" "\n" "\n" "Supprime la dernière note que vous avez envoyée à ." #: plugin.py:236 msgid "There are no note waiting for %s." msgstr "Il n'y a pas de note en attente de %r" #: plugin.py:247 msgid "There are no note from you waiting for %s." msgstr "Il n'y a pas de note de vous en attente pour %s." #: plugin.py:275 msgid "Sent %s: <%s> %s" msgstr "Envoyé %s : <%s> %s" #~ msgid "" #~ "Used to do things later; currently, it only allows the sending of\n" #~ " nick-based notes. Do note (haha!) that these notes are *not* " #~ "private\n" #~ " and don't even pretend to be; if you want such features, consider " #~ "using the\n" #~ " Note plugin." #~ msgstr "" #~ "Utilisé pour faire des choses plus tard ; actuellement, il n'autorise que " #~ "les notes basées sur des nicks. Notez (haha !) que ces notes ne sont " #~ "*pas* privées, et qu'elle ne sont pas conçues pour l'être ; si vous " #~ "voulez une telle fonctionnalité, utilisez le plugin Note." #~ msgid "" #~ "Validate nick according to the IRC RFC 2812 spec.\n" #~ "\n" #~ " Reference: http://tools.ietf.org/rfcmarkup?" #~ "doc=2812#section-2.3.1\n" #~ "\n" #~ " Some irc clients' tab-completion feature appends 'address' " #~ "characters\n" #~ " to nick, such as ':' or ','. We try correcting for that by " #~ "trimming\n" #~ " a char off the end.\n" #~ "\n" #~ " If nick incorrigibly invalid, return False, otherwise,\n" #~ " return (possibly trimmed) nick.\n" #~ " " #~ msgstr "." #~ msgid "%s ago" #~ msgstr "il y a %s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/locales/it.po0000644000175000017500000001631214535072470017251 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-10 14:27+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:46 msgid "" "Determines the maximum number of\n" " messages to be queued for a user. If this value is 0, there is no " "maximum.\n" " " msgstr "" "Determina il numero massimo di messaggi da mettere in coda per un utente.\n" " Se questo valore è uguale a 0, non c'è un limite massimo.\n" " " #: config.py:50 msgid "" "Determines whether users will be notified in\n" " the first place in which they're seen, or in private." msgstr "" "Determina se gli utenti riceveranno una nota nel primo luogo in cui sono " "visti o in privato." #: config.py:53 msgid "" "Determines whether users will be notified upon\n" " joining any channel the bot is in, or only upon sending a message." msgstr "" "Determina se gli utenti riceveranno una nota entrando in qualsiasi canale\n" " in cui è presente il bot o solo dopo aver inviato un messaggio." #: config.py:56 msgid "" "Determines the maximum number of\n" " days that a message will remain queued for a user. After this time " "elapses,\n" " the message will be deleted. If this value is 0, there is no maximum." msgstr "" "Determina il numero massimo di giorni che un messaggio rimarrà in coda per " "un utente. Allo scadere\n" " di questo tempo il messaggio verrà cancellato. Se questo valore è uguale " "a 0, non c'è un limite massimo." #: config.py:62 msgid "" "Determines whether senders' hostname will be\n" " shown in messages (instead of just the nick)." msgstr "" #: plugin.py:51 msgid "" "\n" " Used to do things later; currently, it only allows the sending of\n" " nick-based notes. Do note (haha!) that these notes are *not* private\n" " and don't even pretend to be; if you want such features, consider using " "the\n" " Note plugin.\n" "\n" " Use the ``later tell`` command to leave a message to a user.\n" " If you sent the message by accident or want to cancel it,\n" " you can use the `later undo` command to remove the latest later,\n" " which you have sent.\n" "\n" " You can also see the people who have notes waiting for them by using\n" " the `later notes` command. If you specify a nickname in ``later notes``\n" " command, you will see the notes, which are waiting for the nickname.\n" "\n" " Privacy\n" " -------\n" "\n" " As you probably noticed from above, this plugin isn't private.\n" " Everyone can see notes sent by anyone and the laters are sent on " "channel\n" " by default and as the \"plugin help later\" says::\n" "\n" " Used to do things later; currently, it only allows the sending of " "nick-based notes. Do note (haha!) that these notes are *not* private and " "don't even pretend to be; if you want such features, consider using the Note " "plugin.\n" "\n" " The Note plugin identifies people by username instead of nickname\n" " and allows only users to send notes.\n" " The only people who are able to read notes are the sender, receiver,\n" " and the owner.\n" "\n" " " msgstr "" #: plugin.py:117 msgid "just now" msgstr "proprio ora" #: plugin.py:164 #, fuzzy msgid "" " \n" "\n" " Tells each the next time is seen. " "can\n" " contain wildcard characters, and the first matching nick will be\n" " given the note.\n" " " msgstr "" " \n" "\n" " Riferisce a la prima volta che lo vede. può " "contenere\n" " caratteri jolly, il primo che corrisponde riceverà la notifica.\n" " " #: plugin.py:174 msgid "I can't send notes to myself." msgstr "Non posso inviare note a me stesso." #: plugin.py:185 #, fuzzy msgid "These recipients' message queue are already full: %L" msgstr "La coda dei messaggi di questo utente è già piena." #: plugin.py:193 msgid "" "[]\n" "\n" " If is given, replies with what notes are waiting on ,\n" " otherwise, replies with the nicks that have notes waiting for them.\n" " " msgstr "" "[]\n" "\n" " Se è fornito, risponde con le note in coda per ,\n" " altrimenti con i nick che hanno note in coda.\n" " " #: plugin.py:204 msgid "I have no notes for that nick." msgstr "Non ho note per questo nick." #: plugin.py:209 msgid "I currently have notes waiting for %L." msgstr "Al momento non ho note in coda per %L." #: plugin.py:212 msgid "I have no notes waiting to be delivered." msgstr "Non ho note in attesa di essere consegnate." #: plugin.py:217 msgid "" "\n" "\n" " Removes the notes waiting on .\n" " " msgstr "" "\n" "\n" " Rimuove le note in coda per .\n" " " #: plugin.py:226 msgid "There were no notes for %r" msgstr "Non ci sono note per %r" #: plugin.py:231 msgid "" "\n" "\n" " Removes the latest note you sent to .\n" " " msgstr "" "\n" "\n" " Rimuove l'ultima nota inviata a .\n" " " #: plugin.py:236 msgid "There are no note waiting for %s." msgstr "Non ci sono note in attesa per %s." #: plugin.py:247 msgid "There are no note from you waiting for %s." msgstr "Non ci sono note in attesa per %s da te inviate." #: plugin.py:275 msgid "Sent %s: <%s> %s" msgstr "Inviata %s: <%s> %s" #~ msgid "" #~ "Used to do things later; currently, it only allows the sending of\n" #~ " nick-based notes. Do note (haha!) that these notes are *not* " #~ "private\n" #~ " and don't even pretend to be; if you want such features, consider " #~ "using the\n" #~ " Note plugin." #~ msgstr "" #~ "Utilizzato per fare cose in seguito; attualmente permette solo l'invio di " #~ "note basate\n" #~ " sul nick. Nota (haha!) che queste note *non* sono private e non sono " #~ "state progettate\n" #~ " per esserlo; se vuoi questa caratteristica considera l'utilizzo del " #~ "plugin Note." #~ msgid "%s ago" #~ msgstr "%s fa" #~ msgid "" #~ "Validate nick according to the IRC RFC 2812 spec.\n" #~ "\n" #~ " Reference: http://tools.ietf.org/rfcmarkup?" #~ "doc=2812#section-2.3.1\n" #~ "\n" #~ " Some irc clients' tab-completion feature appends 'address' " #~ "characters\n" #~ " to nick, such as ':' or ','. We try correcting for that by " #~ "trimming\n" #~ " a char off the end.\n" #~ "\n" #~ " If nick incorrigibly invalid, return False, otherwise,\n" #~ " return (possibly trimmed) nick.\n" #~ " " #~ msgstr "" #~ "Convalida il nick secondo la specifica IRC RFC 2812.\n" #~ "\n" #~ " Riferimento: http://tools.ietf.org/rfcmarkup?" #~ "doc=2812#section-2.3.1\n" #~ "\n" #~ " Alcuni client IRC hanno l'autocompletamento del nick che aggiunge " #~ "caratteri di\n" #~ " \"indirizzamento\" come \":\" o \",\" alla fine. Si cerca di " #~ "correggere questo\n" #~ " comportamento tagliando un carattere al fondo.\n" #~ "\n" #~ " Se il nick non è valido restituisce False, altrimenti lo riporta " #~ "(possibilmente tagliato).\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/plugin.py0000644000175000017500000002544314535072470016530 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, 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 csv import time import codecs import datetime import supybot.log as log import supybot.conf as conf import supybot.utils as utils 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('Later') class QueueIsFull(Exception): pass class Later(callbacks.Plugin): """ Used to do things later; currently, it only allows the sending of nick-based notes. Do note (haha!) that these notes are *not* private and don't even pretend to be; if you want such features, consider using the Note plugin. Use the ``later tell`` command to leave a message to a user. If you sent the message by accident or want to cancel it, you can use the `later undo` command to remove the latest later, which you have sent. You can also see the people who have notes waiting for them by using the `later notes` command. If you specify a nickname in ``later notes`` command, you will see the notes, which are waiting for the nickname. Privacy ------- As you probably noticed from above, this plugin isn't private. Everyone can see notes sent by anyone and the laters are sent on channel by default and as the "plugin help later" says:: Used to do things later; currently, it only allows the sending of nick-based notes. Do note (haha!) that these notes are *not* private and don't even pretend to be; if you want such features, consider using the Note plugin. The Note plugin identifies people by username instead of nickname and allows only users to send notes. The only people who are able to read notes are the sender, receiver, and the owner. """ def __init__(self, irc): self.__parent = super(Later, self) self.__parent.__init__(irc) self._notes = ircutils.IrcDict() self.wildcards = [] self.filename = conf.supybot.directories.data.dirize('Later.db') self._openNotes() def die(self): self._flushNotes() def _flushNotes(self): fd = utils.file.AtomicFile(self.filename) writer = csv.writer(fd) for (nick, notes) in self._notes.items(): for (time, whence, text) in notes: writer.writerow([nick, time, whence, text]) fd.close() def _openNotes(self): try: fd = codecs.open(self.filename, encoding='utf8') except EnvironmentError as e: self.log.warning('Couldn\'t open %s: %s', self.filename, e) return reader = csv.reader(fd) for (nick, time, whence, text) in reader: self._addNote(nick, whence, text, at=float(time), maximum=0) fd.close() def _timestamp(self, when): #format = conf.supybot.reply.format.time() diff = when - time.time() try: return utils.timeElapsed(diff, seconds=False) except ValueError: return _('just now') def _addNote(self, nick, whence, text, at=None, maximum=None): if at is None: at = time.time() if maximum is None: maximum = self.registryValue('maximum') try: notes = self._notes[nick] if maximum and len(notes) >= maximum: raise QueueIsFull() else: notes.append((at, whence, text)) except KeyError: self._notes[nick] = [(at, whence, text)] if set('?*!@') & set(nick): if nick not in self.wildcards: self.wildcards.append(nick) self._flushNotes() def _deleteExpired(self): expiry = self.registryValue('messageExpiry') curtime = time.time() nickremovals=[] for (nick, notes) in self._notes.items(): removals = [] for (notetime, whence, text) in notes: td = datetime.timedelta(seconds=(curtime - notetime)) if td.days > expiry: removals.append((notetime, whence, text)) for note in removals: notes.remove(note) if len(notes) == 0: nickremovals.append(nick) for nick in nickremovals: del self._notes[nick] self._flushNotes() ## Note: we call _deleteExpired from 'tell'. This means that it's possible ## for expired notes to remain in the database for longer than the maximum, ## if no tell's are called. ## However, the whole point of this is to avoid crud accumulation in the ## database, so it's fine that we only delete expired notes when we try ## adding new ones. @internationalizeDocstring def tell(self, irc, msg, args, nicks, text): """ Tells each the next time is seen. can contain wildcard characters, and the first matching nick will be given the note. """ self._deleteExpired() validnicks = [] for nick in set(nicks): # Ignore duplicates if ircutils.strEqual(nick, irc.nick): irc.error(_('I can\'t send notes to myself.')) return validnicks.append(nick) full_queues = [] for validnick in validnicks: try: self._addNote(validnick, msg.prefix, text) except QueueIsFull: full_queues.append(validnick) if full_queues: irc.error(format( _('These recipients\' message queue are already full: %L'), full_queues)) else: irc.replySuccess() tell = wrap(tell, [commalist(first('nick', 'hostmask')), 'text']) @internationalizeDocstring def notes(self, irc, msg, args, nick): """[] If is given, replies with what notes are waiting on , otherwise, replies with the nicks that have notes waiting for them. """ if nick: if nick in self._notes: notes = [self._formatNote(when, whence, note) for (when, whence, note) in self._notes[nick]] irc.reply(format('%L', notes)) else: irc.error(_('I have no notes for that nick.')) else: nicks = self._notes.keys() if nicks: utils.sortBy(ircutils.toLower, nicks) irc.reply(format(_('I currently have notes waiting for %L.'), nicks)) else: irc.error(_('I have no notes waiting to be delivered.')) notes = wrap(notes, [additional('something')]) @internationalizeDocstring def remove(self, irc, msg, args, nick): """ Removes the notes waiting on . """ try: del self._notes[nick] self._flushNotes() irc.replySuccess() except KeyError: irc.error(_('There were no notes for %r') % nick) remove = wrap(remove, [('checkCapability', 'admin'), 'something']) @internationalizeDocstring def undo(self, irc, msg, args, nick): """ Removes the latest note you sent to . """ if nick not in self._notes: irc.error(_('There are no note waiting for %s.') % nick) return self._notes[nick].reverse() for note in self._notes[nick]: if ircutils.nickFromHostmask(note[1]) == msg.nick: self._notes[nick].remove(note) if len(self._notes[nick]) == 0: del self._notes[nick] self._flushNotes() irc.replySuccess() return irc.error(_('There are no note from you waiting for %s.') % nick) undo = wrap(undo, ['something']) def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return notes = self._notes.pop(msg.nick, []) # Let's try wildcards. removals = [] for wildcard in self.wildcards: if ircutils.hostmaskPatternEqual(wildcard, msg.prefix): removals.append(wildcard) notes.extend(self._notes.pop(wildcard)) for removal in removals: self.wildcards.remove(removal) if notes: old_repliedto = msg.repliedTo irc = callbacks.SimpleProxy(irc, msg) private = self.registryValue('private') for (when, whence, note) in notes: s = self._formatNote(when, whence, note) irc.reply(s, private=private, prefixNick=not private) self._flushNotes() msg.tag('repliedTo', old_repliedto) def _formatNote(self, when, whence, note): if not self.registryValue('format.senderHostname'): whence = ircutils.nickFromHostmask(whence) return _('Sent %s: <%s> %s') % (self._timestamp(when), whence, note) def doJoin(self, irc, msg): if self.registryValue('tellOnJoin'): self.doPrivmsg(irc, msg) Later = internationalizeDocstring(Later) Class = Later # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Later/test.py0000644000175000017500000001433014535072470016202 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import supybot.conf as conf import time class LaterTestCase(ChannelPluginTestCase): plugins = ('Later',) def testLaterWorksTwice(self): self.assertNotError('later tell foo bar') self.assertNotError('later tell foo baz') def testLaterRemove(self): self.assertNotError('later tell foo 1') self.assertNotError('later tell bar 1') self.assertRegexp('later notes', 'bar.*foo') self.assertNotError('later remove bar') self.assertNotRegexp('later notes', 'bar.*foo') self.assertRegexp('later notes', 'foo') def testLaterUndo(self): self.assertNotError('later tell foo 1') self.assertNotError('later tell bar 1') self.assertRegexp('later notes', 'bar.*foo') self.assertNotError('later undo foo') self.assertNotRegexp('later notes', 'bar.*foo') self.assertRegexp('later notes', 'bar') def testNickValidation(self): origconf = conf.supybot.protocols.irc.strictRfc() conf.supybot.protocols.irc.strictRfc.setValue('True') self.assertError('later tell 1foo bar') self.assertError('later tell foo$moo zoob') conf.supybot.protocols.irc.strictRfc.setValue(origconf) def testWildcard(self): self.assertNotError('later tell foo* stuff') self.assertNotError('later tell bar,baz more stuff') self.assertRegexp('later notes', 'bar.*foo') testPrefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) m = self.getMsg(' ') self.assertEqual(str(m).strip(), 'PRIVMSG #test :foo: Sent just now: stuff') def testHostmask(self): self.assertNotError('later tell foo*!*@baz stuff') self.assertNotError('later tell bar,baz more stuff') self.assertRegexp('later notes', 'bar.*foo') testPrefix = 'foo!bar@baz2' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) m = self.getMsg(' ') self.assertEqual(m, None) testPrefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) m = self.getMsg(' ') self.assertEqual(str(m).strip(), 'PRIVMSG #test :foo: Sent just now: stuff') def testNoteExpiry(self): cb = self.irc.getCallback('Later') # add a note 40 days in the past cb._addNote('foo', 'test', 'some stuff', at=(time.time() - 3456000)) self.assertRegexp('later notes', 'foo') self.assertNotError('later tell moo stuff') self.assertNotRegexp('later notes', 'foo') self.assertRegexp('later notes', 'moo') def testNoteSend(self): self.assertNotError('later tell foo stuff') self.assertNotError('later tell bar,baz more stuff') self.assertRegexp('later notes', 'bar.*foo') testPrefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) m = self.getMsg(' ') self.assertEqual(str(m).strip(), 'PRIVMSG #test :foo: Sent just now: stuff') self.assertNotRegexp('later notes', 'foo') self.assertRegexp('later notes', 'bar') self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix='baz!baz@qux')) m = self.getMsg(' ') self.assertEqual(str(m).strip(), 'PRIVMSG #test :baz: Sent just now: more stuff') real_time = time.time def fake_time(): return real_time() + 62 time.time = fake_time self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix='bar!baz@qux')) m = self.getMsg(' ') self.assertEqual(str(m).strip(), 'PRIVMSG #test :bar: Sent 1 minute ago: more stuff') time.time = real_time def testSenderHostname(self): self.assertNotError('later tell foo stuff') testPrefix = 'foo!bar@baz' with conf.supybot.plugins.Later.format.senderHostname.context(True): self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) m = self.getMsg(' ') self.assertEqual(str(m).strip(), 'PRIVMSG #test :foo: Sent just now: <%s> stuff' % self.prefix) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Limiter/0000755000175000017500000000000014535072535015210 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/__init__.py0000644000175000017500000000514614535072470017325 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This plugin sets channel limits (MODE +l) based on ``plugins.Limiter.MaximumExcess`` plus the current number of users in the channel. This is useful to prevent flood attacks. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/config.py0000644000175000017500000000610514535072470017027 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Limiter') 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('Limiter', True) Limiter = conf.registerPlugin('Limiter') conf.registerChannelValue(Limiter, 'enable', registry.Boolean(False, _("""Determines whether the bot will maintain the channel limit to be slightly above the current number of people in the channel, in order to make clone/drone attacks harder."""))) conf.registerChannelValue(Limiter, 'minimumExcess', registry.PositiveInteger(5, _("""Determines the minimum number of free spots that will be saved when limits are being enforced. This should always be smaller than supybot.plugins.Limiter.limit.maximumExcess."""))) conf.registerChannelValue(Limiter, 'maximumExcess', registry.PositiveInteger(10, _("""Determines the maximum number of free spots that will be saved when limits are being enforced. This should always be larger than supybot.plugins.Limiter.limit.minimumExcess."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Limiter/locales/0000755000175000017500000000000014535072535016632 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/locales/fi.po0000644000175000017500000000517514535072470017576 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-26 20:28+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: config.py:47 msgid "" "Determines whether the bot will maintain the\n" " channel limit to be slightly above the current number of people in the\n" " channel, in order to make clone/drone attacks harder." msgstr "" "Määrittää ylläpitääkö botti kanava rajoitusta\n" " huomattavasti korkeammalla kuin nykyinen ihmisten määrä kanavalla\n" " tehdäkseen klooni/botti hyökkäykset vaikeammiksi." #: config.py:51 msgid "" "Determines the minimum number of free\n" " spots that will be saved when limits are being enforced. This should\n" " always be smaller than supybot.plugins.Limiter.limit.maximumExcess." msgstr "" "Määrittää minimimäärän vapaita paikkoja, jotka\n" " säästetään, kun rajajoja pakotetaan. Tämän pitäisi aina olla pienempi " "kuin\n" " supybot.plugins.Limiter.limit.maximumExcess." #: config.py:55 msgid "" "Determines the maximum number of free spots\n" " that will be saved when limits are being enforced. This should always " "be\n" " larger than supybot.plugins.Limiter.limit.minimumExcess." msgstr "" "Määrittää maksimi määrän vapaita paikkoja, jotka säästetään\n" " kun rajoja pakotetaan. Tämän pitäisi aina olla suurempi kuin\n" " supybot.plugins.Limiter.limit.minimumExcess." #: plugin.py:40 msgid "" "In order to use this plugin, its config values need to be properly\n" " setup. supybot.plugins.Limiter.enable needs to be set to True and\n" " supybot.plugins.Limiter.{maximumExcess,minimumExcess} should be set to\n" " values appropriate to your channel (if the defaults aren't " "satisfactory).\n" " Once these are set, and someone enters/leaves the channel, Supybot will\n" " start setting the proper +l modes.\n" " " msgstr "" "Käyttääkseen tätä lisäosaa, sen asetusarvot täytyy asettaa\n" " kunnolla. supybot.plugins.Limiter.enable täytyy asettaa todeksi (\"True" "\") ja\n" " supybot.plugins.Limiter.{maximumExcess,minimumExcess} \n" " arvot kanavallesi sopiviksi (jos oletukset eivät kelpaa).\n" " Kun nämä on asetettu, ja joku tulee/lähtee kanavalta, Supybotti alkaa\n" " asettamaan oikeita +l tiloja.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/locales/fr.po0000644000175000017500000000520014535072470017574 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:47 msgid "" "Determines whether the bot will maintain the\n" " channel limit to be slightly above the current number of people in the\n" " channel, in order to make clone/drone attacks harder." msgstr "" "Détermine si le bot maintiendra la limite de canal pour être juste au-dessus " "du nombre actuel de personnes, pour rendre les attaques de clones plus " "difficiles" #: config.py:51 msgid "" "Determines the minimum number of free\n" " spots that will be saved when limits are being enforced. This should\n" " always be smaller than supybot.plugins.Limiter.limit.maximumExcess." msgstr "" "Détermine le nombre minimum de 'places' libres à avoir, c'est à dire que la " "limite sera changées lorsque ce nombre sera atteint. Ce doit toujours être " "plus petit que supybot.plugins.Limiter.limit.maximumExcess." #: config.py:55 msgid "" "Determines the maximum number of free spots\n" " that will be saved when limits are being enforced. This should always " "be\n" " larger than supybot.plugins.Limiter.limit.minimumExcess." msgstr "" "Détermine le nombre maximum de 'places' libre, c'est à dire le nombre de " "place qu'il y aura juste après chaque changement de limite. Ce doit toujours " "être plus grand que supybot.plugins.Limiter.limit.minimumExcess." #: plugin.py:40 msgid "" "In order to use this plugin, its config values need to be properly\n" " setup. supybot.plugins.Limiter.enable needs to be set to True and\n" " supybot.plugins.Limiter.{maximumExcess,minimumExcess} should be set to\n" " values appropriate to your channel (if the defaults aren't " "satisfactory).\n" " Once these are set, and someone enters/leaves the channel, Supybot will\n" " start setting the proper +l modes.\n" " " msgstr "" "Pour utiliser ce plugin, ses variables de configuration doivent être " "configurées proprement. supybot.plugins.Limiter.enable doit être défini à " "True, et supybot.plugins.Limiter.{maximumExcess,minimumExcess} doivent être " "définis à la valeur appropriée piur votre canal (si les valeurs par défaut " "ne satisfont pas). Une fois qu'elles sont définies, et que quelqu'un entrera/" "partira du canal, Supybot commencera à définir les modes +l." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/locales/hu.po0000644000175000017500000000524414535072470017611 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Limiter\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-09-01 18:12+0200\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:47 msgid "" "Determines whether the bot will maintain the\n" " channel limit to be slightly above the current number of people in the\n" " channel, in order to make clone/drone attacks harder." msgstr "" "Meghatározza, hogy a bot tartsa-e a csatorna korlátját kicsivel a jelenlegi " "emberek számával a csatornában, hogy a klóntámadásokat nehezebbé tegye." #: config.py:51 msgid "" "Determines the minimum number of free\n" " spots that will be saved when limits are being enforced. This should\n" " always be smaller than supybot.plugins.Limiter.limit.maximumExcess." msgstr "" "Meghatározza a szabad helyek minimum számát, amelyek meg lesznek mentve " "amikor a korlátok kényszerítve vannak. Ennek mindig kisebbnek kell lennie a " "supybot.plugins.Limiter.limit.maximumExcess-nél." #: config.py:55 msgid "" "Determines the maximum number of free spots\n" " that will be saved when limits are being enforced. This should always " "be\n" " larger than supybot.plugins.Limiter.limit.minimumExcess." msgstr "" "Meghatározza a szabad helyek maximum számát, amelyek meg lesznek mentve " "amikor a korlátok kényszerítve vannak. Ennek mindig nagyobbnak kell lennie a " "supybot.plugins.Limiter.limit.minimumExcess-nél." #: plugin.py:40 msgid "" "In order to use this plugin, its config values need to be properly\n" " setup. supybot.plugins.Limiter.enable needs to be set to True and\n" " supybot.plugins.Limiter.{maximumExcess,minimumExcess} should be set to\n" " values appropriate to your channel (if the defaults aren't " "satisfactory).\n" " Once these are set, and someone enters/leaves the channel, Supybot will\n" " start setting the proper +l modes.\n" " " msgstr "" "A bővítmény használatához a konfigurációs értékeinek rendesen be kell " "lenniük állítva. A supybot.plugins.Limiter.enable True-ra kell, hogy legyen " "állítva, és a supybot.plugins.Limiter.{maximumExcess,minimumExcess}-nek a " "csatornának megfelelő értékekre kell lenniük állítva (ha az alapértelmezés " "nem kielégítő). Ahogy ezek be vannak állítva, és valaki be-/kilép a " "csatornáról/-ba, a Supybot elkezdi beállítani a megfelelő +l módokat." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/locales/it.po0000644000175000017500000000501514535072470017605 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-15 13: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" #: config.py:47 msgid "" "Determines whether the bot will maintain the\n" " channel limit to be slightly above the current number of people in the\n" " channel, in order to make clone/drone attacks harder." msgstr "" "Determina se il bot manterrà il limite del canale appena al di sopra\n" " del numero di persone attualmente presenti, in modo da rendere più\n" " difficili gli attacchi di cloni o droni." #: config.py:51 msgid "" "Determines the minimum number of free\n" " spots that will be saved when limits are being enforced. This should\n" " always be smaller than supybot.plugins.Limiter.limit.maximumExcess." msgstr "" "Determina il numero minimo di posti liberi che verrà salvato quando saranno\n" " imposti i limiti. Ciò non dovrebbe essere inferiore al valore di\n" " supybot.plugins.Limiter.limit.maximumExcess." #: config.py:55 msgid "" "Determines the maximum number of free spots\n" " that will be saved when limits are being enforced. This should always " "be\n" " larger than supybot.plugins.Limiter.limit.minimumExcess." msgstr "" "Determina il numero massimo di posti liberi che verrà salvato quando " "saranno\n" " imposti i limiti. Ciò dovrebbe essere sempre maggiore del valore di\n" " supybot.plugins.Limiter.limit.minimumExcess." #: plugin.py:40 msgid "" "In order to use this plugin, its config values need to be properly\n" " setup. supybot.plugins.Limiter.enable needs to be set to True and\n" " supybot.plugins.Limiter.{maximumExcess,minimumExcess} should be set to\n" " values appropriate to your channel (if the defaults aren't " "satisfactory).\n" " Once these are set, and someone enters/leaves the channel, Supybot will\n" " start setting the proper +l modes.\n" " " msgstr "" "Per utilizzare questo plugin, è necessario configurare correttamente i " "valori.\n" " supybot.plugins.Limiter.enable deve essere impostato a True e\n" " supybot.plugins.Limiter.{maximumExcess,minimumExcess} va impostato con " "i\n" " valori appropriati al canale (se i predefiniti non sono soddisfacenti).\n" " Una volta definiti, e qualcuno entra o esce dal canale, Supybot " "imposterà\n" " il mode +l corretto.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/plugin.py0000644000175000017500000000711114535072470017056 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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.commands import * import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Limiter') class Limiter(callbacks.Plugin): """In order to use this plugin, its config values need to be properly setup. supybot.plugins.Limiter.enable needs to be set to True and supybot.plugins.Limiter.{maximumExcess,minimumExcess} should be set to values appropriate to your channel (if the defaults aren't satisfactory). Once these are set, and someone enters/leaves the channel, Supybot will start setting the proper +l modes. """ def _enforce(self, irc, limit): irc.queueMsg(limit) irc.noReply() def _enforceLimit(self, irc, channel): if self.registryValue('enable', channel, irc.network): maximum = self.registryValue('maximumExcess', channel, irc.network) minimum = self.registryValue('minimumExcess', channel, irc.network) assert maximum > minimum currentUsers = len(irc.state.channels[channel].users) currentLimit = irc.state.channels[channel].modes.get('l', 0) if currentLimit - currentUsers < minimum: self._enforce(irc, ircmsgs.limit(channel,currentUsers+maximum)) elif currentLimit - currentUsers > maximum: self._enforce(irc, ircmsgs.limit(channel,currentUsers+minimum)) def doJoin(self, irc, msg): if not ircutils.strEqual(msg.nick, irc.nick): irc = callbacks.SimpleProxy(irc, msg) self._enforceLimit(irc, msg.channel) doPart = doJoin doKick = doJoin def doQuit(self, irc, msg): for channel in msg.tagged('channels'): self._enforceLimit(irc, channel) Limiter = internationalizeDocstring(Limiter) Class = Limiter # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Limiter/test.py0000644000175000017500000000546714535072470016553 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class LimiterTestCase(ChannelPluginTestCase): plugins = ('Limiter',) config = {'supybot.plugins.Limiter.enable': True} def testEnforceLimit(self): origMin = conf.supybot.plugins.Limiter.minimumExcess() origMax = conf.supybot.plugins.Limiter.maximumExcess() try: conf.supybot.plugins.Limiter.minimumExcess.setValue(5) conf.supybot.plugins.Limiter.maximumExcess.setValue(10) self.irc.feedMsg(ircmsgs.join('#foo', prefix='foo!root@host')) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.limit('#foo', 1+10)) self.irc.feedMsg(ircmsgs.join('#foo', prefix='bar!root@host')) m = self.irc.takeMsg() self.assertIsNone(m) conf.supybot.plugins.Limiter.maximumExcess.setValue(7) self.irc.feedMsg(ircmsgs.part('#foo', prefix='bar!root@host')) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.limit('#foo', 1+5)) finally: conf.supybot.plugins.Limiter.minimumExcess.setValue(origMin) conf.supybot.plugins.Limiter.maximumExcess.setValue(origMax) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/LogToIrc/0000755000175000017500000000000014535072535015265 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/LogToIrc/__init__.py0000644000175000017500000000463114535072470017400 0ustar00valval# -*- coding:utf-8 -*- ### # Copyright (c) 2004, Stéphan Kochen # Copyright (c) 2021, 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. ### """ Allows for sending the bot's logging output to channels or users. """ import supybot import supybot.world as world import importlib __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you\'re keeping the plugin in CVS or some similar system. __version__ = "%%VERSION%%" # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from . import handler from importlib import reload importlib.reload(handler) # In case we're being reloaded. importlib.reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/LogToIrc/config.py0000644000175000017500000001277314535072470017114 0ustar00valval### # Copyright (c) 2004, Stéphan Kochen # Copyright (c) 2021, 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 logging import supybot.log as log import supybot.conf as conf import supybot.ircutils as ircutils import supybot.registry as registry from .handler import _ircHandler class IrcLogLevel(log.ValidLogLevel): """Value must be one of INFO, WARNING, ERROR, or CRITICAL.""" minimumLevel = logging.INFO def setValue(self, v): log.ValidLogLevel.setValue(self, v) _ircHandler.setLevel(self()) class ValidChannelOrNick(registry.String): """Value must be a valid channel or a valid nick.""" def setValue(self, v): if not (ircutils.isNick(v) or ircutils.isChannel(v)): self.error() registry.String.setValue(self, v) class Targets(registry.SpaceSeparatedListOfStrings): Value = ValidChannelOrNick conf.registerPlugin('LogToIrc') conf.registerChannelValue(conf.supybot.plugins.LogToIrc, 'level', IrcLogLevel(logging.WARNING, """Determines what the minimum priority level logged will be to IRC. See supybot.log.level for possible values. DEBUG is disabled due to the large quantity of output."""), opSettable=False) conf.registerNetworkValue(conf.supybot.plugins.LogToIrc, 'targets', Targets([], """Space-separated list of channels/nicks the bot should log to. If no channels/nicks are set, this plugin will effectively be turned off.""")) conf.registerGlobalValue(conf.supybot.plugins.LogToIrc, 'networks', registry.SpaceSeparatedSetOfStrings([], """Determines what networks the bot should log to. If no networks are set, the bot will log on one network (whichever happens to be around at the time it feels like logging).""")) conf.registerNetworkValue(conf.supybot.plugins.LogToIrc, 'channelModesRequired', registry.String('s', """Determines what channel modes a channel will be required to have for the bot to log to the channel. If this string is empty, no modes will be checked.""")) conf.registerGlobalValue(conf.supybot.plugins.LogToIrc, 'userCapabilityRequired', registry.String('owner', """Determines what capability is required for the bot to log to in private messages to the user. If this is empty, there will be no capability that's checked.""")) conf.registerChannelValue(conf.supybot.plugins.LogToIrc, 'color', registry.Boolean(False, """Determines whether the bot's logs to IRC will be colorized with mIRC colors.""")) conf.registerChannelValue(conf.supybot.plugins.LogToIrc, 'notice', registry.Boolean(False, """Determines whether the bot's logs to IRC will be sent via NOTICE instead of PRIVMSG. Channels will always be PRIVMSGed, regardless of this variable; NOTICEs will only be used if this variable is True and the target is a nick, not a channel.""")) def configure(advanced): from supybot.questions import something, anything, yn, output output("""Here you can set which channels and who the bot has to send log messages to. Note that by default in order to log to a channel the channel has to have mode +s set. Logging to a user requires the user to have the Owner capability.""") targets = '' while not targets: try: targets = anything('Which channels or users would you like to ' 'send log messages to?') conf.supybot.plugins.LogToIrc.targets.set(targets) except registry.InvalidRegistryValue as e: output(str(e)) targets = '' colorized = yn('Would you like these messages to be colored?') conf.supybot.plugins.LogToIrc.color.setValue(colorized) if advanced: level = '' while not level: try: level = something('What would you like the minimum priority ' 'level to be which will be logged to IRC?') conf.supybot.plugins.LogToIrc.level.set(level) except registry.InvalidRegistryValue as e: output(str(e)) level = '' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/LogToIrc/handler.py0000644000175000017500000001410514535072470017253 0ustar00valval### # Copyright (c) 2004, Stéphan Kochen # Copyright (c) 2021, 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. ### """This module MUST NOT be reloaded after config.py, because it would cause the log level to be unset, ie. to log EVERYTHING, and that's bad """ import logging import supybot.log as log import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils import supybot.world as world import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.registry as registry class IrcHandler(logging.Handler): def emit(self, record): config = conf.supybot.plugins.LogToIrc try: s = utils.str.normalizeWhitespace(self.format(record)) except: self.handleError(record) return for irc in world.ircs: network = irc.network if irc.driver is None: continue for target in config.targets.getSpecific(network=irc.network)(): msgmaker = ircmsgs.privmsg if config.notice.getSpecific(target, network)() \ and not irc.isChannel(target): msgmaker = ircmsgs.notice msg = msgmaker(target, s) try: if not irc.driver.connected: continue except AttributeError as e: import traceback traceback.print_exc() continue networks = conf.supybot.plugins.LogToIrc.networks() if networks and irc.network not in networks: continue msgOk = True level = config.level.getSpecific( network=network, channel=target)() if level > record.levelno: msgOk = False if irc.isChannel(target): if target in irc.state.channels: channel = irc.state.channels[target] modes = config.channelModesRequired.getSpecific( network=network)() for modeChar in modes: if modeChar not in channel.modes: msgOk = False else: msgOk = False else: capability = config.userCapabilityRequired.getSpecific() if capability: try: hostmask = irc.state.nicksToHostmasks[target] except KeyError: msgOk = False continue if not ircdb.checkCapability(hostmask, capability): msgOk = False if msgOk: # We use sendMsg here because queueMsg can cause some # WARNING logs, which might be sent here, which might # cause some more WARNING logs, etc. and that would be # baaaaaad. irc.sendMsg(msg) else: print('*** Not sending to %s @ %s' % (utils.str.quoted(target), irc.network)) class IrcFormatter(log.Formatter): def formatException(self, xxx_todo_changeme): (E, e, tb) = xxx_todo_changeme L = [utils.exnToString(e), '::'] frames = utils.stackTrace(frame=tb.tb_frame).split() L.extend(frames) del tb while sum(map(len, L)) > 350: L.pop() return ' '.join(L) class ColorizedIrcFormatter(IrcFormatter): def formatException(self, xxx_todo_changeme1): (E, e, tb) = xxx_todo_changeme1 if conf.supybot.plugins.LogToIrc.color(): s = IrcFormatter.formatException(self, (E, e, tb)) return ircutils.mircColor(s, fg='red') else: return IrcFormatter.formatException(self, (E, e, tb)) def format(self, record, *args, **kwargs): s = IrcFormatter.format(self, record, *args, **kwargs) if conf.supybot.plugins.LogToIrc.color(): if record.levelno == logging.CRITICAL: s = ircutils.bold(ircutils.bold(s)) elif record.levelno == logging.ERROR: s = ircutils.mircColor(s, fg='red') elif record.levelno == logging.WARNING: s = ircutils.mircColor(s, fg='yellow') return s _ircHandler = IrcHandler() _formatString = '%(name)s: %(levelname)s %(message)s' _ircFormatter = ColorizedIrcFormatter(_formatString) _ircHandler.setFormatter(_ircFormatter) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/LogToIrc/plugin.py0000644000175000017500000000522614535072470017140 0ustar00valval# -*- coding:utf-8 -*- ### # Copyright (c) 2004, Stéphan Kochen # Copyright (c) 2021, 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. ### """ Allows for sending the bot's logging output to channels or users. """ import supybot.plugins as plugins import logging import os.path import supybot.log as log import supybot.conf as conf import supybot.ircdb as ircdb import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from .handler import _ircHandler class LogToIrc(callbacks.Privmsg): threaded = True def __init__(self, irc): self.__parent = super(LogToIrc, self) self.__parent.__init__(irc) log._logger.addHandler(_ircHandler) def die(self): self.__parent.die() log._logger.removeHandler(_ircHandler) def do376(self, irc, msg): targets = self.registryValue('targets') for target in targets: if irc.isChannel(target): networkGroup = conf.supybot.networks.get(irc.network) irc.queueMsg(networkGroup.channels.join(target)) do377 = do422 = do376 Class = LogToIrc # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/LogToIrc/test.py0000644000175000017500000000336514535072470016623 0ustar00valval### # Copyright (c) 2004, Stéphan Kochen # Copyright (c) 2021, 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 * class LogToIrcTestCase(PluginTestCase): plugins = ('LogToIrc',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Math/0000755000175000017500000000000014535072535014474 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Math/__init__.py0000644000175000017500000000474014535072470016610 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This plugin provides a calculator, converter, a list of units and other useful math functions. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {supybot.Author('Keith Jones', 'kmj', ''): ['convert']} from . import config from . import plugin from importlib import reload from .local import convertcore reload(plugin) # In case we're being reloaded. reload(convertcore) if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Math/config.py0000644000175000017500000000472414535072470016320 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Math') 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('Math', True) Math = conf.registerPlugin('Math') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Math, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Math/local/0000755000175000017500000000000014535072535015566 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Math/local/__init__.py0000644000175000017500000000007214535072470017674 0ustar00valval# Stub so local is a module, used for third-party modules ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Math/local/convertcore.py0000644000175000017500000013245214535072470020476 0ustar00valval#**************************************************************************** # This file has been modified from its original version. It has been # formatted to fit your irc bot. # # The original version is a nifty PyQt application written by Douglas Bell, # available at http://convertall.bellz.org/ # # Below is the original copyright. Doug Bell rocks. # The hijacker is Keith Jones, and he has no bomb in his shoe. # #**************************************************************************** import re import copy unitData = \ """ #***************************************************************************** #units.dat, the units data file, version 0.6.2 # # ConvertAll, a units conversion program # Copyright (C) 2016, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, Version 2. This program is # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. #***************************************************************************** # # Units are defined by an optional quantity and an equivalent unit or unit # combination. A Python expression may be used for the quantity, but is # restricted to using only the following operators: *, /, +, -. # Beware of integer division truncation: be sure to use a float for at least # one of the values. # # The unit type must be placed in square brackets before a set of units. The # first comment after the equivalent unit will be put in parenthesis after the # unit name (usually used to give the full name of an abbreviated unit). The # next comment will be used in the program list's comment column; later # comments and full line comments are ignored. # # Non-linear units are indicated with an equivalent unit in square brackets, # followed by either equations or equivalency lists for the definition. For # equations, two are given, separated by a ';'. Both are functions of "x", the # first going from the unit to the equivalent unit and the second one in # reverse. Any valid Python expression returning a float (including the # functions in the math module) should work. The equivalency list is a Python # list of tuples giving points for linear interpolation. # # All units must reduce to primitive units, which are indicated by an # equivalent unit starting with '!'. A special "unitless" primitve unit # (usualty called "unit") has '!!' for an equivalent unit. Circular references # must also be avoided. # # Primitive units: kg, m, s, K, A, mol, cd, rad, sr, bit, unit # ############################################################################## # # mass units # [mass] kg = ! # kilogram kilogram = kg key = kg # # drug slang hectogram = 100 gram dekagram = 10 gram gram = 0.001 kg g = gram # gram decigram = 0.1 gram centigram = 0.01 gram milligram = 0.001 gram mg = milligram # milligram microgram = 0.001 mg tonne = 1000 kg # # metric metric ton = tonne megagram = tonne kilotonne = 1000 tonne # # metric gigagram = 1e9 gram teragram = 1e12 gram carat = 0.2 gram ct = carat # carat amu = 1.66053873e-27 kg # atomic mass atomic mass unit = amu pound = 0.45359237 kg # # avoirdupois lb = pound # pound # avoirdupois lbm = pound # pound # avoirdupois ounce = 1/16.0 pound # # avoirdupois oz = ounce # ounce # avoirdupois lid = ounce # # drug slang dram = 1/16.0 ounce # # avoirdupois pound troy = 5760 grain lb troy = pound troy # pound troy ounce troy = 1/12.0 lb troy oz troy = ounce troy # ounce troy ton = 2000 lb # # non-metric kiloton = 1000 ton # # non-metric long ton = 2240 lb # # Imperial ton imperial = long ton slug = lbf*s^2/ft stone = 14 lb grain = 1/7000.0 lb pennyweight = 24 grain hundredweight long = 112 lb # # Imperial hundredweight short = 100 lb # # US & Canada solar mass = 1.9891e30 kg # # length / distance units # [length] m = ! # meter meter = m metre = m dm = 0.1 m # decimeter decimeter = dm cm = 0.01 m # centimeter centimeter = cm mm = 0.001 m # millimeter millimeter = mm micrometer = 1e-6 m micron = micrometer nanometer = 1e-9 m nm = nanometer # nanometer dekameter = 10 m hectometer = 100 m km = 1000 m # kilometer kilometer = km megameter = 1000 km angstrom = 1e-10 m fermi = 1e-15 m # # nuclear sizes inch = 2.54 cm in = inch # inch inches = inch mil = 0.001 inch microinch = 1e-6 inch microinches = microinch foot = 12 inch ft = foot # foot feet = foot foot US survey = 1200/3937.0 m Cape foot = 1.033 foot yard = 3 ft yd = yard # yard mile = 5280 ft # # statute mile mi = mile # mile # statute mile nautical mile = 1852 m nmi = nautical mile # nautical mile mile US survey = 5280 foot US survey league = 3 mile chain = 66 ft chain US survey = 66 foot US survey link = 0.01 chain fathom = 6 ft cable = 0.1 nautical mile rod = 5.5 yard furlong = 40 rod hand = 4 inch cubit = 21.8 inch # # biblical unit point = 1/72.0 inch # # desktop publishing point pica = 12 point caliber = 1.0 inch # # bullet sizes rack unit = 1.75 in # # computing smoot = 67 inch football field = 100 yd marathon = 46145 yd mil Swedish = 10 km versta = 3500 ft # # Russian unit au = 1.49597870691e11 m # astronomical unit astronomical unit = au LD = 384400 km # lunar distance # astronomical lunar distance = LD # # astronomical distance light year = 365.25 light speed * day light minute = light speed * min light second = light speed * s parsec = 3.0856775813e16 m kiloparsec = 1000 parsec megaparsec = 1000 kiloparsec screw size = [in] 0.013*x + 0.06 ; (x - 0.06) / 0.013 \ # # Unified diameters, non-linear AWG Dia = [in] pow(92.0,(36-x)/39.0)/200.0 ; \ 36 - 39.0*log(200.0*x)/log(92.0) \ # American Wire Gauge \ # use -1, -2 for 00, 000; non-linear American Wire Gauge Dia = [in] pow(92.0,(36-x)/39.0)/200.0 ; \ 36 - 39.0*log(200.0*x)/log(92.0) \ # # use -1, -2 for 00, 000; non-linear British Std Wire Gauge = [in] [(-6, .500), (-5, .464), (-3, .400), \ (-2, .372), (3, .252), (6, .192), (10, .128), \ (14, .080), (19, .040), (23, .024), (26, .018), \ (28, .0148), (30, .0124), (39, .0052), \ (49, .0012), (50, .001)] \ # # use -1, -2 for 2/0, 3/0; non-linear standard gauge = [in] [(-5, .448350), (1, .269010), (14, .0747250), \ (16, .0597800), (17, .0538020), (20, .0358680), \ (26, .0179340), (31, .0104615), (36, .00672525), \ (38, .00597800)] # steel \ # Manufacturers Std. Gauge, non-linear zinc gauge = [in] [(1, .002), (10, .02), (15, .04), (19, .06), \ (23, .1), (24, .125), (27, .5), (28, 1)] \ # # sheet metal thickness, non-linear ring size = [in] 0.1018*x + 1.4216 ; (x - 1.4216) / 0.1018 \ # # US size, circum., non-linear shoe size mens = [in] x/3.0 + 7 + 1/3.0 ; (x - 7 - 1/3.0) * 3 \ # # US sizes, non-linear shoe size womens = [in] x/3.0 + 6 + 5/6.0 ; (x - 6 - 5/6.0) * 3 \ # # US sizes, non-linear # # time units # [time] s = ! # second sec = s # second second = s ms = 0.001 s # millisecond millisecond = ms microsecond = 1e-6 s ns = 1e-9 s # nanosecond nanosecond = ns minute = 60 s min = minute # minute hour = 60 min hr = hour # hour bell = 30 min # # naval definition watch = 4 hour watches = watch day = 24 hr week = 7 day wk = week # week fortnight = 14 day month = 1/12.0 year year = 365.242198781 day yr = year # year calendar year = 365 day decade = 10 year century = 100 year centuries = century millennium = 1000 year millennia = millennium [scheduling] man hour = 168/40.0 hour man week = 40 man hour man month = 1/12.0 man year man year = 52 man week # # temperature # [temperature] K = ! # Kelvin Kelvin = K deg K = K # Kelvin degree Kelvin = K C = [K] x + 273.15 ; x - 273.15 # Celsius # non-linear Celsius = [K] x + 273.15 ; x - 273.15 # # non-linear deg C = [K] x + 273.15 ; x - 273.15 # Celsius # non-linear degree Celsius = [K] x + 273.15 ; x - 273.15 # # non-linear R = 5/9.0 K # Rankine Rankine = R deg R = R # Rankine F = [R] x + 459.67 ; x - 459.67 # Fahrenheit # non-linear Fahrenheit = [R] x + 459.67 ; x - 459.67 # # non-linear deg F = [R] x + 459.67 ; x - 459.67 # Fahrenheit # non-linear degree Fahrenheit = [R] x + 459.67 ; x - 459.67 # # non-linear [temp. diff.] C deg = K # Celsius degree Celsius degree = C deg F deg = R # Fahrenheit deg. Fahrenheit degree = F deg # # electrical units # [current] A = ! # ampere ampere = A amp = A milliampere = 0.001 A milliamp = milliampere mA = milliampere # milliampere microampere = 0.001 mA kiloampere = 1000 A kA = kiloampere # kiloampere [charge] coulomb = A*s amp hour = A*hr mAh = 0.001 amp hour # milliamp hour milliamp hour = mAh [potential] volt = W/A V = volt # volt millivolt = 0.001 volt mV = millivolt # millivolt kilovolt = 1000 volt kV = kilovolt # kilovolt [resistance] ohm = V/A milliohm = 0.001 ohm microhm = 0.001 milliohm kilohm = 1000 ohm [conductance] siemens = A/V [capacitance] farad = coulomb/V millifarad = 0.001 farad microfarad = 0.001 millifarad nanofarad = 1e-9 farad picofarad = 1e-12 farad [magn. flux] weber = V*s Wb = weber # weber maxwell = 1e-8 Wb [inductance] henry = Wb/A H = henry # henry millihenry = 0.001 henry mH = millihenry # millihenry microhenry = 0.001 mH [flux density] tesla = Wb/m^2 T = tesla # tesla gauss = maxwell/cm^2 # # molecular units # [molecular qty] mol = ! # mole # gram mole mole = mol # # gram mole gram mole = mol kilomole = 1000 mol kmol = kilomole # kilomole pound mole = mol*lbm/gram lbmol = pound mole # pound mole [size of a mol] avogadro = gram/(amu*mol) # # Illumination units # [lum. intens.] cd = ! # candela candela = cd [luminous flux] lumen = cd * sr lm = lumen # lumen [illuminance] lux = lumen/m^2 footcandle = lumen/ft^2 metercandle = lumen/m^2 [luminance] lambert = cd/(pi*cm^2) millilambert = 0.001 lambert footlambert = cd/(pi*ft^2) # # angular units # [angle] radian = ! rad = radian # radian circle = 2 pi*radian turn = circle revolution = circle rev = revolution # revolution degree = 1/360.0 circle deg = degree # degree arc min = 1/60.0 degree # minute arc minute = arc min min arc = arc min # minute minute arc = arc min arc sec = 1/60.0 arc min # second arc second = arc sec sec arc = arc sec # second second arc = arc sec quadrant = 1/4.0 circle right angle = quadrant gradian = 0.01 quadrant # # solid angle units # [solid angle] sr = ! # steradian steradian = sr sphere = 4 pi*sr hemisphere = 1/2.0 sphere # # information units # [data] bit = ! kilobit = 1000 bit # # based on power of 10 megabit = 1000 kilobit # # based on power of 10 byte = 8 bit B = byte # byte kilobyte = 1024 byte # # based on power of 2 kB = kilobyte # kilobyte # based on power of 2 megabyte = 1024 kB # # based on power of 2 MB = megabyte # megabyte # based on power of 2 gigabyte = 1024 MB # # based on power of 2 GB = gigabyte # gigabyte # based on power of 2 terabyte = 1024 GB # # based on power of 2 TB = terabyte # terabyte # based on power of 2 petabyte = 1024 TB # # based on power of 2 PB = petabyte # petabyte # based on power of 2 kilobyte IEC std = 1000 byte # # based on power of 10 kB IEC std = kilobyte IEC std # kilobyte # based on power of 10 megabyte IEC std = 1000 kB IEC std # # based on power of 10 MB IEC std = megabyte IEC std # megabyte # based on power of 10 gigabyte IEC std = 1000 MB IEC std # # based on power of 10 GB IEC std = gigabyte IEC std # gigabyte # based on power of 10 terabyte IEC std = 1000 GB IEC std # # based on power of 10 TB IEC std = terabyte IEC std # terabyte # based on power of 10 petabyte IEC std = 1000 TB IEC std # # based on power of 10 PB IEC std = petabyte IEC std # petabyte # based on power of 10 kibibyte = 1024 byte KiB = kibibyte # kibibyte mebibyte = 1024 KiB MiB = mebibyte # mebibyte gibibyte = 1024 MiB GiB = gibibyte # gibibyte tebibyte = 1024 GiB TiB = tebibyte # tebibyte pebibyte = 1024 TiB PiB = pebibyte # pebibyte [data transfer] bps = bit/sec # bits / second kbps = 1000 bps # kilobits / sec. # based on power of 10 # # Unitless numbers # [quantity] unit = !! 1 = unit # unit pi = 3.14159265358979323846 unit pair = 2 unit hat trick = 3 unit # # sports dozen = 12 unit doz = dozen # dozen bakers dozen = 13 unit score = 20 unit gross = 144 unit great gross = 12 gross ream = 500 unit percent = 0.01 unit % = percent mill = 0.001 unit [interest rate] APR = [unit] log(1 + x/100) ; (exp(x) - 1)*100 \ # annual % rate # based on continuous compounding [concentration] proof = 1/200.0 unit # # alcohol content ppm = 1e-6 unit # parts per million parts per million = ppm ppb = 1e-9 unit # parts per billion parts per billion = ppb ppt = 1e-12 unit # parts per trillion parts per trillion = ppt karat = 1/24.0 unit # # gold purity carat gold = karat # # gold purity # # force units # [force] newton = kg*m/s^2 N = newton # newton dekanewton = 10 newton kilonewton = 1000 N kN = kilonewton # kilonewton meganewton = 1000 kN millinewton = 0.001 N dyne = cm*g/s^2 kg force = kg * gravity # kilogram f kgf = kg force # kilogram force kilogram force = kg force kp = kg force # kilopond kilopond = kg force gram force = g * gravity pound force = lbm * gravity # # avoirdupois lbf = pound force # pound force # avoirdupois ton force = ton * gravity ounce force = ounce * gravity ozf = ounce force # ounce force tonne force = tonne * gravity # # metric pdl = lbm * ft / sec^2 # poundal # Imperial force poundal = pdl # # Imperial force # # area units # [area] barn = 1e-28 m^2 # # particle physics are = 100 m^2 decare = 10 are dekare = 10 are hectare = 100 are stremma = 1000 m^2 acre = 10 chain^2 section = mile^2 township = 36 section homestead = 160 acre square perch = 30.25 yd^2 rood = 0.25 acre rai = 1600 m^2 # # Thai ngaan = 400 m^2 # # Thai circular inch = 1/4.0 pi*in^2 # # area of 1 inch circle circular mil = 1/4.0 pi*mil^2 # # area of 1 mil circle AWG Area = [in^2] pi/4*pow(pow(92.0,(36-x)/39.0)/200.0,2) ; \ 36 - 39.0*log(200.0*sqrt(x*4.0/pi))/log(92.0) \ # American Wire Gauge \ # use -1, -2 for 00, 000; non-linear American Wire Gauge Area = [in^2] pi/4*pow(pow(92.0,(36-x)/39.0)/200.0,2) ; \ 36 - 39.0*log(200.0*sqrt(x*4.0/pi))/log(92.0) \ # # use -1, -2 for 00, 000; non-linear # # volume units # [volume] cc = cm^3 # cubic centimeter cubic centimeter = cc liter = 1000 cc l = liter # liter litre = liter deciliter = 0.1 liter centiliter = 0.01 liter milliliter = cc ml = milliliter # milliliter microliter = 1e-6 liter dekaliter = 10 liter hectoliter = 100 liter kiloliter = 1000 liter kl = kiloliter # kiloliter megaliter = 1000 kiloliter gallon = 231 in^3 # # US liquid gal = gallon # gallon # US liquid quart = 1/4.0 gallon # # US liquid qt = quart # quart # US liquid pint = 1/2.0 quart # # US liquid pt = pint # pint # US liquid fluid ounce = 1/16.0 pint # # US fl oz = fluid ounce # fluid ounce # US ounce fluid = fluid ounce # # US fluid dram = 1/8.0 fluid ounce # # US minim = 1/480.0 fluid ounce # # US imperial gallon = 4.54609 liter imp gal = imperial gallon # imperial gallon gallon imperial = imperial gallon imperial quart = 1/4.0 imp gal imp qt = imperial quart # imperial quart quart imperial = imperial quart imperial pint = 1/8.0 imp gal imp pt = imperial pint # imperial pint pint imperial = imperial pint imperial fluid ounce = 1/160.0 imp gal imp fl oz = imperial fluid ounce # imperial fluid ounce imperial fluid dram = 1/8.0 imp fl oz imperial minim = 1/480.0 imp fl oz cup = 8 fl oz tablespoon = 1/16.0 cup tbsp = tablespoon # tablespoon teaspoon = 1/3.0 tbsp tsp = teaspoon # teaspoon barrel = 42 gallon bbl = barrel # barrel shot = 1.5 fl oz fifth = 1/5.0 gallon # # alcohol wine bottle = 750 ml magnum = 1.5 liter # # alcohol keg = 15.5 gallon # # beer hogshead wine = 63 gal hogshead beer = 54 gal bushel = 2150.42 in^3 peck = 1/4.0 bushel cord = 128 ft^3 board foot = ft^2*in board feet = board foot # # velocity units # [velocity] knot = nmi/hr kt = knot # knot light speed = 2.99792458e8 m/s mph = mi/hr # miles/hour kph = km/hr # kilometers/hour mach = 340.29 m/s # # speed sound at STP [rot. velocity] rpm = rev/min # rev/min rps = rev/sec # rev/sec # # flow rate units # [fluid flow] gph = gal/hr # gallons/hour gpm = gal/min # gallons/minute cfs = ft^3/sec # cu ft/second cfm = ft^3/min # cu ft/minute lpm = l/min # liter/min [gas flow] sccm = atm*cc/min # std cc/min # pressure * flow sccs = atm*cc/sec # std cc/sec # pressure * flow slpm = atm*l/min # std liter/min # pressure * flow slph = atm*l/hr # std liter/hour # pressure * flow scfh = atm*ft^3/hour # std cu ft/hour # pressure * flow scfm = atm*ft^3/min # std cu ft/min # pressure * flow # # pressure units # [pressure] Pa = N/m^2 # pascal pascal = Pa hPa = 100 Pa # hectopascal hectopascal = hPa kPa = 1000 Pa # kilopascal kilopascal = kPa MPa = 1000 kPa # megapascal megapascal = MPa GPa = 1000 MPa # gigapascal gigapascal = GPa atm = 101325 Pa # atmosphere atmosphere = atm bar = 1e5 Pa mbar = 0.001 bar # millibar millibar = mbar microbar = 0.001 mbar decibar = 0.1 bar kilobar = 1000 bar megabar = 1000 kilobar mm Hg = mm*density Hg*gravity millimeter of Hg = mm Hg torr = mm Hg micron of Hg = micron*density Hg*gravity in Hg = in*density Hg*gravity # inch of Hg inch of Hg = in Hg m water = m*density water*gravity # meter of H2O # fresh water m H2O = m water # meter of H2O # fresh water meter of water = m water # # fresh water in water = in*density water*gravity # inch of H2O # fresh water in H2O = in water # inch of H2O # fresh water inch of water = in water # # fresh water ft water = ft*density water*gravity # feet of H2O # fresh water ft H2O = ft water # feet of H20 # fresh water feet of water = ft water # # fresh water foot of head = ft water # # fresh water ft hd = ft water # foot of head # fresh water psi = lbf/in^2 # pound / sq inch pound per sq inch = psi ksi = 1000 psi # 1000 lb / sq inch # # density units # [density] density water = gram/cm^3 density sea water = 1.025 gram/cm^3 density Hg = 13.5950981 gram/cm^3 density air = 1.293 kg/m^3 # # at STP density steel = 0.283 lb/in^3 # # carbon steel density aluminum = 0.098 lb/in^3 density zinc = 0.230 lb/in^3 density brass = 0.310 lb/in^3 # # 80Cu-20Zn density copper = 0.295 lb/in^3 density iron = 0.260 lb/in^3 # # cast iron density nickel = 0.308 lb/in^3 density tin = 0.275 lb/in^3 density titanium = 0.170 lb/in^3 density silver = 0.379 lb/in^3 density nylon = 0.045 lb/in^3 density polycarbonate = 0.045 lb/in^3 # # energy units # [energy] joule = N*m J = joule # joule kilojoule = 1000 joule kJ = kilojoule # kilojoule megajoule = 1000 kilojoule gigajoule = 1000 megajoule millijoule = 0.001 joule mJ = millijoule # millijoule calorie = 4.1868 J cal = calorie # calorie kilocalorie = 1000 cal kcal = kilocalorie # kilocalorie calorie food = kilocalorie thermie = 1000 kcal Btu = cal*lb*R/(g*K) # British thermal unit British thermal unit = Btu therm = 100000 Btu erg = cm*dyne electronvolt = 1.602176462e-19 J eV = electronvolt # electronvolt kWh = kW*hour # kilowatt-hour kilowatt hour = kWh ton TNT = 4.184e9 J tonne oil equivalent = 41.868 gigajoule tonne coal equivalent = 7000000 kcal # # power units # [power] watt = J/s W = watt # watt kilowatt = 1000 W kW = kilowatt # kilowatt megawatt = 1000 kW MW = megawatt # megawatt gigawatt = 1000 MW GW = gigawatt # gigawatt milliwatt = 0.001 W horsepower = 550 ft*lbf/sec hp = horsepower # horsepower metric horsepower = 75 kgf*m/s ton refrigeration = 12000 Btu/hr MBH = 1000 Btu/hr # 1000 Btu/hr # # frequency # [frequency] hertz = unit/sec Hz = hertz # hertz millihertz = 0.001 Hz kilohertz = 1000 Hz kHz = kilohertz # kilohertz megahertz = 1000 kHz MHz = megahertz # megahertz gigahertz = 1000 MHz GHz = gigahertz # gigahertz # # radioactivity # [radioactivity] becquerel = unit/sec Bq = becquerel # becquerel curie = 3.7e10 Bq millicurie = 0.001 curie roentgen = 2.58e-4 coulomb/kg [radiation dose] gray = J/kg Gy = gray # gray centigray = 0.01 Gy rad. abs. dose = 0.01 Gy # # commonly rad sievert = J/kg # # equiv. dose millisievert = 0.001 sievert # # equiv. dose Sv = sievert # sievert # equiv. dose rem = 0.01 Sv # # roentgen equiv mammal millirem = 0.001 rem # # roentgen equiv mammal # # viscosity # [dyn viscosity] poise = g/(cm*s) P = poise # poise centipoise = 0.01 poise cP = centipoise # centipoise [kin viscosity] stokes = cm^2/s St = stokes # stokes centistokes = 0.01 stokes cSt = centistokes # centistokes # # misc. units # [acceleration] gravity = 9.80665 m/s^2 galileo = cm/s^2 [constant] gravity constant = 6.673e-11 N*m^2/kg^2 gas constant = 8.314472 J/(mol*K) # R [fuel consumpt.] mpg = mi/gal # miles/gallon mpg imp = mi/gallon imperial # miles/gallon imp liter per 100 km = [mpg] 3.785411784 / (x * 0.01609344) ; \ 3.785411784 / (x * 0.01609344) # # non-linear [permeability] darcy = 1 cm^2*centipoise/atm/s millidarcy = 0.001 darcy """ class UnitGroup: "Stores, updates and converts a group of units" maxDecPlcs = 8 def __init__(self, unitData, option): self.unitData = unitData self.option = option self.unitList = [] self.currentNum = 0 self.factor = 1.0 self.reducedList = [] self.linear = 1 def update(self, text, cursorPos=None): "Decode user entered text into units" self.unitList = self.parseGroup(text) if cursorPos != None: self.updateCurrentUnit(text, cursorPos) else: self.currentNum = len(self.unitList) - 1 def updateCurrentUnit(self, text, cursorPos): "Set current unit number" self.currentNum = len(re.findall('[\*/]', text[:cursorPos])) def currentUnit(self): "Return current unit if its a full match, o/w None" if self.unitList and self.unitList[self.currentNum].equiv: return self.unitList[self.currentNum] return None def currentPartialUnit(self): "Return unit with at least a partial match, o/w None" if not self.unitList: return None return self.unitData.findPartialMatch(self.unitList[self.currentNum]\ .name) def currentSortPos(self): "Return unit near current unit for sorting" if not self.unitList: return self.unitData[self.unitData.sortedKeys[0]] return self.unitData.findSortPos(self.unitList[self.currentNum]\ .name) def replaceCurrent(self, unit): "Replace the current unit with unit" if self.unitList: exp = self.unitList[self.currentNum].exp self.unitList[self.currentNum] = copy.copy(unit) self.unitList[self.currentNum].exp = exp else: self.unitList.append(copy.copy(unit)) def completePartial(self): "Replace a partial unit with a full one" if self.unitList and not self.unitList[self.currentNum].equiv: text = self.unitList[self.currentNum].name unit = self.unitData.findPartialMatch(text) if unit: exp = self.unitList[self.currentNum].exp self.unitList[self.currentNum] = copy.copy(unit) self.unitList[self.currentNum].exp = exp def moveToNext(self, upward): "Replace unit with adjacent one based on match or sort position" unit = self.currentSortPos() num = self.unitData.sortedKeys.index(unit.name.\ replace(' ', '')) \ + (upward and -1 or 1) if 0 <= num < len(self.unitData.sortedKeys): self.replaceCurrent(self.unitData[self.unitData.sortedKeys[num]]) def addOper(self, mult): "Add new operator & blank unit after current, * if mult is true" if self.unitList: self.completePartial() prevExp = self.unitList[self.currentNum].exp self.currentNum += 1 self.unitList.insert(self.currentNum, Unit('')) if (not mult and prevExp > 0) or (mult and prevExp < 0): self.unitList[self.currentNum].exp = -1 def changeExp(self, newExp): "Change the current unit's exponent" if self.unitList: self.completePartial() if self.unitList[self.currentNum].exp > 0: self.unitList[self.currentNum].exp = newExp else: self.unitList[self.currentNum].exp = -newExp def clearUnit(self): "Remove units" self.unitList = [] def parseGroup(self, text): "Return list of units from text string" unitList = [] parts = [part.strip() for part in re.split('([\*/])', text)] numerator = 1 while parts: unit = self.parseUnit(parts.pop(0)) if not numerator: unit.exp = -unit.exp if parts and parts.pop(0) == '/': numerator = not numerator unitList.append(unit) return unitList def parseUnit(self, text): "Return a valid or invalid unit with exponent from a text string" parts = text.split('^', 1) exp = 1 if len(parts) > 1: # has exponent try: exp = int(parts[1]) except ValueError: if parts[1].lstrip().startswith('-'): exp = -Unit.partialExp # tmp invalid exp else: exp = Unit.partialExp unitText = parts[0].strip().replace(' ', '') unit = copy.copy(self.unitData.get(unitText, None)) if not unit and unitText and unitText[-1] == 's' and not \ self.unitData.findPartialMatch(unitText): # check for plural unit = copy.copy(self.unitData.get(unitText[:-1], None)) if not unit: #unit = Unit(parts[0].strip()) # tmp invalid unit raise UnitDataError('%s is not a valid unit.' % (unitText)) unit.exp = exp return unit def unitString(self, unitList=None): "Return the full string for this group or a given group" if unitList == None: unitList = self.unitList[:] fullText = '' if unitList: fullText = unitList[0].unitText(0) numerator = 1 for unit in unitList[1:]: if (numerator and unit.exp > 0) \ or (not numerator and unit.exp < 0): fullText = '%s * %s' % (fullText, unit.unitText(1)) else: fullText = '%s / %s' % (fullText, unit.unitText(1)) numerator = not numerator return fullText def groupValid(self): "Return 1 if all unitself.reducedLists are valid" if not self.unitList: return 0 for unit in self.unitList: if not unit.unitValid(): return 0 return 1 def reduceGroup(self): "Update reduced list of units and factor" self.linear = 1 self.reducedList = [] self.factor = 1.0 if not self.groupValid(): return count = 0 tmpList = self.unitList[:] while tmpList: count += 1 if count > 5000: raise UnitDataError('Circular unit definition') unit = tmpList.pop(0) if unit.equiv in ('!', '!!'): self.reducedList.append(copy.copy(unit)) elif not unit.equiv: raise UnitDataError('Invalid conversion for "%s"' % unit.name) else: if unit.fromEqn: self.linear = 0 newList = self.parseGroup(unit.equiv) for newUnit in newList: newUnit.exp *= unit.exp tmpList.extend(newList) self.factor *= unit.factor**unit.exp self.reducedList.sort() tmpList = self.reducedList[:] self.reducedList = [] for unit in tmpList: if self.reducedList and unit == self.reducedList[-1]: self.reducedList[-1].exp += unit.exp else: self.reducedList.append(unit) self.reducedList = [unit for unit in self.reducedList if \ unit.name != 'unit' and unit.exp != 0] def categoryMatch(self, otherGroup): "Return 1 if unit types are equivalent" if not self.checkLinear() or not otherGroup.checkLinear(): return 0 return self.reducedList == otherGroup.reducedList and \ [unit.exp for unit in self.reducedList] \ == [unit.exp for unit in otherGroup.reducedList] def checkLinear(self): "Return 1 if linear or acceptable non-linear" if not self.linear: if len(self.unitList) > 1 or self.unitList[0].exp != 1: return 0 return 1 def compatStr(self): "Return string with reduced unit or linear compatability problem" if self.checkLinear(): return self.unitString(self.reducedList) return 'Cannot combine non-linear units' def convert(self, num, toGroup): "Return num of this group converted to toGroup" if self.linear: num *= self.factor else: num = self.nonLinearCalc(num, 1) * self.factor n2 = -1 if toGroup.linear: n2 = num / toGroup.factor else: n2 = toGroup.nonLinearCalc(num / toGroup.factor, 0) return n2 def nonLinearCalc(self, num, isFrom): "Return result of non-linear calculation" x = num try: if self.unitList[0].toEqn: # regular equations if isFrom: temp = float(eval(self.unitList[0].fromEqn)) return temp temp = float(eval(self.unitList[0].toEqn)) return temp data = list(eval(self.unitList[0].fromEqn)) # extrapolation list if isFrom: data = [(float(group[0]), float(group[1])) for group in data] else: data = [(float(group[1]), float(group[0])) for group in data] data.sort() pos = len(data) - 1 for i in range(len(data)): if num <= data[i][0]: pos = i break if pos == 0: pos = 1 y = (num-data[pos-1][0]) / float(data[pos][0]-data[pos-1][0]) \ * (data[pos][1]-data[pos-1][1]) + data[pos-1][1] return y except OverflowError: return 1e9999 except: raise UnitDataError('Bad equation for %s' % self.unitList[0].name) def convertStr(self, num, toGroup): "Return formatted string of converted number" return self.formatNumStr(self.convert(num, toGroup)) def formatNumStr(self, num): "Return num string formatted per options" decPlcs = self.option.intData('DecimalPlaces', 0, UnitGroup.maxDecPlcs) if self.option.boolData('SciNotation'): return ('%%0.%dE' % decPlcs) % num if self.option.boolData('FixedDecimals'): return ('%%0.%df' % decPlcs) % num return ('%%0.%dG' % decPlcs) % num class UnitDataError(Exception): pass class UnitData(dict): def __init__(self): dict.__init__(self) self.sortedKeys = [] def readData(self): "Read all unit data from file" types = [] typeUnits = {} lines = unitData.splitlines() for i in range(len(lines)): # join continuation lines delta = 1 while lines[i].rstrip().endswith('\\'): lines[i] = ''.join([lines[i].rstrip()[:-1], lines[i+delta]]) lines[i+delta] = '' delta += 1 units = [Unit(line) for line in lines if \ line.split('#', 1)[0].strip()] # remove comment lines typeText = '' for unit in units: # find & set headings if unit.name.startswith('['): typeText = unit.name[1:-1].strip() types.append(typeText) typeUnits[typeText] = [] unit.typeName = typeText units = [unit for unit in units if unit.equiv] # keep valid units for unit in units: self[unit.name.replace(' ', '')] = unit typeUnits[unit.typeName].append(unit.name) self.sortedKeys = list(self.keys()) self.sortedKeys.sort() if len(self.sortedKeys) < len(units): raise UnitDataError('Duplicate unit names found') return (types, typeUnits) def findPartialMatch(self, text): "Return first partially matching unit or None" text = text.replace(' ', '') if not text: return None for name in self.sortedKeys: if name.startswith(text): return self[name] return None def findSortPos(self, text): "Return unit whose abbrev comes immediately after text" text = text.replace(' ', '') for name in self.sortedKeys: if text <= name: return self[name] return self[self.sortedKeys[-1]] class Unit: "Reads and stores a single unit conversion" partialExp = 1000 def __init__(self, dataStr): dataList = dataStr.split('#') unitList = dataList.pop(0).split('=', 1) self.name = unitList.pop(0).strip() self.equiv = '' self.factor = 1.0 self.fromEqn = '' # used only for non-linear units self.toEqn = '' # used only for non-linear units if unitList: self.equiv = unitList[0].strip() if self.equiv[0] == '[': # used only for non-linear units try: self.equiv, self.fromEqn = re.match('\[(.*?)\](.*)', \ self.equiv).groups() if ';' in self.fromEqn: self.fromEqn, self.toEqn = self.fromEqn.split(';', 1) self.toEqn = self.toEqn.strip() self.fromEqn = self.fromEqn.strip() except AttributeError: raise UnitDataError('Bad equation for "%s"' % self.name) else: # split factor and equiv unit for linear parts = self.equiv.split(None, 1) if len(parts) > 1 and re.search('[^\d\.eE\+\-\*/]', parts[0]) \ == None: # only allowed digits and operators try: self.factor = float(eval(parts[0])) self.equiv = parts[1] except: pass self.comments = [comm.strip() for comm in dataList] self.comments.extend([''] * (2 - len(self.comments))) self.exp = 1 self.viewLink = [None, None] self.typeName = '' def description(self): "Return name and 1st comment (usu. full name) if applicable" if self.comments[0]: return '%s (%s)' % (self.name, self.comments[0]) return self.name def unitValid(self): "Return 1 if unit and exponent are valid" if self.equiv and -Unit.partialExp < self.exp < Unit.partialExp: return 1 return 0 def unitText(self, absExp=0): "Return text for unit name with exponent or absolute value of exp" exp = self.exp if absExp: exp = abs(self.exp) if exp == 1: return self.name if -Unit.partialExp < exp < Unit.partialExp: return '%s^%d' % (self.name, exp) if exp > 1: return '%s^' % self.name else: return '%s^-' % self.name def __cmp__(self, other): return cmp(self.name, other.name) def __lt__(self, other): return self.name < other.name def __eq__(self, other): return self.name == other.name ############################################################################ # Wrapper functionality # ############################################################################ # Parse the data file, and set everything up for conversion data = UnitData() (types, unitsByType) = data.readData() # At the moment, we're not handling options option = None # set up the objects for unit conversion fromUnit = UnitGroup(data, option) toUnit = UnitGroup(data, option) def convert(num, unit1, unit2): """ Convert from one unit to another num is the factor for the first unit. Raises UnitDataError for various errors. """ fromUnit.update(unit1) toUnit.update(unit2) fromUnit.reduceGroup() toUnit.reduceGroup() # Match up unit categories if not fromUnit.categoryMatch(toUnit): raise UnitDataError('unit categories did not match') return fromUnit.convert(num, toUnit) def units(type): """ Return comma separated string list of units of given type, or a list of types if the argument is not valid. """ if type in types: return '%s units: %s' % (type, ', '.join(unitsByType[type])) else: return 'valid types: ' + ', '.join(types) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3457546 limnoria-2023.11.18/plugins/Math/locales/0000755000175000017500000000000014535072535016116 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Math/locales/fi.po0000644000175000017500000001344014535072470017054 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Math plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:16+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:53 msgid "" "Provides commands to work with math, such as a calculator and\n" " a unit converter." msgstr "" "Tarjoaa komentoja matematiikan kanssa työskentelyyn, kuten laskimen ja " "yksikkömuuntimen." #: plugin.py:57 #, fuzzy msgid "" " [] \n" "\n" " Converts from base to base .\n" " If is left out, it converts to decimal.\n" " " msgstr "" " [] \n" "\n" " Muuntaa from base to base .\n" " Jos jätetään pois, se muuntaa desimaaliksi..\n" " " #: plugin.py:68 #, fuzzy msgid "Invalid for base %s: %s" msgstr "Viallinen baselle %s: %s" #: plugin.py:74 #, fuzzy msgid "Convert a decimal number to another base; returns a string." msgstr "Muuunna desimaaliluku; toiseksi baseksi; palauttaa merkkijonon ." #: plugin.py:95 msgid "" "Convert a number from any base, 2 through 36, to any other\n" " base, 2 through 36. Returns a string." msgstr "" "Muunna basesta mikä tahansa numero, 2:sta 36:teen, millä tahansa muulla\n" " basella , 2:sta 36:teen. Palauttaa merkkiketjun." #: plugin.py:134 #, fuzzy msgid "" "\n" "\n" " Returns the value of the evaluated . The syntax " "is\n" " Python syntax; the type of arithmetic is floating point. Floating\n" " point arithmetic is used in order to prevent a user from being able " "to\n" " crash to the bot with something like '10**10**10**10'. One " "consequence\n" " is that large values such as '10**24' might not be exact.\n" " " msgstr "" "\n" "\n" " Palauttaa kehittyneen arvon. Syntaksi\n" " on Pythonin syntaksi; aritmeettisen leijumispisteen tyyppi. Leijuvan " "pisteen\n" " aritmeettiä käytetään estämään käyttäjää kaatamasta bottia, " "jollakin\n" " kuin '10**10**10**10'. Yksi sattuma on suurilla arvoilla, kuin\n" " '10**24' ei ehkä ole tarkka.\n" " " #: plugin.py:148 plugin.py:173 msgid "The answer exceeded %s or so." msgstr "Vastaus ylittää %s:än tai niin." #: plugin.py:150 plugin.py:175 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:152 plugin.py:177 msgid "%s is not a defined function." msgstr "%s ei ole määritetty funktio." #: plugin.py:154 msgid "Memory error (too much recursion?)" msgstr "" #: plugin.py:161 msgid "" "\n" "\n" " This is the same as the calc command except that it allows integer\n" " math, and can thus cause the bot to suck up CPU. Hence it requires\n" " the 'trusted' capability to use.\n" " " msgstr "" "\n" "\n" " Tämä on sama kuin calc komento, paitsi tämä sallii loputtoman\n" " matematiikan ja näin aiheuttaa botin imevän kaiken prosessorin " "suorituskyvyn. Tästä johtuen se vaatii\n" " 'trusted' valtuuden.\n" " " #: plugin.py:187 #, fuzzy msgid "" "\n" "\n" " Returns the value of an RPN expression.\n" " " msgstr "" "\n" "\n" " Palauttaa RPN lausekkeen arvon.\n" " " #: plugin.py:212 msgid "Not enough arguments for %s" msgstr "Ei tarpeeksi parametrejä %s:lle." #: plugin.py:225 msgid "%q is not a defined function." msgstr "%q ei ole määritetty funktio." #: plugin.py:232 msgid "Stack: [%s]" msgstr "Pino: [%s]" #: plugin.py:236 msgid "" "[] to \n" "\n" " Converts from to . If number isn't given, it\n" " defaults to 1. For unit information, see 'units' command.\n" " " msgstr "" "[] to \n" "\n" " Muuntaa to . Jos numeroa ei ole " "annettu, se\n" " on oletuksena 1. Yksikkö tiedoille, katso 'units' komento.\n" " " #: plugin.py:266 msgid "" " []\n" "\n" " With no arguments, returns a list of measurement types, which can " "be\n" " passed as arguments. When called with a type as an argument, " "returns\n" " the units of that type.\n" " " msgstr "" " []\n" "\n" " Ilman parametrejä, palauttaa listan arviointi tyyppejä, joita " "voidaan\n" " käyttää parametreinä. Kun kutsuttu tyyppinä parametrissä, palaittaa\n" " sen tyyppiset yksiköt.\n" " " #~ msgid "" #~ "There's no reason you should have fancy non-ASCII characters in your " #~ "mathematical expression. Please remove them." #~ msgstr "" #~ "Ei ole mitään syytä miksi matemaattisessa ilmaisussasi pitäisi olla " #~ "kivoja ei-ASCII merkkejä. Ole hyvä ja poista ne." #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Ei todella ole mitään syytä miksi sinulla pitäisi olla alaviivoja tai " #~ "sulkuja matemaattisessa lausekkeessasi. Ole hyvä ja poista ne." #~ msgid "You can't use lambda in this command." #~ msgstr "Et voi käyttää Lambdaa tässä komennossa." #~ msgid "Something in there wasn't a valid number." #~ msgstr "Jokin siinä ei ole kelvollinen numero." ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Math/locales/fr.po������������������������������������������������������0000644�0001750�0001750�00000012460�14535072470�017066� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:53 msgid "" "Provides commands to work with math, such as a calculator and\n" " a unit converter." msgstr "" #: plugin.py:57 msgid "" " [] \n" "\n" " Converts from base to base .\n" " If is left out, it converts to decimal.\n" " " msgstr "" " [] \n" "\n" "Converti le nombre d'une base à l'autre. La seconde base est la décimale par " "défaut." #: plugin.py:68 msgid "Invalid for base %s: %s" msgstr "Nombre invalide pour la base %s : %s" #: plugin.py:74 msgid "Convert a decimal number to another base; returns a string." msgstr "Converti un nombre décimal dans une autre base, retourne une chaîne." #: plugin.py:95 msgid "" "Convert a number from any base, 2 through 36, to any other\n" " base, 2 through 36. Returns a string." msgstr "" "Convertir un nombre de n'importe quelle base, de 2 à 36, à n'importe quelle " "base, de 2 à 36." #: plugin.py:134 msgid "" "\n" "\n" " Returns the value of the evaluated . The syntax " "is\n" " Python syntax; the type of arithmetic is floating point. Floating\n" " point arithmetic is used in order to prevent a user from being able " "to\n" " crash to the bot with something like '10**10**10**10'. One " "consequence\n" " is that large values such as '10**24' might not be exact.\n" " " msgstr "" "\n" "\n" "Retourne la valeur de l' évaluée. La syntaxe est " "celle de Python ; le calcul est à virgule flottante. Les calculs à virgule " "flottante sont utilisés pour enmpêcher l'utilisateur de faire crasher le bot " "avec quelque chose du genre '10**10**10**10'. L'une des conséquences est que " "les grandes valeurs commandes '10**24' peuvent ne pas être exactes." #: plugin.py:148 plugin.py:173 msgid "The answer exceeded %s or so." msgstr "La réponse dépasse %s." #: plugin.py:150 plugin.py:175 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:152 plugin.py:177 msgid "%s is not a defined function." msgstr "%s n'est pas une fonction définie" #: plugin.py:154 msgid "Memory error (too much recursion?)" msgstr "" #: plugin.py:161 msgid "" "\n" "\n" " This is the same as the calc command except that it allows integer\n" " math, and can thus cause the bot to suck up CPU. Hence it requires\n" " the 'trusted' capability to use.\n" " " msgstr "" "\n" "\n" "Cette commande est la même que la commande 'calc', excepté qu'elle utilise " "des mathématiques entières, ce qui peut causer une surconsommation de CPU de " "la part du bot. C'est pourquoi elle requiert la capacité 'trusted'." #: plugin.py:187 msgid "" "\n" "\n" " Returns the value of an RPN expression.\n" " " msgstr "" "\n" "\n" "Retourne la valeur de l'expression mathématique NPI." #: plugin.py:212 msgid "Not enough arguments for %s" msgstr "Pas assez d'arguments pour %s." #: plugin.py:225 msgid "%q is not a defined function." msgstr "%q n'est pas une fonction définie." #: plugin.py:232 msgid "Stack: [%s]" msgstr "Pile : [%s]" #: plugin.py:236 msgid "" "[] to \n" "\n" " Converts from to . If number isn't given, it\n" " defaults to 1. For unit information, see 'units' command.\n" " " msgstr "" "[] to \n" "\n" "Convertit de l' à l'. Si le nombre n'est pas donné, il " "vaut 1 par défaut. Pour plus d'informations sur les unités, utilisez la " "commande 'units'." #: plugin.py:266 msgid "" " []\n" "\n" " With no arguments, returns a list of measurement types, which can " "be\n" " passed as arguments. When called with a type as an argument, " "returns\n" " the units of that type.\n" " " msgstr "" "[]\n" "\n" "Sans argument, retourne la liste des types de mesures, qui peuvent être " "passés en argument. Lors de l'appel avec un argument, retourne les unités de " "chaque type." #~ msgid "" #~ "There's no reason you should have fancy non-ASCII characters in your " #~ "mathematical expression. Please remove them." #~ msgstr "" #~ "Il n'y a aucune réelle raison que vous mettiez des caractères non-ASCII " #~ "dans vos expressions mathématiques. Merci de les retirer." #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Il n'y a aucune réelle raison que vous mettiez des underscores ou des " #~ "crochets dans vos expressions mathématiques. Merci de les retirer." #~ msgid "You can't use lambda in this command." #~ msgstr "Vous ne pouvez utiliser lambda dans cette commande." #~ msgid "Something in there wasn't a valid number." #~ msgstr "Quelque chose là-dedans n'est pas un nombre valide" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Math/locales/hu.po������������������������������������������������������0000644�0001750�0001750�00000012323�14535072470�017071� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Math\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-02 21:06+0200\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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:53 msgid "" "Provides commands to work with math, such as a calculator and\n" " a unit converter." msgstr "" #: plugin.py:57 msgid "" " [] \n" "\n" " Converts from base to base .\n" " If is left out, it converts to decimal.\n" " " msgstr "" " [] \n" "\n" "Konvertálja -ot alapról alapra. Ha nincs " "megadva, decimálisra konvertál." #: plugin.py:68 msgid "Invalid for base %s: %s" msgstr "Érvénytelen %s alaphoz: %s" #: plugin.py:74 msgid "Convert a decimal number to another base; returns a string." msgstr "" "Konvertál egy decimális számot egy másik alapra; kiír egy karakterláncot." #: plugin.py:95 msgid "" "Convert a number from any base, 2 through 36, to any other\n" " base, 2 through 36. Returns a string." msgstr "" "Konvertál egy számot bármilyen alapról 2 és 36 között bármilyen másik " "bázisra 2 és 36 között. Kiír egy karakterláncot." #: plugin.py:134 msgid "" "\n" "\n" " Returns the value of the evaluated . The syntax " "is\n" " Python syntax; the type of arithmetic is floating point. Floating\n" " point arithmetic is used in order to prevent a user from being able " "to\n" " crash to the bot with something like '10**10**10**10'. One " "consequence\n" " is that large values such as '10**24' might not be exact.\n" " " msgstr "" "\n" "\n" "Kiírja a kiértékelt értékét. A szintaxis Python " "szintaxis; a számtan típusa lebegőpontos. Lebegőpontos számtan használt " "azért, hogy megakadályozzuk a felhasználókat, hogy képesek legyenek " "előidézni a bot összeomlással valami olyannal, mint '10**10**10**10'. Egy " "következmény, hogy nagy értékek, mint a '10**24' lehet, hogy nem lesznek " "pontosak." #: plugin.py:148 plugin.py:173 msgid "The answer exceeded %s or so." msgstr "A válasz meghaladta %s-t." #: plugin.py:150 plugin.py:175 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:152 plugin.py:177 msgid "%s is not a defined function." msgstr "%s nincs meghatározva függvényként." #: plugin.py:154 msgid "Memory error (too much recursion?)" msgstr "" #: plugin.py:161 msgid "" "\n" "\n" " This is the same as the calc command except that it allows integer\n" " math, and can thus cause the bot to suck up CPU. Hence it requires\n" " the 'trusted' capability to use.\n" " " msgstr "" "\n" "\n" "Ez ugyanaz, mint a calc parancs, kivéve, hogy engedélyez egész számos " "matematikát, és előidézheti, hogy a bot felszívjon sok CPU-t. Ezért a " "'trusted' képesség szükséges a használatához." #: plugin.py:187 msgid "" "\n" "\n" " Returns the value of an RPN expression.\n" " " msgstr "" "\n" "\n" "Kiírja egy RPN kifejezés értékét." #: plugin.py:212 msgid "Not enough arguments for %s" msgstr "Nincs elég paraméter %s-hoz." #: plugin.py:225 msgid "%q is not a defined function." msgstr "%q nincs meghatározva függvényként." #: plugin.py:232 msgid "Stack: [%s]" msgstr "Verem: [%s]" #: plugin.py:236 msgid "" "[] to \n" "\n" " Converts from to . If number isn't given, it\n" " defaults to 1. For unit information, see 'units' command.\n" " " msgstr "" "[] to \n" "\n" "-ről -re konvertál. Ha a szám nem megadott, " "alapértelmezett értéke 1. Mértékegység információért lásd az 'units' " "parancsot." #: plugin.py:266 msgid "" " []\n" "\n" " With no arguments, returns a list of measurement types, which can " "be\n" " passed as arguments. When called with a type as an argument, " "returns\n" " the units of that type.\n" " " msgstr "" "[]\n" "\n" "Paraméterek nélkül kiírja a mértékegységek listáját, amelyek megadhatók " "paraméterenként. Amikor egy típussal van meghívva paraméterként, kiírja a " "típus mértékegységeit." #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Tényleg nincs semmi ok, hogy miért legyenek aláhúzások vagy zárójelek a " #~ "matamatikai kifejezésedben. Kérlek távolítsd el őket." #~ msgid "You can't use lambda in this command." #~ msgstr "Nem használhatsz lambdát ebben a parancsban." #~ msgid "Something in there wasn't a valid number." #~ msgstr "Valami itt nem egy érvényes szám." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Math/locales/it.po������������������������������������������������������0000644�0001750�0001750�00000012315�14535072470�017072� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-21 17:27+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:53 msgid "" "Provides commands to work with math, such as a calculator and\n" " a unit converter." msgstr "" #: plugin.py:57 msgid "" " [] \n" "\n" " Converts from base to base .\n" " If is left out, it converts to decimal.\n" " " msgstr "" " [] \n" "\n" " Converte un numero da una base a un'altra.\n" " Se non è specificata, converte in decimale.\n" " " #: plugin.py:68 msgid "Invalid for base %s: %s" msgstr "Numero non valido per base %s: %s" #: plugin.py:74 msgid "Convert a decimal number to another base; returns a string." msgstr "Converte un numero decimale in un'altra base, restituisce una stringa." #: plugin.py:95 msgid "" "Convert a number from any base, 2 through 36, to any other\n" " base, 2 through 36. Returns a string." msgstr "" "Converte un numero da qualsiasi base tra 2 e 36 ad un'altra tra 2 e 36, " "restituisce una stringa." #: plugin.py:134 msgid "" "\n" "\n" " Returns the value of the evaluated . The syntax " "is\n" " Python syntax; the type of arithmetic is floating point. Floating\n" " point arithmetic is used in order to prevent a user from being able " "to\n" " crash to the bot with something like '10**10**10**10'. One " "consequence\n" " is that large values such as '10**24' might not be exact.\n" " " msgstr "" "\n" "\n" " Restituisce il valore dell' valutata. La " "sintassi\n" " è quella di Python; il tipo di aritmetica è in virgola mobile. " "Quest'ultima\n" " è utilizzata per prevenire che un utente sia in grado di mandare in " "crash il\n" " bot con qualcosa di simile a \"10**10**10**10\". Una delle " "conseguenze però è\n" " che valori di grandi dimensioni come \"10**24\" potrebbero non " "essere precisi.\n" " " #: plugin.py:148 plugin.py:173 msgid "The answer exceeded %s or so." msgstr "La risposta ha superato %s." #: plugin.py:150 plugin.py:175 msgid "Invalid syntax: %s" msgstr "" #: plugin.py:152 plugin.py:177 msgid "%s is not a defined function." msgstr "%s non è una funzione definita." #: plugin.py:154 msgid "Memory error (too much recursion?)" msgstr "" #: plugin.py:161 msgid "" "\n" "\n" " This is the same as the calc command except that it allows integer\n" " math, and can thus cause the bot to suck up CPU. Hence it requires\n" " the 'trusted' capability to use.\n" " " msgstr "" "\n" "\n" " Questo comando è identico a \"calc\" eccetto che permette numeri " "interi\n" " e può causare l'uso massiccio di CPU da parte del bot. Per questo " "motivo\n" " richiede la capacità \"trusted\" per poterlo utilizzare.\n" " " #: plugin.py:187 msgid "" "\n" "\n" " Returns the value of an RPN expression.\n" " " msgstr "" "\n" "\n" " Restituisce il valore di un'espressione RPN (Reverse Polish " "Notation).\n" " " #: plugin.py:212 msgid "Not enough arguments for %s" msgstr "Argomenti per %s insufficienti" #: plugin.py:225 msgid "%q is not a defined function." msgstr "%q non è una funzione definita." #: plugin.py:232 msgid "Stack: [%s]" msgstr "Stack: [%s]" #: plugin.py:236 msgid "" "[] to \n" "\n" " Converts from to . If number isn't given, it\n" " defaults to 1. For unit information, see 'units' command.\n" " " msgstr "" "[] ad \n" "\n" " Converte da ad . Se non è " "specificato,\n" " usa 1 come predefinito. Per informazioni sulle unità, utilizza il " "comando \"units\".\n" " " #: plugin.py:266 msgid "" " []\n" "\n" " With no arguments, returns a list of measurement types, which can " "be\n" " passed as arguments. When called with a type as an argument, " "returns\n" " the units of that type.\n" " " msgstr "" " []\n" "\n" " Senza argomenti restituisce un elenco di tipi di misura che possono " "essere\n" " passati come argomento. Quando viene chiamato con un tipo come " "argomento,\n" " riporta le unità di quel tipo.\n" " " #~ msgid "" #~ "There's really no reason why you should have underscores or brackets in " #~ "your mathematical expression. Please remove them." #~ msgstr "" #~ "Non v'è alcuna ragione di usare underscore o parentesi nelle espressioni " #~ "matematiche; ti invito a rimuoverli." #~ msgid "You can't use lambda in this command." #~ msgstr "Non è possibile usare lambda in questo comando." #~ msgid "Something in there wasn't a valid number." #~ msgstr "Qualcosa non equivaleva a un numero valido." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Math/plugin.py����������������������������������������������������������0000644�0001750�0001750�00000026074�14535072470�016353� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008-2009, James McCoy # Copyright (c) 2010-2021, 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 __future__ import division import re import math import cmath import types import string import supybot.utils as utils from supybot.commands import * import supybot.utils.minisix as minisix import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Math') from .local import convertcore from supybot.utils.math_evaluator import safe_eval, InvalidNode, SAFE_ENV baseArg = ('int', 'base', lambda i: i <= 36) class Math(callbacks.Plugin): """Provides commands to work with math, such as a calculator and a unit converter.""" @internationalizeDocstring def base(self, irc, msg, args, frm, to, number): """ [] Converts from base to base . If is left out, it converts to decimal. """ if not number: number = str(to) to = 10 try: irc.reply(self._convertBaseToBase(number, to, frm)) except ValueError: irc.error(_('Invalid for base %s: %s') % (frm, number)) base = wrap(base, [('int', 'base', lambda i: 2 <= i <= 36), optional(('int', 'base', lambda i: 2 <= i <= 36), 10), additional('something')]) def _convertDecimalToBase(self, number, base): """Convert a decimal number to another base; returns a string.""" if number == 0: return '0' elif number < 0: negative = True number = -number else: negative = False digits = [] while number != 0: digit = number % base if digit >= 10: digit = string.ascii_uppercase[digit - 10] else: digit = str(digit) digits.append(digit) number = number // base digits.reverse() return '-'*negative + ''.join(digits) def _convertBaseToBase(self, number, toBase, fromBase): """Convert a number from any base, 2 through 36, to any other base, 2 through 36. Returns a string.""" number = minisix.long(str(number), fromBase) if toBase == 10: return str(number) return self._convertDecimalToBase(number, toBase) def _floatToString(self, x): if -1e-10 < x < 1e-10: return '0' elif -1e-10 < int(x) - x < 1e-10: return str(int(x)) else: return str(x) def _complexToString(self, x): realS = self._floatToString(x.real) imagS = self._floatToString(x.imag) if imagS == '0': return realS elif imagS == '1': imagS = '+i' elif imagS == '-1': imagS = '-i' elif x.imag < 0: imagS = '%si' % imagS else: imagS = '+%si' % imagS if realS == '0' and imagS == '0': return '0' elif realS == '0': return imagS.lstrip('+') elif imagS == '0': return realS else: return '%s%s' % (realS, imagS) @internationalizeDocstring def calc(self, irc, msg, args, text): """ Returns the value of the evaluated . The syntax is Python syntax; the type of arithmetic is floating point. Floating point arithmetic is used in order to prevent a user from being able to crash to the bot with something like '10**10**10**10'. One consequence is that large values such as '10**24' might not be exact. """ try: self.log.info('evaluating %q from %s', text, msg.prefix) x = complex(safe_eval(text, allow_ints=False)) irc.reply(self._complexToString(x)) except OverflowError: maxFloat = math.ldexp(0.9999999999999999, 1024) irc.error(_('The answer exceeded %s or so.') % maxFloat) except InvalidNode as e: irc.error(_('Invalid syntax: %s') % e.args[0]) except NameError as e: irc.error(_('%s is not a defined function.') % e.args[0]) except MemoryError: irc.error(_('Memory error (too much recursion?)')) except Exception as e: irc.error(str(e)) calc = wrap(calc, ['text']) @internationalizeDocstring def icalc(self, irc, msg, args, text): """ This is the same as the calc command except that it allows integer math, and can thus cause the bot to suck up CPU. Hence it requires the 'trusted' capability to use. """ try: self.log.info('evaluating %q from %s', text, msg.prefix) result = safe_eval(text, allow_ints=True) float(result) # fail early if it is too large to be displayed except OverflowError: maxFloat = math.ldexp(0.9999999999999999, 1024) irc.error(_('The answer exceeded %s or so.') % maxFloat) except InvalidNode as e: irc.error(_('Invalid syntax: %s') % e.args[0]) except NameError as e: irc.error(_('%s is not a defined function.') % str(e).split()[1]) except Exception as e: irc.error(utils.exnToString(e)) else: try: result_str = str(result) except ValueError as e: # Probably too large to be converted to string; go through # floats instead. # https://docs.python.org/3/library/stdtypes.html#int-max-str-digits # (Depending on configuration, this may be dead code because it # is caught by the float() check above. result_str = str(float(result)) irc.reply(result_str) icalc = wrap(icalc, [('checkCapability', 'trusted'), 'text']) _rpnEnv = { 'dup': lambda s: s.extend([s.pop()]*2), 'swap': lambda s: s.extend([s.pop(), s.pop()]) } def rpn(self, irc, msg, args): """ Returns the value of an RPN expression. """ stack = [] for arg in args: try: x = complex(arg) if x == abs(x): x = abs(x) stack.append(x) except ValueError: # Not a float. if arg in SAFE_ENV: f = SAFE_ENV[arg] if callable(f): called = False arguments = [] while not called and stack: arguments.append(stack.pop()) try: stack.append(f(*arguments)) called = True except TypeError: pass if not called: irc.error(_('Not enough arguments for %s') % arg) return else: stack.append(f) elif arg in self._rpnEnv: self._rpnEnv[arg](stack) else: arg2 = stack.pop() arg1 = stack.pop() s = '%s%s%s' % (arg1, arg, arg2) try: stack.append(safe_eval(s, allow_ints=False)) except SyntaxError: irc.error(format(_('%q is not a defined function.'), arg)) return if len(stack) == 1: irc.reply(str(self._complexToString(complex(stack[0])))) else: s = ', '.join(map(self._complexToString, list(map(complex, stack)))) irc.reply(_('Stack: [%s]') % s) @internationalizeDocstring def convert(self, irc, msg, args, number, unit1, unit2): """[] to Converts from to . If number isn't given, it defaults to 1. For unit information, see 'units' command. """ try: digits = len(str(number).split('.')[1]) except IndexError: digits = 0 try: newNum = convertcore.convert(number, unit1, unit2) if isinstance(newNum, float): zeros = 0 for char in "{:f}".format(newNum).split('.')[1]: if char != '0': break zeros += 1 # Let's add one signifiant digit. Physicists would not like # that, but common people usually do not give extra zeros... # (for example, with '32 C to F', an extra digit would be # expected). newNum = round(newNum, digits + 1 + zeros) newNum = self._floatToString(newNum) irc.reply(str(newNum)) except convertcore.UnitDataError as ude: irc.error(str(ude)) convert = wrap(convert, [optional('float', 1.0),'something','to','text']) @internationalizeDocstring def units(self, irc, msg, args, type): """ [] With no arguments, returns a list of measurement types, which can be passed as arguments. When called with a type as an argument, returns the units of that type. """ irc.reply(convertcore.units(type)) units = wrap(units, [additional('text')]) Class = Math # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Math/test.py������������������������������������������������������������0000644�0001750�0001750�00000022341�14535072470�016025� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 __future__ import print_function from supybot.test import * class MathTestCase(PluginTestCase): plugins = ('Math',) def testBase(self): self.assertNotRegexp('base 56 asdflkj', 'ValueError') self.assertResponse('base 16 2 F', '1111') self.assertResponse('base 2 16 1111', 'F') self.assertResponse('base 20 BBBB', '92631') self.assertResponse('base 10 20 92631', 'BBBB') self.assertResponse('base 2 36 10', '2') self.assertResponse('base 36 2 10', '100100') self.assertResponse('base 2 1010101', '85') self.assertResponse('base 2 2 11', '11') self.assertResponse('base 12 0', '0') self.assertResponse('base 36 2 0', '0') self.assertNotError("base 36 " +\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ") self.assertResponse("base 10 36 [base 36 " +\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"\ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz]", "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"\ "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ") self.assertResponse('base 2 10 [base 10 2 12]', '12') self.assertResponse('base 16 2 [base 2 16 110101]', '110101') self.assertResponse('base 10 8 [base 8 76532]', '76532') self.assertResponse('base 10 36 [base 36 csalnwea]', 'CSALNWEA') self.assertResponse('base 5 4 [base 4 5 212231]', '212231') self.assertError('base 37 1') self.assertError('base 1 1') self.assertError('base 12 1 1') self.assertError('base 1 12 1') self.assertError('base 1.0 12 1') self.assertError('base A 1') self.assertError('base 4 4') self.assertError('base 10 12 A') self.assertRegexp('base 2 10 [base 10 2 -12]', '-12') self.assertRegexp('base 16 2 [base 2 16 -110101]', '-110101') def testCalc(self): self.assertResponse('calc 5*0.06', str(5*0.06)) self.assertResponse('calc 2.0-7.0', str(2-7)) self.assertResponse('calc e**(i*pi)+1', '0') if minisix.PY3: # Python 2 has bad handling of exponentiation of negative numbers self.assertResponse('calc (-1)**.5', 'i') self.assertRegexp('calc (-5)**.5', '2.236067977[0-9]+i') self.assertRegexp('calc -((-5)**.5)', '-2.236067977[0-9]+i') self.assertNotRegexp('calc [9, 5] + [9, 10]', 'TypeError') self.assertError('calc [9, 5] + [9, 10]') self.assertNotError('calc degrees(2)') self.assertNotError('calc (2 * 3) - 2*(3*4)') self.assertNotError('calc (3) - 2*(3*4)') self.assertNotError('calc (1600 * 1200) - 2*(1024*1280)') self.assertNotError('calc 3-2*4') self.assertNotError('calc (1600 * 1200)-2*(1024*1280)') self.assertResponse('calc factorial(20000)', 'Error: factorial argument too large') self.assertResponse('calc factorial(20000) / factorial(19999)', 'Error: factorial argument too large') def testCalcNoNameError(self): self.assertRegexp('calc foobar(x)', 'foobar is not a defined function') def testCalcInvalidNode(self): self.assertRegexp('calc {"foo": "bar"}', 'Illegal construct Dict') def testCalcImaginary(self): self.assertResponse('calc 3 + sqrt(-1)', '3+i') def testCalcFloorWorksWithSqrt(self): self.assertNotError('calc floor(sqrt(5))') def testCaseInsensitive(self): self.assertNotError('calc PI**PI') def testCalcMaxMin(self): self.assertResponse('calc max(1,2)', '2') self.assertResponse('calc min(1,2)', '1') def testCalcStrFloat(self): self.assertResponse('calc 3+33333333333333', '33333333333336') def testCalcMemoryError(self): self.assertRegexp('calc ' + '('*10000, '(too much recursion' # cpython < 3.10 '|too many nested parentheses' # cpython >= 3.10 '|parenthesis is never closed)' # pypy ) def testICalc(self): self.assertResponse('icalc 1^1', '0') self.assertResponse('icalc 10**24', '1' + '0'*24) self.assertRegexp('icalc 49/6', '8.16') self.assertRegexp('icalc factorial(20000)', 'Error: The answer exceeded') self.assertResponse('icalc factorial(20000) / factorial(19999)', '20000.0') def testRpn(self): self.assertResponse('rpn 5 2 +', '7') self.assertResponse('rpn 1 2 3 +', 'Stack: [1, 5]') self.assertResponse('rpn 1 dup', 'Stack: [1, 1]') self.assertResponse('rpn 2 3 4 + -', str(2-7)) self.assertNotError('rpn 2 degrees') def testRpnSwap(self): self.assertResponse('rpn 1 2 swap', 'Stack: [2, 1]') def testRpmNoSyntaxError(self): self.assertNotRegexp('rpn 2 3 foobar', 'SyntaxError') def testConvert(self): self.assertResponse('convert 1 m to cm', '100') self.assertResponse('convert m to cm', '100') self.assertResponse('convert 3 metres to km', '0.003') self.assertResponse('convert 1 cm to km', '1e-05') self.assertResponse('convert 32 F to C', '0') self.assertResponse('convert 32 C to F', '89.6') self.assertResponse('convert [calc 2*pi] rad to degree', '360') self.assertResponse('convert amu to atomic mass unit', '1') self.assertResponse('convert [calc 2*pi] rad to circle', '1') self.assertError('convert 1 meatball to bananas') self.assertError('convert 1 gram to meatballs') self.assertError('convert 1 mol to grams') self.assertError('convert 1 m to kpa') def testConvertSingularPlural(self): self.assertResponse('convert [calc 2*pi] rads to degrees', '360') self.assertResponse('convert 1 carat to grams', '0.2') self.assertResponse('convert 10 lbs to oz', '160') self.assertResponse('convert mA to amps', '0.001') def testConvertCaseSensitivity(self): self.assertError('convert MA to amps') self.assertError('convert M to amps') self.assertError('convert Radians to rev') def testUnits(self): self.assertNotError('units') self.assertNotError('units mass') self.assertNotError('units flux density') def testAbs(self): self.assertResponse('calc abs(2)', '2') self.assertResponse('calc abs(-2)', '2') self.assertResponse('calc abs(2.0)', '2') self.assertResponse('calc abs(-2.0)', '2') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/����������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016344� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/__init__.py�����������������������������������������������0000644�0001750�0001750�00000005722�14535072470�020461� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, 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. ### """ The MessageParser plugin allows you to set custom regexp triggers, which will trigger the bot to respond if they match anywhere in the message. This is useful for those cases when you want a bot response even when the bot was not explicitly addressed by name or prefix character. An updated page of this plugin's documentation is located here: https://sourceforge.net/p/gribble/wiki/MessageParser_Plugin/ """ 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__ = "0.1" __author__ = supybot.Author('Daniel Folkinshteyn', 'nanotube', 'nanotube@users.sourceforge.net') __maintainer__ = supybot.authors.limnoria_core # 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 importlib 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 tabstop=4 expandtab textwidth=79: ����������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/config.py�������������������������������������������������0000644�0001750�0001750�00000010560�14535072470�020163� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization from supybot.i18n import internationalizeDocstring _ = PluginInternationalization('MessageParser') except: # This are useless functions that's allow to run the plugin on a bot # without the i18n plugin _ = lambda x:x internationalizeDocstring = 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('MessageParser', True) MessageParser = conf.registerPlugin('MessageParser') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(MessageParser, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) conf.registerChannelValue(MessageParser, 'enable', registry.Boolean(True, _("""Determines whether the message parser is enabled. If enabled, will trigger on regexps added to the regexp db."""))) conf.registerChannelValue(MessageParser, 'enableForNotices', registry.Boolean(False, _("""Determines whether the message parser is enabled for NOTICE messages too."""))) conf.registerChannelValue(MessageParser, 'keepRankInfo', registry.Boolean(True, _("""Determines whether we keep updating the usage count for each regexp, for popularity ranking."""))) conf.registerChannelValue(MessageParser, 'rankListLength', registry.Integer(20, _("""Determines the number of regexps returned by the triggerrank command."""))) conf.registerChannelValue(MessageParser, 'requireVacuumCapability', registry.String('admin', _("""Determines the capability required (if any) to vacuum the database."""))) conf.registerChannelValue(MessageParser, 'requireManageCapability', registry.String('admin; channel,op', _("""Determines the capabilities required (if any) to manage the regexp database, including add, remove, lock, unlock. Use 'channel,capab' for channel-level capabilities. Note that absence of an explicit anticapability means user has capability."""))) conf.registerChannelValue(MessageParser, 'listSeparator', registry.String(', ', _("""Determines the separator used between regexps when shown by the list command."""))) conf.registerChannelValue(MessageParser, 'maxTriggers', registry.Integer(0, _("""Determines the maximum number of triggers in one message. Set this to 0 to allow an infinite number of triggers."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/locales/��������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�017766� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/locales/fi.po���������������������������������������������0000644�0001750�0001750�00000026022�14535072470�020724� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-03-22 14:39+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "X-Generator: Poedit 1.5.4\n" #: config.py:58 msgid "" "Determines whether the\n" " message parser is enabled. If enabled, will trigger on regexps\n" " added to the regexp db." msgstr "" "Määrittää onko viestikursija käytössä.\n" " Jos käytössä aktivoituu säännöllisissä lausekkeissa, jotka on\n" " lisätty säännöllinen lauseke tietokantaan." #: config.py:62 msgid "" "Determines whether the message parser\n" " is enabled for NOTICE messages too." msgstr "" "Määrittää onko viestien parsija käytössä\n" " myös NOTICE viesteille." #: config.py:65 msgid "" "Determines whether we keep updating the usage\n" " count for each regexp, for popularity ranking." msgstr "" "Määrottää pidetäänkö käyttö\n" " count for each regexp, for popularity ranking." #: config.py:68 msgid "" "Determines the number of regexps returned\n" " by the triggerrank command." msgstr "" "Määrittää palautuneiden säännöllisten lausekkeiden, jotka palautuvat\n" " triggerrank komennolla, määrän." #: config.py:71 msgid "" "Determines the capability required (if any) to\n" " vacuum the database." msgstr "" "Määrittää valtuuden (jos mikään), joka on vaadittu\n" " tietokannan tyhjentämiseen." #: config.py:74 msgid "" "Determines the\n" " capabilities required (if any) to manage the regexp database,\n" " including add, remove, lock, unlock. 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) on vaadittu hallitsemaan säännöllinen " "lauseke tietokantaa,\n" " sisältäen lisää, poista, lukitse, avaa. Käytä 'kanava,valtuus':ksia\n" " kanava tason valtuuksille.\n" " Huomaa, että jopa antivaltuuden olemassaolo tarkoittaa, että käyttäjällä " "on \n" " valtuus." #: config.py:81 msgid "" "Determines the separator used between regexps when\n" " shown by the list command." msgstr "" "Määrittää erottajan, jota käytetään list komennon näyttämien\n" " säännöllisten lausekkeiden välissä." #: config.py:84 msgid "" "Determines the maximum number of triggers in\n" " one message. Set this to 0 to allow an infinite number of triggers." msgstr "" "Määrittää liipaisimien enimmäismäärän yhdessä viestissä.\n" " Aseta tämä arvoon 0 salliaksesi rajattoman määrän liipaisimia." #: plugin.py:75 msgid "" "This plugin can set regexp triggers to activate the bot.\n" " Use 'add' command to add regexp trigger, 'remove' to remove." msgstr "" "Tämä lisäosa voi asettaa säännöllisiä lausekeke liipaisimia, jotka " "aktivoivat botin.\n" " Käytä 'add' komentoa lisätäksesi säännöllisen lausekkeen ja 'remove' " "poistaaksesi säännöllisen lausekkeen." #: plugin.py:83 msgid "Create the database and connect to it." msgstr "Luo tietokanta ja yhdistä siihen." #: plugin.py:108 msgid "Use this to get a database for a specific channel." msgstr "Käytä tätä saadaksesi tietokannan tietylle kanavalle." #: plugin.py:132 msgid "Run a command from message, as if command was sent over IRC." msgstr "" "Suorittaa komennon viestistä, kuin jos komento olisi lähetetty IRC:een yli." #: plugin.py:146 msgid "" "Check if the user has any of the required capabilities to manage\n" " the regexp database." msgstr "" "Tarkista onko käyttäjällä vaadittu oikeus säännöllisen lauseke tietokannan\n" " muokkaamiseen." #: plugin.py:209 #, fuzzy msgid "" "[|global] \n" "\n" " Associates with . is only\n" " necessary if the message isn't sent on the channel\n" " itself. Action is echoed upon regexp match, with variables $1, $2,\n" " etc. being interpolated from the regexp match groups." msgstr "" "[|global] \n" "\n" " Liittää . vaaditaan\n" " vain, ellei viestiä lähetetä kanavalla itsellään.\n" " toiminto toistetaan säännöllisen lausekkeen täsmätessä muuttujilla $1, $2,\n" " jne. tulevat interpolatoiduiksi säännöllisen lausekkeen täsmäysryhmistä." #: plugin.py:231 msgid "Invalid python regexp: %s" msgstr "Viallinen Python säännöllinen lauseke: %s" #: plugin.py:243 msgid "That trigger is locked." msgstr "Tuo liipaisin on lukittu." #: plugin.py:249 #, fuzzy msgid "" "[|global] [--id] ]\n" "\n" " Removes the trigger for from the triggers database.\n" " is only necessary if\n" " the message isn't sent in the channel itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] ]\n" "\n" " Poistaa lipaisimen liipaisin " "tietokannasta.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla " "itsellään\n" " jos viestiä ei lähetetä kanavalla itsellään.\n" " Jos asetus --id on annettu, hakee säännöllinen lauseke id:llä, ei " "sisällöllä.\n" " " #: plugin.py:271 plugin.py:301 plugin.py:324 plugin.py:352 plugin.py:382 msgid "There is no such regexp trigger." msgstr "Tuollaista säännöllinen lauseke liipaisinta ei ole." #: plugin.py:275 msgid "This regexp trigger is locked." msgstr "Tämä säännöllinen lauseke liipaisin on lukittu." #: plugin.py:287 #, fuzzy msgid "" "[|global] \n" "\n" " Locks the so that it cannot be\n" " removed or overwritten to. is only necessary if the " "message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Lukitsee , jotta sitä ei voida poistaa tai " "ylikirjoittaa.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla " "itsellään.\n" " " #: plugin.py:310 #, fuzzy msgid "" "[|global] \n" "\n" " Unlocks the entry associated with so that it can be\n" " removed or overwritten. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Avaa merkinnän, joka on liitetty , " "jotta se voidaan\n" " poistaa tai ylikirjoittaa. on vaadittu vain jos viestiä " "ei\n" " lähetetä kanavalla itsellään.\n" " " #: plugin.py:333 #, fuzzy msgid "" "[|global] [--id] \n" "\n" " Looks up the value of in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" " Etsii arvoa liipaisin tietokannassa.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " Jos asetus --id on määritetty, haetaan säännöllisen lausekkeen id:n, " "ei sisällön perusteella\n" " " #: plugin.py:362 #, fuzzy msgid "" "[|global] [--id] \n" "\n" " Display information about in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" " Näyttää tiedot liipaisin " "tietokannassa.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla " "itsellään.\n" " Jos asetus --id on annettu, hakee säännöllisen lausekkeen id:een, ei " "sisällön perusteella.\n" " " #: plugin.py:385 msgid "" "The regexp id is %d, regexp is \"%s\", and action is \"%s\". It was added by " "user %s on %s, has been triggered %d times, and is %s." msgstr "" "Säännöllisen lausekkeen id on %d, säännöllinen lauseke on \"%s\", ja " "toiminto on \"%s\". Sen lisäsi %s %s:llä, on liipaistu %d kertaa, ja on %s." #: plugin.py:394 msgid "locked" msgstr "lukittu" #: plugin.py:394 msgid "not locked" msgstr "ei lukittu" #: plugin.py:401 #, fuzzy msgid "" "[|global]\n" "\n" " Lists regexps present in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself. Regexp ID listed in parentheses.\n" " " msgstr "" "[]\n" "\n" " Luettelee säännölliset lausekkeet, jotka ovat liipaisin " "tietokannassa kanavalla.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään. Säännöllinen lauseke ID on luetteloitu suluissa.\n" " " #: plugin.py:414 plugin.py:440 msgid "There are no regexp triggers in the database." msgstr "Säännöllinen lauseke tietokannassa ei ole liipaisimia." #: plugin.py:424 #, fuzzy msgid "" "[|global]\n" "\n" " Returns a list of top-ranked regexps, sorted by usage count\n" " (rank). The number of regexps returned is set by the\n" " rankListLength registry value. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa top säännölliset lausekkeet, jotka on lajiteltu " "käyttömäärän\n" " (rank). Palautuneiden säännöllisten lausekkeiden lukumäärän " "määrittää\n" " rankListLength rekisteriarvo. on vaadittu vain jos viestiä " "ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:448 #, fuzzy msgid "" "[|global]\n" "\n" " Vacuums the database for .\n" " See SQLite vacuum doc here: http://www.sqlite.org/lang_vacuum.html\n" " is only necessary if the message isn't sent in\n" " the channel itself.\n" " First check if user has the required capability specified in plugin\n" " config requireVacuumCapability.\n" " " msgstr "" "[]\n" "\n" " Tyhjentää tietokannan.\n" " Katso SQLite tyhjennys documentaatio täältä: http://www.sqlite.org/" "lang_vacuum.html\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla " "itsellään.\n" " Tarkista ensin onko käyttäjällä vaadittu valtuus, joka on " "määritetty\n" " asetuksessa requireVacuumCapability.\n" " " ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/locales/fr.po���������������������������������������������0000644�0001750�0001750�00000023611�14535072470�020736� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:58 msgid "" "Determines whether the\n" " message parser is enabled. If enabled, will trigger on regexps\n" " added to the regexp db." msgstr "" "Détermine si le parseur de messages est activé. S'il l'est, il réagira aux " "expressions régulières qui sont dans la base de données d'expressions " "régulières." #: config.py:62 msgid "" "Determines whether the message parser\n" " is enabled for NOTICE messages too." msgstr "Détermine si le parseur de messages est aussi activé pour les NOTICEs." #: config.py:65 msgid "" "Determines whether we keep updating the usage\n" " count for each regexp, for popularity ranking." msgstr "" "Détermine si on met à jour le compteur d'utilisation de chaque expression " "régulière, pour un classement de popularité" #: config.py:68 msgid "" "Determines the number of regexps returned\n" " by the triggerrank command." msgstr "" "Détermine le nombre d'expressions régulières retournées par la commande " "triggerrank" #: config.py:71 msgid "" "Determines the capability required (if any) to\n" " vacuum the database." msgstr "" "Détermine la capacité requise (s'il y en a une) pour faire un vacuum de la " "base de données." #: config.py:74 msgid "" "Determines the\n" " capabilities required (if any) to manage the regexp database,\n" " including add, remove, lock, unlock. 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 gérer la base de données " "d'expressions régulières, ce qui inclue l'ajout, la suppression, le " "verrouillage, et le déverrouillage. Utilisez 'canal,capa' pour des capacités " "par canal. Notez que l'absence de toute anti-capacité explicite signifit que " "l'utilisateur peut le faire." #: config.py:81 msgid "" "Determines the separator used between regexps when\n" " shown by the list command." msgstr "" "Détermine le séparateur utilisé entre les expressions régulières affichées " "par la commande list." #: config.py:84 msgid "" "Determines the maximum number of triggers in\n" " one message. Set this to 0 to allow an infinite number of triggers." msgstr "." #: plugin.py:75 msgid "" "This plugin can set regexp triggers to activate the bot.\n" " Use 'add' command to add regexp trigger, 'remove' to remove." msgstr "" "Ce plugin peut définir les triggers pour activer le bot. Utilisez la " "commande 'add' pour ajouter un trigger et 'remove' pour en retirer un." #: plugin.py:83 msgid "Create the database and connect to it." msgstr "." #: plugin.py:108 msgid "Use this to get a database for a specific channel." msgstr "." #: plugin.py:132 msgid "Run a command from message, as if command was sent over IRC." msgstr "." #: plugin.py:146 msgid "" "Check if the user has any of the required capabilities to manage\n" " the regexp database." msgstr "." #: plugin.py:209 msgid "" "[|global] \n" "\n" " Associates with . is only\n" " necessary if the message isn't sent on the channel\n" " itself. Action is echoed upon regexp match, with variables $1, $2,\n" " etc. being interpolated from the regexp match groups." msgstr "" "[] \n" "\n" "Associe l' à l'. est affiché après la " "correspondance avec l', avec les variables $1, $2, " "etc, récupérés à partir des groupes de correspondance de l'. n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:231 msgid "Invalid python regexp: %s" msgstr "Expression régulière Python invalide : %s" #: plugin.py:243 msgid "That trigger is locked." msgstr "Ce trigger est bloqué." #: plugin.py:249 msgid "" "[|global] [--id] ]\n" "\n" " Removes the trigger for from the triggers database.\n" " is only necessary if\n" " the message isn't sent in the channel itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" "Supprime le déclencheur pour l' de la base de données " "des déclencheurs. Si l'option --id est spécifiée, l'id de l' sera récupéré, et non le contenu." #: plugin.py:271 plugin.py:301 plugin.py:324 plugin.py:352 plugin.py:382 msgid "There is no such regexp trigger." msgstr "Cette expression régulière n'existe pas." #: plugin.py:275 msgid "This regexp trigger is locked." msgstr "Cette expression régulière est verrouillée" #: plugin.py:287 msgid "" "[|global] \n" "\n" " Locks the so that it cannot be\n" " removed or overwritten to. is only necessary if the " "message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Verrouille l', ce qui fait que l'on ne puisse plus la " "supprimer ou la modifier. n'est nécessaire que si le message n'est " "pas envoyé sur le canal lui-même." #: plugin.py:310 msgid "" "[|global] \n" "\n" " Unlocks the entry associated with so that it can be\n" " removed or overwritten. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Déverrouille l', ce qui fait que l'on peut à nouveau " "la supprimer ou la modifier. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:333 msgid "" "[|global] [--id] \n" "\n" " Looks up the value of in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" "Recherche la valeur de l' de la base de données des " "déclencheurs. Si l'option --id est spécifiée, l'id de l' sera récupéré, et non le contenu." #: plugin.py:362 msgid "" "[|global] [--id] \n" "\n" " Display information about in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" "Affiche des informations à propos de l' de la base de " "données des déclencheurs. Si l'option --id est spécifiée, l'id de " "l' sera récupéré, et non le contenu." #: plugin.py:385 msgid "" "The regexp id is %d, regexp is \"%s\", and action is \"%s\". It was added by " "user %s on %s, has been triggered %d times, and is %s." msgstr "" "L'id de l'expression régulière est %d, l'expression régulière est \"%s\", et " "l'action est \"%s\". Elle a été ajoutée par l'utilisateur %s le %s, et a été " "utilisée %d fois, et est %s" #: plugin.py:394 msgid "locked" msgstr "verouillée" #: plugin.py:394 msgid "not locked" msgstr "non verrouillée" #: plugin.py:401 msgid "" "[|global]\n" "\n" " Lists regexps present in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself. Regexp ID listed in parentheses.\n" " " msgstr "" "[]\n" "\n" "Liste les expressions régulières présentes dans la base de données. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:414 plugin.py:440 msgid "There are no regexp triggers in the database." msgstr "Il n'y a pas d'expression régulière dans ma base de données." #: plugin.py:424 msgid "" "[|global]\n" "\n" " Returns a list of top-ranked regexps, sorted by usage count\n" " (rank). The number of regexps returned is set by the\n" " rankListLength registry value. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne une liste des expressions régulières les plus utilisées. Le nombre " "d'expressions régulières est définie par la variable de registre supybot." "plugins.MessageParser.rankListLength. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:448 msgid "" "[|global]\n" "\n" " Vacuums the database for .\n" " See SQLite vacuum doc here: http://www.sqlite.org/lang_vacuum.html\n" " is only necessary if the message isn't sent in\n" " the channel itself.\n" " First check if user has the required capability specified in plugin\n" " config requireVacuumCapability.\n" " " msgstr "" "[]\n" "\n" "Fait un vacuum de la base de données pour le .Lisez la documentation " "de SQLite sur cette fonctionnalité : http://www.sqlite.org/lang_vacuum." "htmlVérifie d'abord si l'utilisateur a bien la capacité spécifiée dans la " "variable de configuration supybot.plugins.requireVacuumCapability " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." �����������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/locales/it.po���������������������������������������������0000644�0001750�0001750�00000024310�14535072470�020740� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-17 01: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:58 msgid "" "Determines whether the\n" " message parser is enabled. If enabled, will trigger on regexps\n" " added to the regexp db." msgstr "" "Determina se il parser dei messaggi è abilitato. In caso lo sia, reagirà\n" " alle regexp aggiunte al database delle espressioni regolari." #: config.py:62 #, fuzzy msgid "" "Determines whether the message parser\n" " is enabled for NOTICE messages too." msgstr "" "Determina se il parser dei messaggi è abilitato. In caso lo sia, reagirà\n" " alle regexp aggiunte al database delle espressioni regolari." #: config.py:65 msgid "" "Determines whether we keep updating the usage\n" " count for each regexp, for popularity ranking." msgstr "" "Determina se il conteggio dell'uso venga mantenuto aggiornato per ogni " "regexp, per una classifica di popolarità." #: config.py:68 msgid "" "Determines the number of regexps returned\n" " by the triggerrank command." msgstr "Determina il numero di regexp restituite dal comando \"triggerrank\"." #: config.py:71 msgid "" "Determines the capability required (if any) to\n" " vacuum the database." msgstr "Determina la capacità richiesta (eventuale) per svuotare il database." #: config.py:74 msgid "" "Determines the\n" " capabilities required (if any) to manage the regexp database,\n" " including add, remove, lock, unlock. Use 'channel,capab' for\n" " channel-level capabilities.\n" " Note that absence of an explicit anticapability means user has\n" " capability." msgstr "" "Determina la capacità richiesta (eventuale) per gestire il database delle\n" " regexp che include aggiunta, rimozione, blocco e sblocco. Utilizza\n" " \"canale,capacità\" per le capacità del singolo canale. L'assenza di\n" " un'esplicita anti-capacità significa che l'utente può usare i comandi." #: config.py:81 msgid "" "Determines the separator used between regexps when\n" " shown by the list command." msgstr "" "Determina il separatore utilizzato tra le regexp quando mostrate dal comando " "\"list\"." #: config.py:84 msgid "" "Determines the maximum number of triggers in\n" " one message. Set this to 0 to allow an infinite number of triggers." msgstr "" #: plugin.py:75 msgid "" "This plugin can set regexp triggers to activate the bot.\n" " Use 'add' command to add regexp trigger, 'remove' to remove." msgstr "" "Questo plugin può definire dei trigger basati su regexp per attivare il " "bot.\n" " Utilizza il comando \"add\" per aggiungerne e \"remove\" per rimuoverle." #: plugin.py:83 msgid "Create the database and connect to it." msgstr "Crea il database e ci si connette." #: plugin.py:108 msgid "Use this to get a database for a specific channel." msgstr "Utilizzalo per ottenere un database per un canale specifico." #: plugin.py:132 msgid "Run a command from message, as if command was sent over IRC." msgstr "" "Esegue un comando da un messaggio, come se questo fosse stato inviato su IRC." #: plugin.py:146 msgid "" "Check if the user has any of the required capabilities to manage\n" " the regexp database." msgstr "" "Controlla se l'utente ha una delle capacità richieste per gestire il " "database delle regexp." #: plugin.py:209 #, fuzzy msgid "" "[|global] \n" "\n" " Associates with . is only\n" " necessary if the message isn't sent on the channel\n" " itself. Action is echoed upon regexp match, with variables $1, $2,\n" " etc. being interpolated from the regexp match groups." msgstr "" "[] \n" "\n" " Associa ad . L'azione viene mostrata sulla " "corrispondenza\n" " della regexp, con le variabili $1, $2, ecc. inserita dai gruppi di " "corrispondenza\n" " dell'espressione regolare. è necessario solo se il " "messaggio\n" " non viene inviato nel canale stesso." #: plugin.py:231 msgid "Invalid python regexp: %s" msgstr "Espressione regolare python non valida: %s" #: plugin.py:243 msgid "That trigger is locked." msgstr "Questo trigger è bloccato." #: plugin.py:249 #, fuzzy msgid "" "[|global] [--id] ]\n" "\n" " Removes the trigger for from the triggers database.\n" " is only necessary if\n" " the message isn't sent in the channel itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] ]\n" "\n" " Rimuove il trigger per dal database. è necessario\n" " solo se il messaggio non viene inviato nel canale stesso. Se " "l'opzione\n" " --id è specificata, verrà recuperato tramite ID della regexp anziché " "contenuto.\n" " " #: plugin.py:271 plugin.py:301 plugin.py:324 plugin.py:352 plugin.py:382 msgid "There is no such regexp trigger." msgstr "Questa espressione regolare non esiste." #: plugin.py:275 msgid "This regexp trigger is locked." msgstr "Questa espressione regolare è bloccata." #: plugin.py:287 #, fuzzy msgid "" "[|global] \n" "\n" " Locks the so that it cannot be\n" " removed or overwritten to. is only necessary if the " "message isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Blocca in modo che non sia possibile rimuoverla o " "sovrascriverla.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:310 #, fuzzy msgid "" "[|global] \n" "\n" " Unlocks the entry associated with so that it can be\n" " removed or overwritten. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Sblocca la voce associata a in modo che sia possibile " "rimuoverla o\n" " sovrascriverla. è necessario solo se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:333 #, fuzzy msgid "" "[|global] [--id] \n" "\n" " Looks up the value of in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" " Cerca il valore di nel database dei trigger. è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso. Se " "l'opzione --id\n" " è specificata, verrà recuperato tramite ID della regexp anziché " "contenuto.\n" " " #: plugin.py:362 #, fuzzy msgid "" "[|global] [--id] \n" "\n" " Display information about in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " If option --id specified, will retrieve by regexp id, not content.\n" " " msgstr "" "[] [--id] \n" "\n" " Mostra informazioni su presente nel database dei trigger. " " è\n" " necessario solo se il messaggio non viene inviato nel canale stesso. " "Se l'opzione\n" " --id è specificata, verranno recuperate tramite ID della regexp " "anziché contenuto.\n" " " #: plugin.py:385 msgid "" "The regexp id is %d, regexp is \"%s\", and action is \"%s\". It was added by " "user %s on %s, has been triggered %d times, and is %s." msgstr "" "L'ID della regexp è %d, la regexp è \"%s\" e l'azione associata è \"%s\". È " "stata aggiunta dall'utente %s il %s, è stata utilizzata %d volte ed è %s." #: plugin.py:394 msgid "locked" msgstr "bloccata" #: plugin.py:394 msgid "not locked" msgstr "non bloccata" #: plugin.py:401 #, fuzzy msgid "" "[|global]\n" "\n" " Lists regexps present in the triggers database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself. Regexp ID listed in parentheses.\n" " " msgstr "" "[]\n" "\n" " Elenca le regexp presenti nel database dei trigger. è " "necessario solo se il\n" " messaggio non viene inviato nel canale stesso. Gli ID delle regexp " "sono tra parentesi.\n" " " #: plugin.py:414 plugin.py:440 msgid "There are no regexp triggers in the database." msgstr "Non ci sono espressioni regolari nel database." #: plugin.py:424 #, fuzzy msgid "" "[|global]\n" "\n" " Returns a list of top-ranked regexps, sorted by usage count\n" " (rank). The number of regexps returned is set by the\n" " rankListLength registry value. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta un elenco delle regexp più utilizzate. Il numero di " "espressioni\n" " regolari restituito è definito dalla voce di registro " "rankListLength. \n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:448 #, fuzzy msgid "" "[|global]\n" "\n" " Vacuums the database for .\n" " See SQLite vacuum doc here: http://www.sqlite.org/lang_vacuum.html\n" " is only necessary if the message isn't sent in\n" " the channel itself.\n" " First check if user has the required capability specified in plugin\n" " config requireVacuumCapability.\n" " " msgstr "" "[]\n" "\n" " Svuota il database di . Consulta la documentazione di SQLite " "relativa\n" " all'indirizzo http://www.sqlite.org/lang_vacuum.html e controlla " "prima se l'utente\n" " ha la capacità richiesta specificata nella variabile " "requireVacuumCapability.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/plugin.py�������������������������������������������������0000644�0001750�0001750�00000047562�14535072470�020230� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.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.callbacks as callbacks import supybot.conf as conf import supybot.ircdb as ircdb import re import os import sys import time try: from supybot.i18n import PluginInternationalization from supybot.i18n import internationalizeDocstring _ = PluginInternationalization('MessageParser') except: # This are useless functions that's allow to run the plugin on a bot # without the i18n plugin _ = lambda x:x internationalizeDocstring = lambda x:x #try: #import sqlite #except ImportError: #raise callbacks.Error, 'You need to have PySQLite installed to use this ' \ #'plugin. Download it at ' \ #'' import sqlite3 # these are needed cuz we are overriding getdb import threading import supybot.world as world import supybot.log as log class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): """This plugin can set regexp triggers to activate the bot. Use 'add' command to add regexp trigger, 'remove' to remove.""" threaded = True def __init__(self, irc): callbacks.Plugin.__init__(self, irc) plugins.ChannelDBHandler.__init__(self) def makeDb(self, filename): """Create the database and connect to it.""" if os.path.exists(filename): db = sqlite3.connect(filename) if minisix.PY2: db.text_factory = str return db db = sqlite3.connect(filename) if minisix.PY2: db.text_factory = str cursor = db.cursor() cursor.execute("""CREATE TABLE triggers ( id INTEGER PRIMARY KEY, regexp TEXT UNIQUE ON CONFLICT REPLACE, added_by TEXT, added_at TIMESTAMP, usage_count INTEGER, action TEXT, locked BOOLEAN )""") db.commit() return db # override this because sqlite3 doesn't have autocommit # use isolation_level instead. 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 _updateRank(self, network, channel, regexp): subfolder = None if channel == 'global' else channel if self.registryValue('keepRankInfo', subfolder, network): db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT usage_count FROM triggers WHERE regexp=?""", (regexp,)) old_count = cursor.fetchall()[0][0] cursor.execute("UPDATE triggers SET usage_count=? WHERE regexp=?", (old_count + 1, regexp,)) db.commit() def _runCommandFunction(self, irc, msg, command, action_name): """Run a command from message, as if command was sent over IRC.""" try: tokens = callbacks.tokenize(command, channel=msg.channel, network=irc.network) except SyntaxError as e: # Emulate what callbacks.py does self.log.debug('Error return: %s', utils.exnToString(e)) irc.error(format('%s, in %r (triggered by %r)', e, command, action_name)) try: self.Proxy(irc.irc, msg, tokens) except Exception as e: log.exception('Uncaught exception in function called by MessageParser:') def _checkManageCapabilities(self, irc, msg, channel): """Check if the user has any of the required capabilities to manage the regexp database.""" capabilities = self.registryValue('requireManageCapability') if capabilities: for capability in re.split(r'\s*;\s*', capabilities): if capability.startswith('channel,'): capability = capability[8:] if channel != 'global': capability = ircdb.makeChannelCapability(channel, capability) if capability and ircdb.checkCapability(msg.prefix, capability): #print "has capability:", capability return True return False else: return True def do_privmsg_notice(self, irc, msg): channel = msg.channel if not channel: return if 'batch' in msg.server_tags: parent_batches = irc.state.getParentBatches(msg) parent_batch_types = [batch.type for batch in parent_batches] if 'chathistory' in parent_batch_types: # Either sent automatically by the server upon join, # or triggered by a plugin (why?!) # Either way, replying to messages from the history would # look weird, because they may have been sent a while ago, # and we may have already answered them. # (this is the same behavior as in Owner.doPrivmsg and # PluginRegexp.doPrivmsg) return if self.registryValue('enable', channel, irc.network): actions = [] results = [] for channel in set(map(plugins.getChannel, (channel, 'global'))): db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT regexp, action FROM triggers") # Fetch results and prepend channel name or 'global'. This # prevents duplicating the following lines. results.extend([(channel,)+x for x in cursor.fetchall()]) if len(results) == 0: return max_triggers = self.registryValue('maxTriggers', channel, irc.network) for (channel, regexp, action) in results: try: for match in re.finditer(regexp, msg.args[1]): if match is not None: thisaction = action self._updateRank(irc.network, channel, regexp) for (i, j) in enumerate(match.groups()): if match.group(i+1) is not None: # Need a lambda to prevent re.sub from # interpreting backslashes in the replacement thisaction = re.sub(r'\$' + str(i+1), lambda _: match.group(i+1), thisaction) actions.append((regexp, thisaction)) if max_triggers != 0 and max_triggers == len(actions): break if max_triggers != 0 and max_triggers == len(actions): break except Exception: self.log.exception('Error while handling %r', regexp) for (regexp, action) in actions: self._runCommandFunction(irc, msg, action, regexp) def doPrivmsg(self, irc, msg): if not callbacks.addressed(irc, msg): #message is not direct command self.do_privmsg_notice(irc, msg) def doNotice(self, irc, msg): if self.registryValue('enableForNotices', msg.channel, irc.network): self.do_privmsg_notice(irc, msg) @internationalizeDocstring def add(self, irc, msg, args, channel, regexp, action): """[|global] Associates with . is only necessary if the message isn't sent on the channel itself. Action is echoed upon regexp match, with variables $1, $2, etc. being interpolated from the regexp match groups.""" if not self._checkManageCapabilities(irc, msg, channel): capabilities = self.registryValue('requireManageCapability') irc.errorNoCapability(capabilities, Raise=True) db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id, usage_count, locked FROM triggers WHERE regexp=?", (regexp,)) results = cursor.fetchall() if len(results) != 0: (id, usage_count, locked) = list(map(int, results[0])) else: locked = 0 usage_count = 0 if not locked: try: re.compile(regexp) except Exception as e: irc.error(_('Invalid python regexp: %s') % (e,)) return if ircdb.users.hasUser(msg.prefix): name = ircdb.users.getUser(msg.prefix).name else: name = msg.nick cursor.execute("""INSERT INTO triggers VALUES (NULL, ?, ?, ?, ?, ?, ?)""", (regexp, name, int(time.time()), usage_count, action, locked,)) db.commit() irc.replySuccess() else: irc.error(_('That trigger is locked.')) return add = wrap(add, ['channelOrGlobal', 'something', 'something']) @internationalizeDocstring def remove(self, irc, msg, args, channel, optlist, regexp): """[|global] [--id] ] Removes the trigger for from the triggers database. is only necessary if the message isn't sent in the channel itself. If option --id specified, will retrieve by regexp id, not content. """ if not self._checkManageCapabilities(irc, msg, channel): capabilities = self.registryValue('requireManageCapability') irc.errorNoCapability(capabilities, Raise=True) db = self.getDb(channel) cursor = db.cursor() target = 'regexp' for (option, arg) in optlist: if option == 'id': target = 'id' sql = "SELECT id, locked FROM triggers WHERE %s=?" % (target,) cursor.execute(sql, (regexp,)) results = cursor.fetchall() if len(results) != 0: (id, locked) = list(map(int, results[0])) else: irc.error(_('There is no such regexp trigger.')) return if locked: irc.error(_('This regexp trigger is locked.')) return cursor.execute("""DELETE FROM triggers WHERE id=?""", (id,)) db.commit() irc.replySuccess() remove = wrap(remove, ['channelOrGlobal', getopts({'id': '',}), 'something']) @internationalizeDocstring def lock(self, irc, msg, args, channel, regexp): """[|global] Locks the so that it cannot be removed or overwritten to. is only necessary if the message isn't sent in the channel itself. """ if not self._checkManageCapabilities(irc, msg, channel): capabilities = self.registryValue('requireManageCapability') irc.errorNoCapability(capabilities, Raise=True) db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id FROM triggers WHERE regexp=?", (regexp,)) results = cursor.fetchall() if len(results) == 0: irc.error(_('There is no such regexp trigger.')) return cursor.execute("UPDATE triggers SET locked=1 WHERE regexp=?", (regexp,)) db.commit() irc.replySuccess() lock = wrap(lock, ['channelOrGlobal', 'text']) @internationalizeDocstring def unlock(self, irc, msg, args, channel, regexp): """[|global] Unlocks the entry associated with so that it can be removed or overwritten. is only necessary if the message isn't sent in the channel itself. """ if not self._checkManageCapabilities(irc, msg, channel): capabilities = self.registryValue('requireManageCapability') irc.errorNoCapability(capabilities, Raise=True) db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT id FROM triggers WHERE regexp=?", (regexp,)) results = cursor.fetchall() if len(results) == 0: irc.error(_('There is no such regexp trigger.')) return cursor.execute("UPDATE triggers SET locked=0 WHERE regexp=?", (regexp,)) db.commit() irc.replySuccess() unlock = wrap(unlock, ['channelOrGlobal', 'text']) @internationalizeDocstring def show(self, irc, msg, args, channel, optlist, regexp): """[|global] [--id] Looks up the value of in the triggers database. is only necessary if the message isn't sent in the channel itself. If option --id specified, will retrieve by regexp id, not content. """ db = self.getDb(channel) cursor = db.cursor() target = 'regexp' for (option, arg) in optlist: if option == 'id': target = 'id' sql = "SELECT regexp, action FROM triggers WHERE %s=?" % (target,) cursor.execute(sql, (regexp,)) results = cursor.fetchall() if len(results) != 0: (regexp, action) = results[0] else: irc.error(_('There is no such regexp trigger.')) return irc.reply("The action for regexp trigger \"%s\" is \"%s\"" % (regexp, action)) show = wrap(show, ['channelOrGlobal', getopts({'id': '',}), 'something']) @internationalizeDocstring def info(self, irc, msg, args, channel, optlist, regexp): """[|global] [--id] Display information about in the triggers database. is only necessary if the message isn't sent in the channel itself. If option --id specified, will retrieve by regexp id, not content. """ db = self.getDb(channel) cursor = db.cursor() target = 'regexp' for (option, arg) in optlist: if option == 'id': target = 'id' sql = "SELECT * FROM triggers WHERE %s=?" % (target,) cursor.execute(sql, (regexp,)) results = cursor.fetchall() if len(results) != 0: (id, regexp, added_by, added_at, usage_count, action, locked) = results[0] else: irc.error(_('There is no such regexp trigger.')) return irc.reply(_("The regexp id is %d, regexp is \"%s\", and action is" " \"%s\". It was added by user %s on %s, has been " "triggered %d times, and is %s.") % (id, regexp, action, added_by, time.strftime(conf.supybot.reply.format.time(), time.localtime(int(added_at))), usage_count, locked and _("locked") or _("not locked"),)) info = wrap(info, ['channelOrGlobal', getopts({'id': '',}), 'something']) @internationalizeDocstring def list(self, irc, msg, args, channel): """[|global] Lists regexps present in the triggers database. is only necessary if the message isn't sent in the channel itself. Regexp ID listed in parentheses. """ db = self.getDb(channel) cursor = db.cursor() cursor.execute("SELECT regexp, id FROM triggers ORDER BY id") results = cursor.fetchall() if len(results) != 0: regexps = results else: irc.reply(_('There are no regexp triggers in the database.')) return s = [ "%s: %s" % (ircutils.bold('#'+str(regexp[1])), regexp[0]) for regexp in regexps ] separator = self.registryValue('listSeparator', channel, irc.network) irc.reply(separator.join(s)) list = wrap(list, ['channelOrGlobal']) @internationalizeDocstring def rank(self, irc, msg, args, channel): """[|global] Returns a list of top-ranked regexps, sorted by usage count (rank). The number of regexps returned is set by the rankListLength registry value. is only necessary if the message isn't sent in the channel itself. """ numregexps = self.registryValue('rankListLength', channel, irc.network) db = self.getDb(channel) cursor = db.cursor() cursor.execute("""SELECT regexp, usage_count FROM triggers ORDER BY usage_count DESC LIMIT ?""", (numregexps,)) regexps = cursor.fetchall() if len(regexps) == 0: irc.reply(_('There are no regexp triggers in the database.')) return s = [ "#%d \"%s\" (%d)" % (i+1, regexp[0], regexp[1]) for i, regexp in enumerate(regexps) ] irc.reply(", ".join(s)) rank = wrap(rank, ['channelOrGlobal']) @internationalizeDocstring def vacuum(self, irc, msg, args, channel): """[|global] Vacuums the database for . See SQLite vacuum doc here: http://www.sqlite.org/lang_vacuum.html is only necessary if the message isn't sent in the channel itself. First check if user has the required capability specified in plugin config requireVacuumCapability. """ capability = self.registryValue('requireVacuumCapability') if capability: if not ircdb.checkCapability(msg.prefix, capability): irc.errorNoCapability(capability, Raise=True) db = self.getDb(channel) cursor = db.cursor() cursor.execute("""VACUUM""") db.commit() irc.replySuccess() vacuum = wrap(vacuum, ['channelOrGlobal']) MessageParser = internationalizeDocstring(MessageParser) Class = MessageParser # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ����������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MessageParser/test.py���������������������������������������������������0000644�0001750�0001750�00000026742�14535072470�017706� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2010, Daniel Folkinshteyn # Copyright (c) 2010-2021, 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 * import sqlite3 class MessageParserTestCase(ChannelPluginTestCase): plugins = ('MessageParser','Utilities','User') #utilities for the 'echo' #user for register for testVacuum def testAdd(self): self.assertError('messageparser add') #no args self.assertError('messageparser add "stuff"') #no action arg self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertRegexp('messageparser show "stuff"', '.*i saw some stuff.*') self.assertError('messageparser add "[a" "echo stuff"') #invalid regexp self.assertError('messageparser add "(a" "echo stuff"') #invalid regexp self.assertNotError('messageparser add "stuff" "echo i saw no stuff"') #overwrite existing regexp self.assertRegexp('messageparser show "stuff"', '.*i saw no stuff.*') try: world.testing = False origuser = self.prefix self.prefix = 'stuff!stuff@stuff' self.assertNotError('register nottester stuff', private=True) self.assertError('messageparser add "aoeu" "echo vowels are nice"') origconf = conf.supybot.plugins.MessageParser.requireManageCapability() conf.supybot.plugins.MessageParser.requireManageCapability.setValue('') self.assertNotError('messageparser add "aoeu" "echo vowels are nice"') finally: world.testing = True self.prefix = origuser conf.supybot.plugins.MessageParser.requireManageCapability.setValue(origconf) def testGroups(self): self.assertNotError('messageparser add "this (.+) a(.*)" "echo $1 $2"') self.feedMsg('this is a foo') self.assertResponse(' ', 'is foo') self.feedMsg('this is a') self.assertResponse(' ', 'is') self.assertNotError('messageparser remove "this (.+) a(.*)"') self.assertNotError('messageparser add "this (.+) a(.*)" "echo $1"') self.feedMsg('this is a foo') self.assertResponse(' ', 'is') self.feedMsg('this is a') self.assertResponse(' ', 'is') self.assertNotError('messageparser remove "this (.+) a(.*)"') self.assertNotError('messageparser add "this( .+)? a(.*)" "echo $1 $2"') self.feedMsg('this a foo') self.assertResponse(' ', '$1 foo') self.feedMsg('this a') self.assertResponse(' ', '$1') self.assertNotError('messageparser remove "this( .+)? a(.*)"') def testSyntaxError(self): self.assertNotError(r'messageparser add "test" "echo foo \" bar"') self.feedMsg('test') self.assertResponse( ' ', r"""Error: No closing quotation, in """ r"""'echo foo " bar' (triggered by 'test')""") def testMatchedBackslashes(self): # Makes sure backslashes in matched arguments are not interpreted # (re.sub interprets them in the repl argument for some reason...) self.assertNotError(r'messageparser add test(.*)test "echo $1"') self.feedMsg(r'testhello\xhellotest') self.assertResponse(' ', r'hello\xhello') def testShow(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertRegexp('messageparser show "nostuff"', 'there is no such regexp trigger') self.assertRegexp('messageparser show "stuff"', '.*i saw some stuff.*') self.assertRegexp('messageparser show --id 1', '.*i saw some stuff.*') def testInfo(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertRegexp('messageparser info "nostuff"', 'there is no such regexp trigger') self.assertRegexp('messageparser info "stuff"', '.*i saw some stuff.*') self.assertRegexp('messageparser info --id 1', '.*i saw some stuff.*') self.assertRegexp('messageparser info "stuff"', 'has been triggered 0 times') self.feedMsg('this message has some stuff in it') self.getMsg(' ') self.assertRegexp('messageparser info "stuff"', 'has been triggered 1 times') def testTrigger(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.irc.feedMsg(ircmsgs.IrcMsg( prefix=self.prefix, command='PRIVMSG', args=(self.channel, 'this message has some stuff in it'))) m = self.getMsg(' ') self.assertTrue(str(m).startswith('PRIVMSG #test :i saw some stuff')) def testIgnoreChathistory(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('+123', 'chathistory', self.channel))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123'}, prefix=self.prefix, command='PRIVMSG', args=(self.channel, 'this message has some stuff in it'))) self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('-123',))) m = self.getMsg(' ') self.assertFalse(m) def testMaxTriggers(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertNotError('messageparser add "sbd" "echo i saw somebody"') self.feedMsg('this message issued by sbd has some stuff in it') m = self.getMsg(' ') self.assertTrue(str(m).startswith('PRIVMSG #test :i saw some')) m = self.getMsg(' ') self.assertTrue(str(m).startswith('PRIVMSG #test :i saw some')) with conf.supybot.plugins.messageparser.maxtriggers.context(1): self.feedMsg('this message issued by sbd has some stuff in it') m = self.getMsg(' ') self.assertTrue(str(m).startswith('PRIVMSG #test :i saw some')) m = self.getMsg(' ') self.assertFalse(m) def testLock(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertNotError('messageparser lock "stuff"') self.assertError('messageparser add "stuff" "echo some other stuff"') self.assertError('messageparser remove "stuff"') self.assertRegexp('messageparser info "stuff"', 'is locked') def testUnlock(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertNotError('messageparser lock "stuff"') self.assertError('messageparser remove "stuff"') self.assertNotError('messageparser unlock "stuff"') self.assertRegexp('messageparser info "stuff"', 'is not locked') self.assertNotError('messageparser remove "stuff"') def testRank(self): self.assertRegexp('messageparser rank', r'There are no regexp triggers in the database\.') self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertRegexp('messageparser rank', r'#1 "stuff" \(0\)') self.assertNotError('messageparser add "aoeu" "echo vowels are nice!"') self.assertRegexp('messageparser rank', r'#1 "stuff" \(0\), #2 "aoeu" \(0\)') self.feedMsg('instead of asdf, dvorak has aoeu') self.getMsg(' ') self.assertRegexp('messageparser rank', r'#1 "aoeu" \(1\), #2 "stuff" \(0\)') def testList(self): self.assertRegexp('messageparser list', r'There are no regexp triggers in the database\.') self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertRegexp('messageparser list', '\x02#1\x02: stuff') self.assertNotError('messageparser add "aoeu" "echo vowels are nice!"') self.assertRegexp('messageparser list', '\x02#1\x02: stuff, \x02#2\x02: aoeu') def testRemove(self): self.assertError('messageparser remove "stuff"') self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertNotError('messageparser lock "stuff"') self.assertError('messageparser remove "stuff"') self.assertNotError('messageparser unlock "stuff"') self.assertNotError('messageparser remove "stuff"') self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertNotError('messageparser remove --id 1') def testVacuum(self): self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.assertNotError('messageparser remove "stuff"') self.assertNotError('messageparser vacuum') # disable world.testing since we want new users to not # magically be endowed with the admin capability try: world.testing = False original = self.prefix self.prefix = 'stuff!stuff@stuff' self.assertNotError('register nottester stuff', private=True) self.assertError('messageparser vacuum') orig = conf.supybot.plugins.MessageParser.requireVacuumCapability() conf.supybot.plugins.MessageParser.requireVacuumCapability.setValue('') self.assertNotError('messageparser vacuum') finally: world.testing = True self.prefix = original conf.supybot.plugins.MessageParser.requireVacuumCapability.setValue(orig) def testKeepRankInfo(self): orig = conf.supybot.plugins.MessageParser.keepRankInfo() try: conf.supybot.plugins.MessageParser.keepRankInfo.setValue(False) self.assertNotError('messageparser add "stuff" "echo i saw some stuff"') self.feedMsg('instead of asdf, dvorak has aoeu') self.getMsg(' ') self.assertRegexp('messageparser info "stuff"', 'has been triggered 0 times') finally: conf.supybot.plugins.MessageParser.keepRankInfo.setValue(orig) self.feedMsg('this message has some stuff in it') self.getMsg(' ') self.assertRegexp('messageparser info "stuff"', 'has been triggered 1 times') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/�������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014476� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/__init__.py��������������������������������������������������������0000644�0001750�0001750�00000004373�14535072470�016614� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Miscellaneous 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/config.py����������������������������������������������������������0000644�0001750�0001750�00000010043�14535072470�016311� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Misc') 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('Misc', True) Misc = conf.registerPlugin('Misc') conf.registerChannelValue(Misc, 'mores', registry.PositiveInteger(1, _("""Determines how many messages the bot will issue when using the 'more' command."""))) conf.registerGlobalValue(Misc, 'listPrivatePlugins', registry.Boolean(False, _("""Determines whether the bot will list private plugins with the list command if given the --private switch. If this is disabled, non-owner users should be unable to see what private plugins are loaded."""))) conf.registerGlobalValue(Misc, 'customHelpString', registry.String('', _("""Sets a custom help string, displayed when the 'help' command is called without arguments."""))) conf.registerGlobalValue(Misc, 'listUnloadedPlugins', registry.Boolean(False, _("""Determines whether the bot will list unloaded plugins with the list command if given the --unloaded switch. If this is disabled, non-owner users should be unable to see what unloaded plugins are available."""))) conf.registerGlobalValue(Misc, 'timestampFormat', registry.String('[%H:%M:%S]', _("""Determines the format string for timestamps in the Misc.last command. Refer to the Python documentation for the time module to see what formats are accepted. If you set this variable to the empty string, the timestamp will not be shown."""))) conf.registerGroup(Misc, 'last') conf.registerGroup(Misc.last, 'nested') conf.registerChannelValue(Misc.last.nested, 'includeTimestamp', registry.Boolean(False, _("""Determines whether or not the timestamp will be included in the output of last when it is part of a nested command"""))) conf.registerChannelValue(Misc.last.nested, 'includeNick', registry.Boolean(False, _("""Determines whether or not the nick will be included in the output of last when it is part of a nested command"""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/locales/�����������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016120� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/locales/de.po������������������������������������������������������0000644�0001750�0001750�00000031621�14535072470�017051� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:40+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:46 msgid "" "Determines how many messages the bot\n" " will issue when using the 'more' command." msgstr "" #: config.py:49 msgid "" "Determines whether the bot will list private\n" " plugins with the list command if given the --private switch. If this " "is\n" " disabled, non-owner users should be unable to see what private plugins\n" " are loaded." msgstr "" "Legt fest ob der Bot private Plugins mit dem list Befehl auflisten soll, " "wenn man den --private Schalter angibt. Falls das deaktiviert ist, ist es " "nicht Inhabern unmöglich zu sehen ob private Plugins geladen sind." #: config.py:54 msgid "" "Sets a custom help string, displayed when the 'help'\n" " command is called without arguments." msgstr "" #: config.py:57 #, fuzzy msgid "" "Determines whether the bot will list unloaded\n" " plugins with the list command if given the --unloaded switch. If this " "is\n" " disabled, non-owner users should be unable to see what unloaded plugins\n" " are available." msgstr "" "Legt fest ob der Bot private Plugins mit dem list Befehl auflisten soll, " "wenn man den --private Schalter angibt. Falls das deaktiviert ist, ist es " "nicht Inhabern unmöglich zu sehen ob private Plugins geladen sind." #: config.py:62 msgid "" "Determines the format string for\n" " timestamps in the Misc.last command. Refer to the Python documentation\n" " for the time module to see what formats are accepted. If you set this\n" " variable to the empty string, the timestamp will not be shown." msgstr "" "Legt das Format der Zeichenkette für Zeitstempel im Misc.last Befehl fest. " "Schau in die Python Dokumentation für das time Modul um zu sehen welche " "Formate unterstützt sind. Falls du diese Variable auf eine leere " "Zeichenkette setzt wird kein Zeitstempel angezeigt." #: config.py:69 msgid "" "Determines whether or not\n" " the timestamp will be included in the output of last when it is part of " "a\n" " nested command" msgstr "" "Legt fest ob der Zeitstempel mit in der Ausgabe stehen soll, wenn der letzte " "Befehl teil eines verschachtelten Befehls war." #: config.py:73 msgid "" "Determines whether or not the\n" " nick will be included in the output of last when it is part of a nested\n" " command" msgstr "" "Legt fest ob der Nick mit in der Ausgabe stehen soll, wenn der letzte Befehl " "teil eines verschachtelten Befehls war." #: plugin.py:76 msgid "" "Miscellaneous commands to access Supybot core. This is a core\n" " Supybot plugin that should not be removed!" msgstr "" #: plugin.py:114 #, fuzzy msgid "" "You've given me %s invalid commands within the last %i seconds; I'm now " "ignoring you for %s." msgstr "" "Du hast mir %s nicht zulässige Befehle innerhalb der letzten Minute gegeben. " "Ich werde dich für %s ignorieren." #: plugin.py:157 msgid "" "The %q plugin is loaded, but there is no command named %q in it. Try \"list " "%s\" to see the commands in the %q plugin." msgstr "" "Das %q Plugin ist geladen, aber es besitzt keinen Befehl mit dem Namen %q. " "Versuche \"list %s\" um die Befehle des %q Plugins zu sehen." #: plugin.py:163 plugin.py:166 msgid "command" msgstr "" #: plugin.py:172 msgid "private" msgstr "" #: plugin.py:188 #, fuzzy msgid "" "[--private] [--unloaded] []\n" "\n" " Lists the commands available in the given plugin. If no plugin is\n" " given, lists the public plugins available. If --private is given,\n" " lists the private plugins. If --unloaded is given, it will list\n" " available plugins that are not loaded.\n" " " msgstr "" "[--private] []\n" "\n" "Listet die Befehl auf die das gegebene Plugin anbietet. Falls kein Plugin " "angegeben wird, werden alle verfügbaren öffentliche Befehle aufgelistet. " "Falls --private angegeben wird, werden alle privaten Plugins aufgelistet." #: plugin.py:209 msgid "--private and --unloaded are incompatible options." msgstr "" #: plugin.py:237 msgid "There are no private plugins." msgstr "Es gibt keine privaten Plugins." #: plugin.py:239 msgid "There are no public plugins." msgstr "Es gibt keine öffentlichen Plugins." #: plugin.py:246 msgid "" "That plugin exists, but has no commands. This probably means that it has " "some configuration variables that can be changed in order to modify its " "behavior. Try \"config list supybot.plugins.%s\" to see what configuration " "variables it has." msgstr "" "Dieses Plugin existiert, hat aber keine Befehle. Das heißt womöglich das es " "einige Konfigurationsvariablen hat, die verändert werden können um das " "Verhalten zu beeinflussen. Probiere \"config list supybot.plugins.%s\" um zu " "sehen welche Konfigurationsvariablen es hat." #: plugin.py:258 msgid "" "\n" "\n" " Searches for in the commands currently offered by the bot,\n" " returning a list of the commands containing that string.\n" " " msgstr "" "\n" "\n" "Sucht nach in den Befehlen die der Bot momentan anbietet, " "gibt eine Liste der Befehle zurück in denen die Zeichenkette vorkommt." #: plugin.py:277 msgid "No appropriate commands were found." msgstr "Kein passender Befehl gefunden." #: plugin.py:282 #, fuzzy msgid "" "[] []\n" "\n" " This command gives a useful description of what does.\n" " is only necessary if the command is in more than one " "plugin.\n" "\n" " You may also want to use the 'list' command to list all available\n" " plugins and commands.\n" " " msgstr "" "[] []\n" "\n" "Gibt eine nützliche Beschreibung was der tut. wird nur " "benötigt, falls es den Befehl in mehr wie einem Plugin gibt." #: plugin.py:295 msgid "" "Use the 'list' command to list all plugins, and 'list ' to list all " "commands in a plugin. To show the help of a command, use 'help '. " msgstr "" #: plugin.py:306 msgid "" "That command exists in the %L plugins. Please specify exactly which plugin " "command you want help with." msgstr "" "Den Befehl gibt es in diesen %L Plugins. Bitte gibt genau an mit welchen " "Plugin Befehl du Hilfe benötigst." #: plugin.py:315 msgid "There is no command %q." msgstr "Es gibt keinen Befehl %q." #: plugin.py:319 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its help using 'plugin help {0}' and its provided commands using 'list {0}'." msgstr "" #: plugin.py:326 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its provided commands using 'list {0}'." msgstr "" #: plugin.py:337 msgid "" "takes no arguments\n" "\n" " Returns the version of the current bot.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt die momentane Bot Version aus." #: plugin.py:354 msgid "The newest versions available online are %s." msgstr "Die neuste online Version ist %s." #: plugin.py:355 msgid "%s (in %s)" msgstr "%s (in %s)" #: plugin.py:359 msgid "I couldn't fetch the newest version from the Limnoria repository." msgstr "" "Ich konnte mir die neuste Version nicht aus dem Limnoria Verzeichnis holen." #: plugin.py:361 #, fuzzy msgid "" "The current (running) version of this Limnoria is %s, running on Python %s. " "%s" msgstr "Die momentane (laufende) Version von Supybot ist %s. %s" #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Returns a URL saying where to get Limnoria.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt eine URL zurück die dir sagt wo man Limnoria bekommt." #: plugin.py:373 #, fuzzy msgid "My source is at https://github.com/progval/Limnoria" msgstr "Mein Quellcode ist auf https://github.com/ProgVal/Limnoria" #: plugin.py:378 msgid "" "[]\n" "\n" " If the last command was truncated due to IRC message length\n" " limitations, returns the next chunk of the result of the last " "command.\n" " If is given, it takes the continuation of the last command " "from\n" " instead of the person sending this message.\n" " " msgstr "" "[]\n" "\n" "Falls die Ausgabe des letzten Befehls abgeschnitten wurde, wegen der IRC " "Nachrichtenlänge beschränkung, wird der nexte Teil der Ausgabe des letzten " "Befehls ausgegben. Falls angegeben wird, wird die AUsgabe des letzten " "Befehls von weitergeführt, anstatt der Person die den Befehl sendete." #: plugin.py:395 msgid "%s has no public mores." msgstr "%s hat keine öffentlichen Nachrichten mehr." #: plugin.py:398 msgid "Sorry, I can't find any mores for %s" msgstr "Sorry, Ich kann nicht mehr für %s finden" #: plugin.py:403 msgid "" "You haven't asked me a command; perhaps you want to see someone else's " "more. To do so, call this command with that person's nick." msgstr "" "Du hast mir keinen Befehl geben; eventuell willst du mehr Nachrichten einer " "anderen Person sehen. Um das zu ereichen, rufe diesen Befehl auf mit dem " "Nicknamen der Person auf." #: plugin.py:433 msgid "That's all, there is no more." msgstr "Das ist alles, mehr ist nicht da." #: plugin.py:443 msgid "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Returns the last message matching the given criteria. --from " "requires\n" " a nick from whom the message came; --in requires a channel the " "message\n" " was sent to; --on requires a network the message was sent on; --" "with\n" " requires some string that had to be in the message; --regexp " "requires\n" " a regular expression the message must match; --nolimit returns all\n" " the messages that can be found. By default, the channel this " "command is\n" " given in is searched.\n" " " msgstr "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" "Gibt die letzte Nachricht aus, die auf die gegeben Kriterien passt. --from " "benötigt den Nick von dem die Nachricht kam; --in benötigt den Kanal in den " "die Nachricht gesendet wurde; --on benötigt das Netzwerk in dem die " "Nachricht gesendet wurde; --with benötigt eine Zeichenkette die in der " "Nachricht vorkommt; --regexp benötigt einen regulären Ausdruck auf der auf " "die Nachricht zutrifft; --nolimit zeigt alle Nachrichten die gefunden " "wurden. Voreinstellung: Der Kanal in dem der Befehl gegeben wird, wird " "durchsucht." #: plugin.py:540 msgid "The regular expression timed out." msgstr "" #: plugin.py:553 msgid "" "I couldn't find a message matching that criteria in my history of %s " "messages." msgstr "" "I konnte keine Nachricht in meiner Geschichte von %s Nachrichten finden die " "auf die Kriterien passt." #: plugin.py:572 msgid "Hey, just give the command. No need for the tell." msgstr "Junge, gib mir einfach den Befehl." #: plugin.py:577 msgid "You just told me, why should I tell myself?" msgstr "Das hast du mir gerade gesagt, wieso sollte ich es mir selbst sagen?" #: plugin.py:582 msgid "I haven't seen %s, I'll let you do the telling." msgstr "Ich habe %s nicht gesehen, sag es ihm selbst." #: plugin.py:587 msgid "%s wants me to tell you: %s" msgstr "%s will dir folgendes sagen: %s" #: plugin.py:593 msgid "" " \n" "\n" " Tells the whatever is. Use nested commands to your\n" " benefit here.\n" " " msgstr "" " \n" "\n" "Sagt den Inhalt von . Benutze verschachtelte Befehle um nutzen " "daraus zu ziehen." #: plugin.py:603 #, fuzzy msgid "" " \n" "\n" " Tells the whatever is, in a notice. Use nested\n" " commands to your benefit here.\n" " " msgstr "" " \n" "\n" "Sagt den Inhalt von . Benutze verschachtelte Befehle um nutzen " "daraus zu ziehen." #: plugin.py:613 msgid "" "takes no arguments\n" "\n" " Checks to see if the bot is alive.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Überprüft ob der Bot noch am Leben ist." #: plugin.py:617 msgid "pong" msgstr "Pong" #: plugin.py:621 msgid "" "[] [--match-case]\n" "\n" " Returns the nick of someone on the channel whose nick begins with " "the\n" " given .\n" " defaults to the current channel." msgstr "" #: plugin.py:627 msgid "I'm not even in %s." msgstr "" #: plugin.py:639 msgid "No such nick." msgstr "" #: plugin.py:645 #, fuzzy msgid "" "takes no arguments\n" "\n" " Clears all mores for the current network." msgstr "" "hat keine Argumente\n" "\n" "Gibt die momentane Bot Version aus." #, fuzzy #~ msgid "1 more message" #~ msgstr "1 mehr Nachricht" #, fuzzy #~ msgid "%i more messages" #~ msgstr "%i mehr Nachrichten" ���������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/locales/fi.po������������������������������������������������������0000644�0001750�0001750�00000034332�14535072470�017061� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Misc plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Misc plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14:51+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:46 msgid "" "Determines how many messages the bot\n" " will issue when using the 'more' command." msgstr "" "Määrittää 'more'-komentoa käytettäessä annettavien\n" " viestien määrän." #: config.py:49 msgid "" "Determines whether the bot will list private\n" " plugins with the list command if given the --private switch. If this " "is\n" " disabled, non-owner users should be unable to see what private plugins\n" " are loaded." msgstr "" "Määrittää luetteleeko botti yksityiset lisäosat\n" " \"list\" komennolla jos --private valitsin on annettu. Jos tämä on " "poistettu\n" " käytöstä, ei-omistaja käyttäjien pitäisi olla kykenemättömiä näkemään " "mitkä\n" " yksityiset lisäosat ovat ladattuina." #: config.py:54 msgid "" "Sets a custom help string, displayed when the 'help'\n" " command is called without arguments." msgstr "" "Asettaa mukautetun ohjeen, joka näytetään pyydettäessä 'help'-komentoa ilman " "parametrejä." #: config.py:57 msgid "" "Determines whether the bot will list unloaded\n" " plugins with the list command if given the --unloaded switch. If this " "is\n" " disabled, non-owner users should be unable to see what unloaded plugins\n" " are available." msgstr "" "Määrittää luetteleeko botti lataamattomat lisäosat\n" " \"list\" komennolla jos --unloaded valitsin on annettu. Jos tämä on " "poistettu käytöstä,\n" " ei-omistaja käyttäjien pitäisi olla kykenemättömiä näkemään mitkä " "lisäosat eivät ole\n" " ladattuja." #: config.py:62 msgid "" "Determines the format string for\n" " timestamps in the Misc.last command. Refer to the Python documentation\n" " for the time module to see what formats are accepted. If you set this\n" " variable to the empty string, the timestamp will not be shown." msgstr "" "Määrittää muotoketjun\n" " aikaleimoille Misc.last komennossa. Katso Python dokumentaatioon\n" " aikamoduulin kohdalle nähdäksesi mitkä aikamuodot hyväksytään. Jos " "asetat tämän asetuksen\n" " tyhjäksi, niin aikaleimaa ei näytetä." #: config.py:69 msgid "" "Determines whether or not\n" " the timestamp will be included in the output of last when it is part of " "a\n" " nested command" msgstr "" "Määrittää sisällytetäänkö\n" " aikeleima \"last\" komennon ulostuloon, kun se on osa\n" " sisäkkäisiä komentoja." #: config.py:73 msgid "" "Determines whether or not the\n" " nick will be included in the output of last when it is part of a nested\n" " command" msgstr "" "Määrittää sisällytetäänkö\n" " sisällytetäänkö nimimerkki \"last\" komennon ulostuloon, kun se on osa " "sisäkkäisiä\n" " komentoja." #: plugin.py:76 msgid "" "Miscellaneous commands to access Supybot core. This is a core\n" " Supybot plugin that should not be removed!" msgstr "" "Sekalaisia komentoja Supybotin ytimeen pääsemiseksi. Tänä on ydin Supybot-" "plugini, jota ei pitäisi poistaa." #: plugin.py:114 msgid "" "You've given me %s invalid commands within the last %i seconds; I'm now " "ignoring you for %s." msgstr "" "Olet antanut minulle %s epäkelvollista komentoa minuutin sisällä; Minä jätän " "sinut nyt huomioitta ajaksi %s." #: plugin.py:157 msgid "" "The %q plugin is loaded, but there is no command named %q in it. Try \"list " "%s\" to see the commands in the %q plugin." msgstr "" "Lisäosa %q on ladattu, mutta siinä ei ole %q nimistä komentoa. Käytä " "komentoa \"list %s\" mähdäksesi kaikki komennot lisäosassa %q." #: plugin.py:163 plugin.py:166 msgid "command" msgstr "komento" #: plugin.py:172 msgid "private" msgstr "yksityinen" #: plugin.py:188 msgid "" "[--private] [--unloaded] []\n" "\n" " Lists the commands available in the given plugin. If no plugin is\n" " given, lists the public plugins available. If --private is given,\n" " lists the private plugins. If --unloaded is given, it will list\n" " available plugins that are not loaded.\n" " " msgstr "" "[--private] []\n" "\n" " Luettelee kaikki komennot, jotka ovat saatavilla annetussa " "lisäosassa. Jos lisäosaa ei ole annettu,\n" " luettelee kaikki julkiset lisäosat. Jos --private on annettu,\n" " luettelee kaikki yksityiset lisäosat.\n" " " #: plugin.py:209 msgid "--private and --unloaded are incompatible options." msgstr "--private ja --unloaded ovat epäyhteensopivia asetuksia." #: plugin.py:237 msgid "There are no private plugins." msgstr "Yksityisiä lisäosia ei ole." #: plugin.py:239 msgid "There are no public plugins." msgstr "Julkisia lisäosia ei ole." #: plugin.py:246 msgid "" "That plugin exists, but has no commands. This probably means that it has " "some configuration variables that can be changed in order to modify its " "behavior. Try \"config list supybot.plugins.%s\" to see what configuration " "variables it has." msgstr "" "Tuo lisäosa on olemassa, mutta siinä ei ole komentoja. Tämä tarkoittaa " "luultavasti sitä, että sillä on joitain asetusarvoja, joita voidaan muuttaa " "sen käyttäytymisen muokkaamiseksi. Käytä komentoa \"config list supybot." "plugins.%s\" nähdäksesi mitä asetusarvoja sillä on." #: plugin.py:258 msgid "" "\n" "\n" " Searches for in the commands currently offered by the bot,\n" " returning a list of the commands containing that string.\n" " " msgstr "" "\n" "\n" " Etsii komennoista joita botti tarjoaa tällä " "hetkellä,\n" " palauttaen listan komennoista, jotka sisältävät sen merkkiketjun.\n" " " #: plugin.py:277 msgid "No appropriate commands were found." msgstr "Sopivia komentoja ei löytynyt." #: plugin.py:282 msgid "" "[] []\n" "\n" " This command gives a useful description of what does.\n" " is only necessary if the command is in more than one " "plugin.\n" "\n" " You may also want to use the 'list' command to list all available\n" " plugins and commands.\n" " " msgstr "" "[] []\n" "\n" " Tämä komento antaa hyödyllisen kuvauksen mitä tekee.\n" " on vaadittu vain jos komento on useammassa kuin yhdessä " "lisäosassa.\n" " 'list'-komentoa voidaan myös käyttää kaikkien saatavilla olevien lisä-osien " "ja komentojen luettelointiin.\n" " " #: plugin.py:295 msgid "" "Use the 'list' command to list all plugins, and 'list ' to list all " "commands in a plugin. To show the help of a command, use 'help '. " msgstr "" #: plugin.py:306 msgid "" "That command exists in the %L plugins. Please specify exactly which plugin " "command you want help with." msgstr "" "Tuo komento on %L lisäosassa. Ole hyvä ja määritä minkä komennon kanssa " "haluat apua." #: plugin.py:315 msgid "There is no command %q." msgstr "Komentoa %q ei ole." #: plugin.py:319 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its help using 'plugin help {0}' and its provided commands using 'list {0}'." msgstr "" #: plugin.py:326 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its provided commands using 'list {0}'." msgstr "" #: plugin.py:337 msgid "" "takes no arguments\n" "\n" " Returns the version of the current bot.\n" " " msgstr "" "Ei ota parametrejä\n" "\n" " Palauttaa nykyisen botin version.\n" " " #: plugin.py:354 msgid "The newest versions available online are %s." msgstr "Uusimmat verkossa olevat versiot ovat %s." #: plugin.py:355 msgid "%s (in %s)" msgstr "%s (%s:ssa)" #: plugin.py:359 msgid "I couldn't fetch the newest version from the Limnoria repository." msgstr "Minä en voinut tarkistaa uusinta versiota Limnorian pakettivarastosta." #: plugin.py:361 #, fuzzy msgid "" "The current (running) version of this Limnoria is %s, running on Python %s. " "%s" msgstr "" "Tällä hetkellä (käynnissä) oleva versio tästä Supybotista on %s, Python, " "jota suoritetaan Pythonin versiolla %s. %s" #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Returns a URL saying where to get Limnoria.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa URL:n, joka kertoo mistä Limnorian saa.\n" " " #: plugin.py:373 #, fuzzy msgid "My source is at https://github.com/progval/Limnoria" msgstr "Minun lähdekoodini on osoitteessa: https://github.com/ProgVal/Limnoria" #: plugin.py:378 msgid "" "[]\n" "\n" " If the last command was truncated due to IRC message length\n" " limitations, returns the next chunk of the result of the last " "command.\n" " If is given, it takes the continuation of the last command " "from\n" " instead of the person sending this message.\n" " " msgstr "" "[]\n" "\n" " Jos viestiä lyhennetään IRC viestin pituusrajoitusten takia,\n" " palauttaa seuraavan pätkän edellistä komentoa.\n" " Jos on annettu, se ottaa jatkon\n" " komennon antaneen henkilön sijasta.\n" " " #: plugin.py:395 msgid "%s has no public mores." msgstr "%s:llä ei ole julkista jatkoa." #: plugin.py:398 msgid "Sorry, I can't find any mores for %s" msgstr "Anteeksi, en voi löytää jatkoa %s:lle." #: plugin.py:403 msgid "" "You haven't asked me a command; perhaps you want to see someone else's " "more. To do so, call this command with that person's nick." msgstr "" "Et ole pyytänyt minulta komentoa; ehkäpä tahdoit nähdä jonkun muun jatkon. " "Tehdäksesi niin, käytä tätä komentoa tuon henkilön nimimerkillä." #: plugin.py:433 msgid "That's all, there is no more." msgstr "Siinä kaikki, enempää ei ole." #: plugin.py:443 msgid "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Returns the last message matching the given criteria. --from " "requires\n" " a nick from whom the message came; --in requires a channel the " "message\n" " was sent to; --on requires a network the message was sent on; --" "with\n" " requires some string that had to be in the message; --regexp " "requires\n" " a regular expression the message must match; --nolimit returns all\n" " the messages that can be found. By default, the channel this " "command is\n" " given in is searched.\n" " " msgstr "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Palauttaa viimeisimmän vietin, joka täsmää annettuihin " "kriteereihin. --from vaatii\n" " keneltä nimimerkin, jolta viesti tuli; --in vaatii kanavan, jolta " "viesti\n" " lähetettiin; --on vaatii verkon, jossa viesti lähetettiin; --with\n" " vaatii jonkun merkkiketjun, joka täytyy olla viestissä; --regexp " "vaatii\n" " säännöllisen lausekkeen, johon viestin täytyy täsmätä; --nolimit " "palauttaa kaikki\n" " viestit, jotka löydetään. Oletuksena etsitään kanavalta, jolla\n" " komento on annettu.\n" " " #: plugin.py:540 msgid "The regular expression timed out." msgstr "Säännöllinen lauseke aiheutti aikakatkaisun." #: plugin.py:553 msgid "" "I couldn't find a message matching that criteria in my history of %s " "messages." msgstr "" "En voinut löytää viestiä, joka täsmää noihin kriteereihin %s:än viestin " "historiassa." #: plugin.py:572 msgid "Hey, just give the command. No need for the tell." msgstr "Keikari, anna vain komento. Ei tarvitse kertoa." #: plugin.py:577 msgid "You just told me, why should I tell myself?" msgstr "Sinä kerroit juuri minulle, miksi minun pitäisi kertoa itselleni?" #: plugin.py:582 msgid "I haven't seen %s, I'll let you do the telling." msgstr "En ole nähnyt %s:ää, annan sinun hoitaa kertomisen." #: plugin.py:587 msgid "%s wants me to tell you: %s" msgstr "%s haluaa minun kertovan sinulle: %s" #: plugin.py:593 msgid "" " \n" "\n" " Tells the whatever is. Use nested commands to your\n" " benefit here.\n" " " msgstr "" " \n" "\n" " Kertoo ihansama mikä on. Käytä sisäkkäisiä " "komentoja\n" " eduksesi tässä.\n" " " #: plugin.py:603 msgid "" " \n" "\n" " Tells the whatever is, in a notice. Use nested\n" " commands to your benefit here.\n" " " msgstr "" " \n" "\n" " Kertoo mitä tahansa mitä on NOTICE-viestinä. " "Käytä\n" " sisäkkäisiä komentoja hyväksesi tässä.\n" " " #: plugin.py:613 msgid "" "takes no arguments\n" "\n" " Checks to see if the bot is alive.\n" " " msgstr "" "Ei ota parametrejä\n" "\n" " Tarkistaa onko botti elossa.\n" " " #: plugin.py:617 msgid "pong" msgstr "pong" #: plugin.py:621 msgid "" "[] [--match-case]\n" "\n" " Returns the nick of someone on the channel whose nick begins with " "the\n" " given .\n" " defaults to the current channel." msgstr "" "[] [--match-case]\n" "\n" " Palauttaa jonkun kanavalla olevan nimimerkin, joka alkaa\n" " annetulla .\n" " on oletuksena nykyinen kanava." #: plugin.py:627 msgid "I'm not even in %s." msgstr "En edes ole kanavalla %s." #: plugin.py:639 msgid "No such nick." msgstr "Tuollaista nimimerkkiä ei ole." #: plugin.py:645 #, fuzzy msgid "" "takes no arguments\n" "\n" " Clears all mores for the current network." msgstr "" "Ei ota parametrejä\n" "\n" " Palauttaa nykyisen botin version.\n" " " #~ msgid "1 more message" #~ msgstr "1 viesti jäljellä" #~ msgid "%i more messages" #~ msgstr "%i viestiä jäljellä" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/locales/fr.po������������������������������������������������������0000644�0001750�0001750�00000032663�14535072470�017077� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:46 msgid "" "Determines how many messages the bot\n" " will issue when using the 'more' command." msgstr "" "Détermine combien de message le bot enverra lorsque l’on utilise la commande " "« more »." #: config.py:49 msgid "" "Determines whether the bot will list private\n" " plugins with the list command if given the --private switch. If this " "is\n" " disabled, non-owner users should be unable to see what private plugins\n" " are loaded." msgstr "" "Détermine si le bot listera les plugins privés dans la commande 'list', si " "l'option --private est donné. Si cette variable est désactivée, les " "utilisateurs non-owner ne pourront pas voir quels plugins privés sont " "chargés." #: config.py:54 msgid "" "Sets a custom help string, displayed when the 'help'\n" " command is called without arguments." msgstr "" #: config.py:57 msgid "" "Determines whether the bot will list unloaded\n" " plugins with the list command if given the --unloaded switch. If this " "is\n" " disabled, non-owner users should be unable to see what unloaded plugins\n" " are available." msgstr "" "Détermine si le bot listera les plugins déchargés dans la commande 'list', " "si l'option --unloaded est donné. Si cette variable est désactivée, les " "utilisateurs non-owner ne pourront pas voir quels plugins déchargés sont " "présents." #: config.py:62 msgid "" "Determines the format string for\n" " timestamps in the Misc.last command. Refer to the Python documentation\n" " for the time module to see what formats are accepted. If you set this\n" " variable to the empty string, the timestamp will not be shown." msgstr "" "Détermine la chaîne de formattage pour les timestamps de la commande Misc." "last. Référez-vous à la documentation de Python sur le module 'time' pour " "voir quels formats sont acceptés. Si vous définissez cette variable pour " "être une chaîne vide, le timestamp ne sera pas affiché." #: config.py:69 msgid "" "Determines whether or not\n" " the timestamp will be included in the output of last when it is part of " "a\n" " nested command" msgstr "" "Détermine si le timestamp sera inclu dans la sortie de 'last' lorsqu'il est " "dans une commande imbriquée." #: config.py:73 msgid "" "Determines whether or not the\n" " nick will be included in the output of last when it is part of a nested\n" " command" msgstr "" "Détermine si le nick est inclu dans la sortie de 'last' lorsqu'il est dans " "une commande imbriquée." #: plugin.py:76 msgid "" "Miscellaneous commands to access Supybot core. This is a core\n" " Supybot plugin that should not be removed!" msgstr "" #: plugin.py:114 msgid "" "You've given me %s invalid commands within the last %i seconds; I'm now " "ignoring you for %s." msgstr "" "Vous m'avez donné %s commandes invalides durant les %i dernières secondes, " "je vous ignore maintenant pendant %s." #: plugin.py:157 msgid "" "The %q plugin is loaded, but there is no command named %q in it. Try \"list " "%s\" to see the commands in the %q plugin." msgstr "" "Le plugin %q est chargé, mais il n'a pas de commande appelée %q. Essayez " "\"list %s\" pour voir les commandes dans le plugin %q." #: plugin.py:163 plugin.py:166 msgid "command" msgstr "commande" #: plugin.py:172 msgid "private" msgstr "" #: plugin.py:188 msgid "" "[--private] [--unloaded] []\n" "\n" " Lists the commands available in the given plugin. If no plugin is\n" " given, lists the public plugins available. If --private is given,\n" " lists the private plugins. If --unloaded is given, it will list\n" " available plugins that are not loaded.\n" " " msgstr "" "[--private] [--unloaded] []\n" "\n" "Liste les commandes disponibles dans le plugin donné. Si aucun plugin n'est " "donné, liste les plugins publics. Si --private est donné, il liste les " "plugins privés. Si --unloaded est donné, il liste les plugins disponibles " "qui ne sont pas chargés." #: plugin.py:209 msgid "--private and --unloaded are incompatible options." msgstr "--private et --unloaded ne sont pas des options compatibles." #: plugin.py:237 msgid "There are no private plugins." msgstr "Il n'y a pas de plugin privé." #: plugin.py:239 msgid "There are no public plugins." msgstr "Il n'y a pas de plugin privé." #: plugin.py:246 msgid "" "That plugin exists, but has no commands. This probably means that it has " "some configuration variables that can be changed in order to modify its " "behavior. Try \"config list supybot.plugins.%s\" to see what configuration " "variables it has." msgstr "" "Ce plugin existe, mais n'a pas de commande. Cela signifie probablement qu'il " "a des variables de configuration qui peuvent être changés pour modifier son " "comportement. Essayez \"config list supybot.plugins.%s\" pour voir quelles " "variables de configuration il a." #: plugin.py:258 msgid "" "\n" "\n" " Searches for in the commands currently offered by the bot,\n" " returning a list of the commands containing that string.\n" " " msgstr "" "\n" "\n" "Recherche la dans les commandes actuellement fournies par le bot et " "retourne une list des commandes contenant cette chaîne." #: plugin.py:277 msgid "No appropriate commands were found." msgstr "Aucune commande appropriée n'a été trouvée." #: plugin.py:282 msgid "" "[] []\n" "\n" " This command gives a useful description of what does.\n" " is only necessary if the command is in more than one " "plugin.\n" "\n" " You may also want to use the 'list' command to list all available\n" " plugins and commands.\n" " " msgstr "" "[] []\n" "\n" "Cette commande donne une description utile de ce que fait la . " " n'est nécessaire que si la commande est présente dans plus d'un " "plugin. Il se peut que vous vouliez utiliser la commande « list » pour " "lister tous les plugins et commandes disponibles." #: plugin.py:295 msgid "" "Use the 'list' command to list all plugins, and 'list ' to list all " "commands in a plugin. To show the help of a command, use 'help '. " msgstr "" #: plugin.py:306 msgid "" "That command exists in the %L plugins. Please specify exactly which plugin " "command you want help with." msgstr "" "Cette commande existe dans les plugins %L. Veuillez spécifier dans quel " "plugin se trouve la commande pour laquelle vous cherchez de l'aide." #: plugin.py:315 msgid "There is no command %q." msgstr "Il n'y a pas de commande %q." #: plugin.py:319 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its help using 'plugin help {0}' and its provided commands using 'list {0}'." msgstr "" #: plugin.py:326 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its provided commands using 'list {0}'." msgstr "" #: plugin.py:337 msgid "" "takes no arguments\n" "\n" " Returns the version of the current bot.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne la version actuelle du bot" #: plugin.py:354 msgid "The newest versions available online are %s." msgstr "Les dernières versions disponibles en ligne sont %s." #: plugin.py:355 msgid "%s (in %s)" msgstr "%s (dans %s)" #: plugin.py:359 msgid "I couldn't fetch the newest version from the Limnoria repository." msgstr "Je ne peux récupérer la dernière version sur le dépôt de Limnoria." #: plugin.py:361 #, fuzzy msgid "" "The current (running) version of this Limnoria is %s, running on Python %s. " "%s" msgstr "" "La version actuelle de ce Supybot est %s, fonctionnant sur Python %s. %s" #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Returns a URL saying where to get Limnoria.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne une URL disant où trouver Limnoria." #: plugin.py:373 #, fuzzy msgid "My source is at https://github.com/progval/Limnoria" msgstr "Ma source est disponible sur https://github.com/ProgVal/Limnoria" #: plugin.py:378 msgid "" "[]\n" "\n" " If the last command was truncated due to IRC message length\n" " limitations, returns the next chunk of the result of the last " "command.\n" " If is given, it takes the continuation of the last command " "from\n" " instead of the person sending this message.\n" " " msgstr "" "[]\n" "\n" "Si la dernière commande était tronquée par les limitations de taille des " "messages sur IRC, retourne le morceau suivant résultant de la dernière " "commande. Si le est donné, continue la dernière commande du " "plutôt que de la personne envoyant ce message." #: plugin.py:395 msgid "%s has no public mores." msgstr "%s n'a pas de 'more' public." #: plugin.py:398 msgid "Sorry, I can't find any mores for %s" msgstr "Désolé, je ne peux trouver de 'more' pour %s" #: plugin.py:403 msgid "" "You haven't asked me a command; perhaps you want to see someone else's " "more. To do so, call this command with that person's nick." msgstr "" "Vous ne m'avez donné aucune commande. Peut-être que vous voulez voir celle " "de quelqu'un d'autre. Pour cela, appelez cette commande en ajoutant le nick " "de cette personne." #: plugin.py:433 msgid "That's all, there is no more." msgstr "C'est tout, il n'y a plus de 'more'" #: plugin.py:443 msgid "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Returns the last message matching the given criteria. --from " "requires\n" " a nick from whom the message came; --in requires a channel the " "message\n" " was sent to; --on requires a network the message was sent on; --" "with\n" " requires some string that had to be in the message; --regexp " "requires\n" " a regular expression the message must match; --nolimit returns all\n" " the messages that can be found. By default, the channel this " "command is\n" " given in is searched.\n" " " msgstr "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" "Retourne le dernier message correspondant aux critères donnés. --from " "requiert le nick de la personne qui a envoyé le message ; --in requiert le " "canal sur lequel a été envoyé le message ; --with requiert une chaîne qui " "doit être dans le message --regexp requiert une expression régulière à " "laquelle le message doit correspondre ; --nolimit retourne tous les messages " "qui peuvent être trouvés. Par défaut, recherche dans les logs du canal sur " "lequel est envoyée cette commande." #: plugin.py:540 msgid "The regular expression timed out." msgstr "L'expression régulière a pris trop de temps à être évaluée." #: plugin.py:553 msgid "" "I couldn't find a message matching that criteria in my history of %s " "messages." msgstr "" "Je ne peux trouver de message correspondant à ce critère dans mon historique " "de %s messages." #: plugin.py:572 msgid "Hey, just give the command. No need for the tell." msgstr "" "Oh, contentes-toi de me donner la commande. Pas besoin d'utiliser 'tell'." #: plugin.py:577 msgid "You just told me, why should I tell myself?" msgstr "Vous venez de me le dire, pourquoi devrais-je me le dire moi-même ?" #: plugin.py:582 msgid "I haven't seen %s, I'll let you do the telling." msgstr "Je n'ai pas vu %s, je vous laisse lui dire." #: plugin.py:587 msgid "%s wants me to tell you: %s" msgstr "%s veut que je vous dise : %s" #: plugin.py:593 msgid "" " \n" "\n" " Tells the whatever is. Use nested commands to your\n" " benefit here.\n" " " msgstr "" " \n" "\n" "Dit le au . Utile si vous utilisez des commandes imbriquées." #: plugin.py:603 msgid "" " \n" "\n" " Tells the whatever is, in a notice. Use nested\n" " commands to your benefit here.\n" " " msgstr "" " \n" "\n" "Dit le à , dans une notice. Utile si vous utilisez des " "commandes imbriquées." #: plugin.py:613 msgid "" "takes no arguments\n" "\n" " Checks to see if the bot is alive.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Vérifie si le bot est encore en vie." #: plugin.py:617 msgid "pong" msgstr "pong" #: plugin.py:621 msgid "" "[] [--match-case]\n" "\n" " Returns the nick of someone on the channel whose nick begins with " "the\n" " given .\n" " defaults to the current channel." msgstr "" "[] [--match-case]\n" "Retourne le nick de quelqu'un dans le salon dont le nick commence par les " " indiquées. correspond par défaut au salon actuel." #: plugin.py:627 msgid "I'm not even in %s." msgstr "Je ne suis pas dans %s." #: plugin.py:639 msgid "No such nick." msgstr "Ce nick n'existe pas." #: plugin.py:645 #, fuzzy msgid "" "takes no arguments\n" "\n" " Clears all mores for the current network." msgstr "" "ne prend pas d'argument\n" "\n" "Retourne la version actuelle du bot" #~ msgid "1 more message" #~ msgstr "1 autre message" #~ msgid "%i more messages" #~ msgstr "%i autres messages" �����������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/locales/hu.po������������������������������������������������������0000644�0001750�0001750�00000031631�14535072470�017076� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Misc\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:15+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:46 msgid "" "Determines how many messages the bot\n" " will issue when using the 'more' command." msgstr "" #: config.py:49 msgid "" "Determines whether the bot will list private\n" " plugins with the list command if given the --private switch. If this " "is\n" " disabled, non-owner users should be unable to see what private plugins\n" " are loaded." msgstr "" "Meghatározza, hogy a bot listázza-e a privát bővítményeket a list " "paranccsal, ha a --private kapcsoló meg van adva. Ha ez le van tiltva, a " "tulajdonoson kívül más felhasználók nem láthatják, hogy milyen privát " "bővítmények vannak betöltve." #: config.py:54 msgid "" "Sets a custom help string, displayed when the 'help'\n" " command is called without arguments." msgstr "" #: config.py:57 #, fuzzy msgid "" "Determines whether the bot will list unloaded\n" " plugins with the list command if given the --unloaded switch. If this " "is\n" " disabled, non-owner users should be unable to see what unloaded plugins\n" " are available." msgstr "" "Meghatározza, hogy a bot listázza-e a privát bővítményeket a list " "paranccsal, ha a --private kapcsoló meg van adva. Ha ez le van tiltva, a " "tulajdonoson kívül más felhasználók nem láthatják, hogy milyen privát " "bővítmények vannak betöltve." #: config.py:62 msgid "" "Determines the format string for\n" " timestamps in the Misc.last command. Refer to the Python documentation\n" " for the time module to see what formats are accepted. If you set this\n" " variable to the empty string, the timestamp will not be shown." msgstr "" "Meghatározza az időbélyegek formátumát a Misc.last parancsban. Hivatkozz a " "Python dokumentációra a time modulhoz, hogy lásd, milyen formátumok " "fogadhatók el. Ha üres karakterláncra állítód ezt a változót, az időbélyegző " "nem lesz megjelenítve." #: config.py:69 msgid "" "Determines whether or not\n" " the timestamp will be included in the output of last when it is part of " "a\n" " nested command" msgstr "" "Meghatározza, hogy a last kimenete tartalmazza-e az időbélyegzőt, ha az egy " "beágyazott parancs része." #: config.py:73 msgid "" "Determines whether or not the\n" " nick will be included in the output of last when it is part of a nested\n" " command" msgstr "" "Meghatározza, hogy a last kimenete tartalmazza-e a nevet, ha az egy " "beágyazott parancs része." #: plugin.py:76 msgid "" "Miscellaneous commands to access Supybot core. This is a core\n" " Supybot plugin that should not be removed!" msgstr "" #: plugin.py:114 #, fuzzy msgid "" "You've given me %s invalid commands within the last %i seconds; I'm now " "ignoring you for %s." msgstr "" "%s érvénytelen parancsot adtál nekem az utolsó percben; most mellőzlek %s-ig." #: plugin.py:157 msgid "" "The %q plugin is loaded, but there is no command named %q in it. Try \"list " "%s\" to see the commands in the %q plugin." msgstr "" "A %q bővítmény be van töltve, de nincs benne parancs %q névvel. Próbáld meg " "a \"list %s\"-t, hogy lásd a parancsokat a %q bővítményben." #: plugin.py:163 plugin.py:166 msgid "command" msgstr "" #: plugin.py:172 msgid "private" msgstr "" #: plugin.py:188 #, fuzzy msgid "" "[--private] [--unloaded] []\n" "\n" " Lists the commands available in the given plugin. If no plugin is\n" " given, lists the public plugins available. If --private is given,\n" " lists the private plugins. If --unloaded is given, it will list\n" " available plugins that are not loaded.\n" " " msgstr "" "[--private] []\n" "\n" "Kiírja az elérhető parancsokat a megadott bővítményben. Ha nincs megadva " "bővítmény, kiírja az elérhető publikus bővítményeket. Ha --private meg van " "adva, kiírja a privát bővítményeket." #: plugin.py:209 msgid "--private and --unloaded are incompatible options." msgstr "" #: plugin.py:237 msgid "There are no private plugins." msgstr "Nincsenek privát bővítmények." #: plugin.py:239 msgid "There are no public plugins." msgstr "Nincsenek publikus bővítmények." #: plugin.py:246 msgid "" "That plugin exists, but has no commands. This probably means that it has " "some configuration variables that can be changed in order to modify its " "behavior. Try \"config list supybot.plugins.%s\" to see what configuration " "variables it has." msgstr "" "Ez a bővítmény létezik, de nincsenek parancsai. Ez valószínűleg azt jelenti, " "hogy van néhány konfigurációs változója, ami megváltoztatható, hogy " "módosítsd a viselkedését. Próbáld meg a \"config list supybot.plugins.%s\"-" "t, hogy lásd, milyen konfigurációs változói vannak." #: plugin.py:258 msgid "" "\n" "\n" " Searches for in the commands currently offered by the bot,\n" " returning a list of the commands containing that string.\n" " " msgstr "" "\n" "\n" "-ra keres a parancsokban, amelyeket a bot kínál, és kiírja a " "parancsokat, amelyek tartalmazzák a karakterláncot." #: plugin.py:277 msgid "No appropriate commands were found." msgstr "Nem található megfelelő parancs." #: plugin.py:282 #, fuzzy msgid "" "[] []\n" "\n" " This command gives a useful description of what does.\n" " is only necessary if the command is in more than one " "plugin.\n" "\n" " You may also want to use the 'list' command to list all available\n" " plugins and commands.\n" " " msgstr "" "[] []\n" "\n" "Ez a parancs egy hasznos leírást ad arról, hogy mit csinál . " " csak akkor szükséges, ha a parancs egynél több bővítményben van." #: plugin.py:295 msgid "" "Use the 'list' command to list all plugins, and 'list ' to list all " "commands in a plugin. To show the help of a command, use 'help '. " msgstr "" #: plugin.py:306 msgid "" "That command exists in the %L plugins. Please specify exactly which plugin " "command you want help with." msgstr "" "Ez a parancs a(z) %L bővítményekben létezik. Kérlek pontosan határozd meg, " "hogy melyik bővítmény parancsának akarod látni a segítségét." #: plugin.py:315 msgid "There is no command %q." msgstr "Nincs %q parancs." #: plugin.py:319 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its help using 'plugin help {0}' and its provided commands using 'list {0}'." msgstr "" #: plugin.py:326 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its provided commands using 'list {0}'." msgstr "" #: plugin.py:337 msgid "" "takes no arguments\n" "\n" " Returns the version of the current bot.\n" " " msgstr "" "paraméter nélküli\n" "\n" "Kiírja a bot verzióját." #: plugin.py:354 msgid "The newest versions available online are %s." msgstr "A legújabb elérhető verziók az interneten: %s." #: plugin.py:355 msgid "%s (in %s)" msgstr "%s (%s-ban)" #: plugin.py:359 msgid "I couldn't fetch the newest version from the Limnoria repository." msgstr "Nem tudtam lekérdezni a legújabb verziót a Limnoria gyűjteményből." #: plugin.py:361 #, fuzzy msgid "" "The current (running) version of this Limnoria is %s, running on Python %s. " "%s" msgstr "Az aktuális (futó) verziója ennek a Supybot-nak %s. %s" #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Returns a URL saying where to get Limnoria.\n" " " msgstr "" "paraméter nélküli\n" "Kiír egy linket, ami megmondja, honnan lehet a Limnoria-t megszerezni." #: plugin.py:373 #, fuzzy msgid "My source is at https://github.com/progval/Limnoria" msgstr "A forrásom https://github.com/ProgVal/Limnoria -ban van." #: plugin.py:378 msgid "" "[]\n" "\n" " If the last command was truncated due to IRC message length\n" " limitations, returns the next chunk of the result of the last " "command.\n" " If is given, it takes the continuation of the last command " "from\n" " instead of the person sending this message.\n" " " msgstr "" "[]\n" "\n" "Ha a legutóbbi parancs le volt rövidítve az IRC üzenethosszúság-korlátai " "miatt, kiírja a következő részét az utolsó parancs eredményének. Ha " "meg van adva, utolsó parancsának a folytatását írja ki." #: plugin.py:395 msgid "%s has no public mores." msgstr "%s-nek nincsenek publikus folytatásai." #: plugin.py:398 msgid "Sorry, I can't find any mores for %s" msgstr "Sajnálom, nem találok folytatásokat %s-hoz." #: plugin.py:403 msgid "" "You haven't asked me a command; perhaps you want to see someone else's " "more. To do so, call this command with that person's nick." msgstr "" "Nem adtál nekem parancsot; talán valaki más folyatását szeretnéd látni. Ha " "ezt szeretnéd tenni, hívd meg ezt a parancsot az adott ember nevével." #: plugin.py:433 msgid "That's all, there is no more." msgstr "Ez minden, nincs több." #: plugin.py:443 msgid "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Returns the last message matching the given criteria. --from " "requires\n" " a nick from whom the message came; --in requires a channel the " "message\n" " was sent to; --on requires a network the message was sent on; --" "with\n" " requires some string that had to be in the message; --regexp " "requires\n" " a regular expression the message must match; --nolimit returns all\n" " the messages that can be found. By default, the channel this " "command is\n" " given in is searched.\n" " " msgstr "" "[--{from,in,on,with,without,regexp} <érték>] [--nolimit]\n" "Kiírja a legutolsó üzenetet, amire illeszkedik a megadott kritérium. --from " "kér egy nevet, akitől az üzenet jött; --in kér egy csatornát, ahová az " "üzenet lett küldve; --on kér egy hálózatpt, amelyen az üzenet lett küldve; --" "with kér egy karakterlánc aminek az üzenetben kellett lennie; --regexp kér " "egy szabályos kifejezést, amelyre az üzenetnek illeszkednie kell; --nolimit " "kiírja az összes talált üzenetet. Alapértelmezésben abban a csatornában " "keres, ahol ez a parancs végre lett hajtva." #: plugin.py:540 msgid "The regular expression timed out." msgstr "" #: plugin.py:553 msgid "" "I couldn't find a message matching that criteria in my history of %s " "messages." msgstr "" "Nem találtam a kritériumra illeszkedő üzenetet a(z) %s üzenetes " "előzményeimben." #: plugin.py:572 msgid "Hey, just give the command. No need for the tell." msgstr "Haver, csak add meg a parancsot. Nem kell mondani." #: plugin.py:577 msgid "You just told me, why should I tell myself?" msgstr "Most mondtad el nekem, miért mondjam el magamnak?" #: plugin.py:582 msgid "I haven't seen %s, I'll let you do the telling." msgstr "Nem láttam %s-t, rád hagyom a mondást." #: plugin.py:587 msgid "%s wants me to tell you: %s" msgstr "%s szeretné, hogy megmondjam neked: %s" #: plugin.py:593 msgid "" " \n" "\n" " Tells the whatever is. Use nested commands to your\n" " benefit here.\n" " " msgstr "" " Megmondja -nek szöveget. Itt az előnyödre használhatod a " "beágyazott parancsokat." #: plugin.py:603 #, fuzzy msgid "" " \n" "\n" " Tells the whatever is, in a notice. Use nested\n" " commands to your benefit here.\n" " " msgstr "" " Megmondja -nek szöveget. Itt az előnyödre használhatod a " "beágyazott parancsokat." #: plugin.py:613 msgid "" "takes no arguments\n" "\n" " Checks to see if the bot is alive.\n" " " msgstr "" "paraméter nélküli\n" "\n" "Ellenőrzi, hogy a bot él-e." #: plugin.py:617 msgid "pong" msgstr "pong" #: plugin.py:621 msgid "" "[] [--match-case]\n" "\n" " Returns the nick of someone on the channel whose nick begins with " "the\n" " given .\n" " defaults to the current channel." msgstr "" #: plugin.py:627 msgid "I'm not even in %s." msgstr "" #: plugin.py:639 msgid "No such nick." msgstr "" #: plugin.py:645 #, fuzzy msgid "" "takes no arguments\n" "\n" " Clears all mores for the current network." msgstr "" "paraméter nélküli\n" "\n" "Kiírja a bot verzióját." #, fuzzy #~ msgid "1 more message" #~ msgstr "eggyel több üzenet" #, fuzzy #~ msgid "%i more messages" #~ msgstr "%i több üzenet" �������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/locales/it.po������������������������������������������������������0000644�0001750�0001750�00000032341�14535072470�017075� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-15 21:01+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:46 msgid "" "Determines how many messages the bot\n" " will issue when using the 'more' command." msgstr "" #: config.py:49 msgid "" "Determines whether the bot will list private\n" " plugins with the list command if given the --private switch. If this " "is\n" " disabled, non-owner users should be unable to see what private plugins\n" " are loaded." msgstr "" "Determina se il bot elencherà i plugin privati con il comando \"list\" se " "usato\n" " con l'opzione --private. Se questa variabile è disattivata, gli utenti " "non\n" " proprietari (owner) potranno vedere quali plugin privati sono caricati." #: config.py:54 msgid "" "Sets a custom help string, displayed when the 'help'\n" " command is called without arguments." msgstr "" #: config.py:57 msgid "" "Determines whether the bot will list unloaded\n" " plugins with the list command if given the --unloaded switch. If this " "is\n" " disabled, non-owner users should be unable to see what unloaded plugins\n" " are available." msgstr "" "Determina se il bot elencherà i plugin non caricati con il comando \"list\" " "se usato\n" " con l'opzione --unloaded. Se questa variabile è disattivata, gli utenti " "non\n" " proprietari (owner) non saranno in grado di vedere quali altri plugin " "sono\n" " disponibili." #: config.py:62 msgid "" "Determines the format string for\n" " timestamps in the Misc.last command. Refer to the Python documentation\n" " for the time module to see what formats are accepted. If you set this\n" " variable to the empty string, the timestamp will not be shown." msgstr "" "Determina il formato per i timestamp del comando Misc.last. Per sapere quali " "sono\n" " i formati validi fai riferimento alla documentazione di Python per il " "modulo time.\n" " Se si assegna una stringa vuota a questa variabile, il timestamp non " "verrà mostrato." #: config.py:69 msgid "" "Determines whether or not\n" " the timestamp will be included in the output of last when it is part of " "a\n" " nested command" msgstr "" "Determina se il timestamp verrà incluso o meno nell'output di \"last\" " "quando fa parte di un comando nidificato." #: config.py:73 msgid "" "Determines whether or not the\n" " nick will be included in the output of last when it is part of a nested\n" " command" msgstr "" "Determines se il nick verrà incluso o meno nell'output di \"last\" quando fa " "parte di un comando nidificato." #: plugin.py:76 msgid "" "Miscellaneous commands to access Supybot core. This is a core\n" " Supybot plugin that should not be removed!" msgstr "" #: plugin.py:114 #, fuzzy msgid "" "You've given me %s invalid commands within the last %i seconds; I'm now " "ignoring you for %s." msgstr "" "Mi hai fornito %s comandi non validi entro l'ultimo minuto; ti ignoro per %s." #: plugin.py:157 msgid "" "The %q plugin is loaded, but there is no command named %q in it. Try \"list " "%s\" to see the commands in the %q plugin." msgstr "" "Il plugin %q è caricato ma non ha nessun comando chiamato %q. Prova \"list %s" "\" per vedere i comandi disponibili nel plugin %q." #: plugin.py:163 plugin.py:166 msgid "command" msgstr "" #: plugin.py:172 msgid "private" msgstr "" #: plugin.py:188 msgid "" "[--private] [--unloaded] []\n" "\n" " Lists the commands available in the given plugin. If no plugin is\n" " given, lists the public plugins available. If --private is given,\n" " lists the private plugins. If --unloaded is given, it will list\n" " available plugins that are not loaded.\n" " " msgstr "" "[--private] [--unloaded] []\n" "\n" " Elenca i comandi disponibili nel dato plugin. Se nessun plugin è " "specificato,\n" " riporta l'elenco di quelli pubblici. Specificando --private vengono " "mostrati quelli\n" " privati, mentre con --unloaded quelli non caricati.\n" " " #: plugin.py:209 msgid "--private and --unloaded are incompatible options." msgstr "Le opzioni --private e --unloaded non possono essere usate insieme." #: plugin.py:237 msgid "There are no private plugins." msgstr "Non ci sono plugin privati." #: plugin.py:239 msgid "There are no public plugins." msgstr "Non ci sono plugin pubblici." #: plugin.py:246 msgid "" "That plugin exists, but has no commands. This probably means that it has " "some configuration variables that can be changed in order to modify its " "behavior. Try \"config list supybot.plugins.%s\" to see what configuration " "variables it has." msgstr "" "Il plugin esiste ma non ha comandi. Probabilmente significa che ha delle " "variabili di configurazione modificabili che cambiano il suo comportamento. " "Prova \"config list supybot.plugins.%s\" per vedere quali sono disponibili." #: plugin.py:258 msgid "" "\n" "\n" " Searches for in the commands currently offered by the bot,\n" " returning a list of the commands containing that string.\n" " " msgstr "" "\n" "\n" " Cerca nei comandi attualmente forniti dal bot,\n" " riportando un elenco di quelli che contengono la stringa.\n" " " #: plugin.py:277 msgid "No appropriate commands were found." msgstr "Non è stato trovato alcun comando appropriato." #: plugin.py:282 #, fuzzy msgid "" "[] []\n" "\n" " This command gives a useful description of what does.\n" " is only necessary if the command is in more than one " "plugin.\n" "\n" " You may also want to use the 'list' command to list all available\n" " plugins and commands.\n" " " msgstr "" "[] []\n" "\n" " Fornisce un'utile descrizione di cosa fa . è\n" " necessario solo se il comando è presente in più di un plugin.\n" " " #: plugin.py:295 msgid "" "Use the 'list' command to list all plugins, and 'list ' to list all " "commands in a plugin. To show the help of a command, use 'help '. " msgstr "" #: plugin.py:306 msgid "" "That command exists in the %L plugins. Please specify exactly which plugin " "command you want help with." msgstr "Questo comando esiste nei plugin %L. Specifica per quale vuoi aiuto." #: plugin.py:315 msgid "There is no command %q." msgstr "Non c'è nessun comando %q." #: plugin.py:319 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its help using 'plugin help {0}' and its provided commands using 'list {0}'." msgstr "" #: plugin.py:326 msgid "" " However, '{0}' is the name of a loaded plugin, and you may be able to find " "its provided commands using 'list {0}'." msgstr "" #: plugin.py:337 msgid "" "takes no arguments\n" "\n" " Returns the version of the current bot.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta la versione attuale del bot.\n" " " #: plugin.py:354 msgid "The newest versions available online are %s." msgstr "Le versioni online più recenti sono %s." #: plugin.py:355 msgid "%s (in %s)" msgstr "%s (in %s)" #: plugin.py:359 msgid "I couldn't fetch the newest version from the Limnoria repository." msgstr "" "Non riesco a recuperare la versione più recente dal repository di Limnoria." #: plugin.py:361 #, fuzzy msgid "" "The current (running) version of this Limnoria is %s, running on Python %s. " "%s" msgstr "La versione di questo Supybot è %s. %s" #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Returns a URL saying where to get Limnoria.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta un URL che dice dove ottenere Limnoria.\n" " " #: plugin.py:373 #, fuzzy msgid "My source is at https://github.com/progval/Limnoria" msgstr "" "I miei sorgenti sono disponibili all'indirizzo https://github.com/ProgVal/" "Limnoria" #: plugin.py:378 msgid "" "[]\n" "\n" " If the last command was truncated due to IRC message length\n" " limitations, returns the next chunk of the result of the last " "command.\n" " If is given, it takes the continuation of the last command " "from\n" " instead of the person sending this message.\n" " " msgstr "" "[]\n" "\n" " Se l'ultimo comando è stato troncato a causa di limitazioni della " "lunghezza\n" " dei messaggi IRC, riporta il pezzo successivo dell'output del " "comando.\n" " Se è specificato, continua l'ultimo messaggio di " "anziché\n" " dell'utente che usa questo comando.\n" " " #: plugin.py:395 msgid "%s has no public mores." msgstr "%s non ha \"more\" pubblici." #: plugin.py:398 msgid "Sorry, I can't find any mores for %s" msgstr "Spiacente, non trovo alcun \"more\" per %s" #: plugin.py:403 msgid "" "You haven't asked me a command; perhaps you want to see someone else's " "more. To do so, call this command with that person's nick." msgstr "" "Non mi hai richiesto un comando, forse vuoi vedere quelli di qualcun altro. " "Per farlo usa questo comando con il nick di quell'utente." #: plugin.py:433 msgid "That's all, there is no more." msgstr "È tutto, non c'è nessun \"more\"." #: plugin.py:443 msgid "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Returns the last message matching the given criteria. --from " "requires\n" " a nick from whom the message came; --in requires a channel the " "message\n" " was sent to; --on requires a network the message was sent on; --" "with\n" " requires some string that had to be in the message; --regexp " "requires\n" " a regular expression the message must match; --nolimit returns all\n" " the messages that can be found. By default, the channel this " "command is\n" " given in is searched.\n" " " msgstr "" "[--{from,in,on,with,without,regexp} ] [--nolimit]\n" "\n" " Riporta l'ultimo messaggio corrispondente al dato criterio. --from " "richiede un\n" " nick da cui è partito il messaggio; --in un canale dove il messaggio " "è stato\n" " inviato; --on una rete sulla quale il messaggio è stato inviato; --" "with necessita\n" " una stringa che deve essere nel messaggio; --regexp un'espressione " "regolare\n" " alla quale deve corrispondere il messaggio; --nolimit riporta tutti " "i messaggi\n" " trovati. Per impostazione predefinita ricerca nel canale in cui è " "stato dato il comando.\n" " " #: plugin.py:540 msgid "The regular expression timed out." msgstr "L'espressione regolare è scaduta." #: plugin.py:553 msgid "" "I couldn't find a message matching that criteria in my history of %s " "messages." msgstr "" "Non trovo un messaggio corrispondente a questo criterio nella cronologia di " "%s messaggi." #: plugin.py:572 msgid "Hey, just give the command. No need for the tell." msgstr "Dammi il comando, non c'è bisogno di usare \"tell\"." #: plugin.py:577 msgid "You just told me, why should I tell myself?" msgstr "Me l'hai appena detto, perché dovrei ripetermelo?" #: plugin.py:582 msgid "I haven't seen %s, I'll let you do the telling." msgstr "Non ho mai visto %s, lascio a te l'invio del messaggio." #: plugin.py:587 msgid "%s wants me to tell you: %s" msgstr "%s vuole che ti dica: %s" #: plugin.py:593 msgid "" " \n" "\n" " Tells the whatever is. Use nested commands to your\n" " benefit here.\n" " " msgstr "" " \n" "\n" " Dice a . Utilizza i comandi nidificati a tuo " "vantaggio.\n" " " #: plugin.py:603 #, fuzzy msgid "" " \n" "\n" " Tells the whatever is, in a notice. Use nested\n" " commands to your benefit here.\n" " " msgstr "" " \n" "\n" " Dice a . Utilizza i comandi nidificati a tuo " "vantaggio.\n" " " #: plugin.py:613 msgid "" "takes no arguments\n" "\n" " Checks to see if the bot is alive.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Controlla che il bot sia ancora vivo.\n" " " #: plugin.py:617 msgid "pong" msgstr "pong" #: plugin.py:621 msgid "" "[] [--match-case]\n" "\n" " Returns the nick of someone on the channel whose nick begins with " "the\n" " given .\n" " defaults to the current channel." msgstr "" "[] [--match-case]\n" "\n" " Restituisce i nick presenti in canale che cominciano con il dato " "pattern\n" " specificato da .\n" " è quello corrente." #: plugin.py:627 msgid "I'm not even in %s." msgstr "Non sono in %s." #: plugin.py:639 msgid "No such nick." msgstr "Nessun nick trovato." #: plugin.py:645 #, fuzzy msgid "" "takes no arguments\n" "\n" " Clears all mores for the current network." msgstr "" "non necessita argomenti\n" "\n" " Riporta la versione attuale del bot.\n" " " #, fuzzy #~ msgid "1 more message" #~ msgstr "altro messaggio" #, fuzzy #~ msgid "%i more messages" #~ msgstr "%i altri messaggi" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/plugin.py����������������������������������������������������������0000644�0001750�0001750�00000071716�14535072470�016360� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2022, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import os import sys import json import time import queue import functools import multiprocessing import supybot import supybot.conf as conf from supybot import commands import supybot.utils as utils from supybot.commands import * from supybot.commands import ProcessTimeoutError import supybot.ircdb as ircdb import supybot.irclib as irclib import supybot.utils.minisix as minisix import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks import supybot.registry as registry from supybot import commands from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Misc') if minisix.PY2: from itertools import ifilter as filter def getPluginsInDirectory(directory): # get modules in a given directory plugins = [] for filename in os.listdir(directory): pluginPath = os.path.join(directory, filename) if os.path.isdir(pluginPath): if all(os.path.isfile(os.path.join(pluginPath, x)) for x in ['__init__.py', 'config.py', 'plugin.py']): plugins.append(filename) return plugins class RegexpTimeout(Exception): pass class Misc(callbacks.Plugin): """Miscellaneous commands to access Supybot core. This is a core Supybot plugin that should not be removed!""" def __init__(self, irc): self.__parent = super(Misc, self) self.__parent.__init__(irc) self.invalidCommands = \ ircutils.FloodQueue(conf.supybot.abuse.flood.interval()) conf.supybot.abuse.flood.interval.addCallback(self.setFloodQueueTimeout) def setFloodQueueTimeout(self, *args, **kwargs): self.invalidCommands.timeout = conf.supybot.abuse.flood.interval() def callPrecedence(self, irc): return ([cb for cb in irc.callbacks if cb is not self], []) def invalidCommand(self, irc, msg, tokens): assert not msg.repliedTo, 'repliedTo msg in Misc.invalidCommand.' assert self is irc.callbacks[-1], 'Misc isn\'t last callback.' assert msg.command in ('PRIVMSG', 'NOTICE') self.log.debug('Misc.invalidCommand called (tokens %s)', tokens) # First, we check for invalidCommand floods. This is rightfully done # here since this will be the last invalidCommand called, and thus it # will only be called if this is *truly* an invalid command. maximum = conf.supybot.abuse.flood.command.invalid.maximum() self.invalidCommands.enqueue(msg) if self.invalidCommands.len(msg) > maximum and \ conf.supybot.abuse.flood.command.invalid() and \ not ircdb.checkCapability(msg.prefix, 'trusted'): punishment = conf.supybot.abuse.flood.command.invalid.punishment() banmask = '*!%s@%s' % (msg.user, msg.host) self.log.info('Ignoring %s for %s seconds due to an apparent ' 'invalid command flood.', banmask, punishment) if tokens and tokens[0] == 'Error:': self.log.warning('Apparent error loop with another Supybot ' 'observed. Consider ignoring this bot ' 'permanently.') ircdb.ignores.add(banmask, time.time() + punishment) if conf.supybot.abuse.flood.command.invalid.notify(): irc.reply(_('You\'ve given me %s invalid commands within the last ' '%i seconds; I\'m now ignoring you for %s.') % (maximum, conf.supybot.abuse.flood.interval(), utils.timeElapsed(punishment, seconds=False))) return # Now, for normal handling. channel = msg.channel # Only bother with the invaildCommand flood handling if it's actually # enabled if conf.supybot.abuse.flood.command.invalid(): # First, we check for invalidCommand floods. This is rightfully done # here since this will be the last invalidCommand called, and thus it # will only be called if this is *truly* an invalid command. maximum = conf.supybot.abuse.flood.command.invalid.maximum() banmasker = conf.supybot.protocols.irc.banmask.makeBanmask if self.invalidCommands.len(msg) > maximum and \ not ircdb.checkCapability(msg.prefix, 'trusted') and \ msg.prefix != irc.prefix and \ ircutils.isUserHostmask(msg.prefix): penalty = conf.supybot.abuse.flood.command.invalid.punishment() banmask = banmasker(msg.prefix, channel=channel, network=irc.network) self.log.info('Ignoring %s for %s seconds due to an apparent ' 'invalid command flood.', banmask, penalty) if tokens and tokens[0] == 'Error:': self.log.warning('Apparent error loop with another Supybot ' 'observed. Consider ignoring this bot ' 'permanently.') ircdb.ignores.add(banmask, time.time() + penalty) if conf.supybot.abuse.flood.command.invalid.notify(): irc.reply('You\'ve given me %s invalid commands within ' 'the last minute; I\'m now ignoring you for %s.' % (maximum, utils.timeElapsed(penalty, seconds=False))) return # Now, for normal handling. if conf.supybot.reply.whenNotCommand.getSpecific( irc.network, channel)(): if len(tokens) >= 2: cb = irc.getCallback(tokens[0]) if cb: plugin = cb.name() irc.error(format(_('The %q plugin is loaded, but there is ' 'no command named %q in it. Try "list ' '%s" to see the commands in the %q ' 'plugin.'), plugin, tokens[1], plugin, plugin)) else: irc.errorInvalid(_('command'), tokens[0], repr=False) else: command = tokens and tokens[0] or '' irc.errorInvalid(_('command'), command, repr=False) else: if tokens: # echo [] will get us an empty token set, but there's no need # to log this in that case anyway, it being a nested command. self.log.info('Not replying to %s in %s, not a command.', tokens[0], channel or _('private')) if irc.nested: bracketConfig = conf.supybot.commands.nested.brackets brackets = bracketConfig.getSpecific(irc.network, channel)() if brackets: (left, right) = brackets irc.reply(left + ' '.join(tokens) + right) else: pass # Let's just do nothing, I can't think of better. def isPublic(self, cb): name = cb.name() return conf.supybot.plugins.get(name).public() @internationalizeDocstring def list(self, irc, msg, args, optlist, cb): """[--private] [--unloaded] [] Lists the commands available in the given plugin. If no plugin is given, lists the public plugins available. If --private is given, lists the private plugins. If --unloaded is given, it will list available plugins that are not loaded. """ private = False unloaded = False for (option, argument) in optlist: if option == 'private': private = True if not self.registryValue('listPrivatePlugins') and \ not ircdb.checkCapability(msg.prefix, 'owner'): irc.errorNoCapability('owner') elif option == 'unloaded': unloaded = True if not self.registryValue('listUnloadedPlugins') and \ not ircdb.checkCapability(msg.prefix, 'owner'): irc.errorNoCapability('owner') if unloaded and private: irc.error(_('--private and --unloaded are incompatible options.')) return if not cb: if unloaded: # We were using the path of Misc + .. to detect the install # directory. However, it fails if Misc is not in the # installation directory for some reason, so we use a # supybot module. installedPluginsDirectory = os.path.join( os.path.dirname(conf.__file__), 'plugins') plugins = getPluginsInDirectory(installedPluginsDirectory) for directory in conf.supybot.directories.plugins()[:]: plugins.extend(getPluginsInDirectory(directory)) # Remove loaded plugins: loadedPlugins = [x.name() for x in irc.callbacks] plugins = [x for x in plugins if x not in loadedPlugins] plugins.sort() irc.reply(format('%L', plugins)) else: names = [cb.name() for cb in irc.callbacks if (private and not self.isPublic(cb)) or (not private and self.isPublic(cb))] names.sort() if names: irc.reply(format('%L', names)) else: if private: irc.reply(_('There are no private plugins.')) else: irc.reply(_('There are no public plugins.')) else: commands = cb.listCommands() if commands: commands.sort() irc.reply(format('%L', commands)) else: irc.reply(format(_('That plugin exists, but has no commands. ' 'This probably means that it has some ' 'configuration variables that can be ' 'changed in order to modify its behavior. ' 'Try "config list supybot.plugins.%s" to see ' 'what configuration variables it has.'), cb.name())) list = wrap(list, [getopts({'private':'', 'unloaded':''}), additional('plugin')]) @internationalizeDocstring def apropos(self, irc, msg, args, s): """ Searches for in the commands currently offered by the bot, returning a list of the commands containing that string. """ commands = {} L = [] for cb in irc.callbacks: if isinstance(cb, callbacks.Plugin): for command in cb.listCommands(): if s in command: commands.setdefault(command, []).append(cb.name()) for (key, names) in commands.items(): for name in names: L.append('%s %s' % (name, key)) if L: L.sort() irc.reply(format('%L', L)) else: irc.reply(_('No appropriate commands were found.')) apropos = wrap(apropos, ['lowered']) @internationalizeDocstring def help(self, irc, msg, args, command): """[] [] This command gives a useful description of what does. is only necessary if the command is in more than one plugin. You may also want to use the 'list' command to list all available plugins and commands. """ if not command: cHelp = self.registryValue("customHelpString") if cHelp: irc.reply(cHelp) else: irc.reply(_( "Use the 'list' command to list all plugins, and " "'list ' to list all commands in a plugin. " "To show the help of a command, use 'help '. " )) return command = list(map(callbacks.canonicalName, command)) (maxL, cbs) = irc.findCallbacksForArgs(command) if maxL == command: if len(cbs) > 1: names = sorted([cb.name() for cb in cbs]) irc.error(format(_('That command exists in the %L plugins. ' 'Please specify exactly which plugin command ' 'you want help with.'), names)) else: assert cbs, 'Odd, maxL == command, but no cbs.' irc.reply(_.__call__(cbs[0].getCommandHelp(command, False))) else: plugins = [cb.name() for cb in irc.callbacks if self.isPublic(cb)] s = format(_('There is no command %q.'), callbacks.formatCommand(command)) if command[0].lower() in map(str.lower, plugins): if "Plugin" in plugins: template = _( " However, '{0}' is the name of a loaded plugin, and " "you may be able to find its help using " "'plugin help {0}' and its provided commands using " "'list {0}'." ) else: template = _( " However, '{0}' is the name of a loaded plugin, and " "you may be able to find its provided commands using " "'list {0}'." ) s += template.format(command[0].title()) irc.error(s) help = wrap(help, [any('something')]) @internationalizeDocstring def version(self, irc, msg, args): """takes no arguments Returns the version of the current bot. """ try: newestUrl = 'https://api.github.com/repos/progval/Limnoria/' + \ 'commits/%s' versions = {} for branch in ('master', 'testing'): data = json.loads(utils.web.getUrl(newestUrl % branch) .decode('utf8')) version = data['commit']['committer']['date'] # Strip the last 'Z': version = version.rsplit('T', 1)[0].replace('-', '.') if minisix.PY2 and isinstance(version, unicode): version = version.encode('utf8') versions[branch] = version newest = _('The newest versions available online are %s.') % \ ', '.join([_('%s (in %s)') % (y,x) for x,y in versions.items()]) except utils.web.Error as e: self.log.info('Couldn\'t get website version: %s', e) newest = _('I couldn\'t fetch the newest version ' 'from the Limnoria repository.') s = _('The current (running) version of this Limnoria is %s, ' 'running on Python %s. %s') % \ (conf.version, sys.version.replace('\n', ' '), newest) irc.reply(s) version = wrap(thread(version)) @internationalizeDocstring def source(self, irc, msg, args): """takes no arguments Returns a URL saying where to get Limnoria. """ irc.reply(_('My source is at https://github.com/progval/Limnoria')) source = wrap(source) @internationalizeDocstring def more(self, irc, msg, args, nick): """[] If the last command was truncated due to IRC message length limitations, returns the next chunk of the result of the last command. If is given, it takes the continuation of the last command from instead of the person sending this message. """ if '!' in msg.prefix and '@' in msg.prefix: userHostmask = msg.prefix.split('!', 1)[1] else: userHostmask = msg.nick if nick: try: (private, L) = irc._mores[nick] if not private: irc._mores[userHostmask] = L[:] else: irc.error(_('%s has no public mores.') % nick) return except KeyError: irc.error(_('Sorry, I can\'t find any mores for %s') % nick) return try: L = irc._mores[userHostmask] except KeyError: irc.error(_('You haven\'t asked me a command; perhaps you want ' 'to see someone else\'s more. To do so, call this ' 'command with that person\'s nick.'), Raise=True) number = self.registryValue('mores', msg.channel, irc.network) if conf.supybot.protocols.irc.experimentalExtensions() \ and 'draft/multiline' in irc.state.capabilities_ack: use_multiline = True multiline_cap_values = ircutils.parseCapabilityKeyValue( irc.state.capabilities_ls['draft/multiline']) if multiline_cap_values.get('max-lines', '').isnumeric(): number = min(number, int(multiline_cap_values['max-lines'])) else: use_multiline = False msgs = L[-number:] msgs.reverse() L[-number:] = [] if msgs: if use_multiline and len(msgs) > 1: # If draft/multiline is available, use it. # TODO: set concat=True. For now we can't, because every # message has "(XX more messages)" at the end, so it would be # unreadable if the messages were concatenated irc.queueMultilineBatches(msgs, target=msgs[0].args[0], targetNick=msg.nick, concat=False) else: for msg in msgs: irc.queueMsg(msg) else: irc.error(_('That\'s all, there is no more.')) more = wrap(more, [additional('seenNick')]) def _validLastMsg(self, irc, msg): return msg.prefix and \ msg.command == 'PRIVMSG' and \ msg.channel @internationalizeDocstring def last(self, irc, msg, args, optlist): """[--{from,in,on,with,without,regexp} ] [--nolimit] Returns the last message matching the given criteria. --from requires a nick from whom the message came; --in requires a channel the message was sent to; --on requires a network the message was sent on; --with requires some string that had to be in the message; --regexp requires a regular expression the message must match; --nolimit returns all the messages that can be found. By default, the channel this command is given in is searched. """ predicates = {} nolimit = False skipfirst = True if msg.channel: predicates['in'] = lambda m: ircutils.strEqual(m.args[0], msg.channel) else: skipfirst = False final_predicates = [] for (option, arg) in optlist: if option == 'from': def f(m, arg=arg): return ircutils.hostmaskPatternEqual(arg, m.nick) predicates['from'] = f elif option == 'in': def f(m, arg=arg): return ircutils.strEqual(m.args[0], arg) predicates['in'] = f if arg != msg.channel: skipfirst = False elif option == 'on': def f(m, arg=arg): return m.receivedOn == arg predicates['on'] = f elif option == 'with': def f(m, arg=arg): return arg.lower() in m.args[1].lower() predicates.setdefault('with', []).append(f) elif option == 'without': def f(m, arg=arg): return arg.lower() not in m.args[1].lower() predicates.setdefault('without', []).append(f) elif option == 'regexp': def f(messages, arg=arg): reobj = re.compile(arg) # using a queue to return results, so we can return at # least some results in case of timeout q = multiprocessing.Queue() def p(messages): for m in messages: if ircmsgs.isAction(m): s = ircmsgs.unAction(m) else: s = m.args[1] if reobj.search(s): q.put(m) try: process(p, messages, timeout=3., pn=self.name(), cn='last') except ProcessTimeoutError: pass results = [] while True: try: results.append(q.get(False)) except queue.Empty: break return results final_predicates.append(f) elif option == 'nolimit': nolimit = True iterable = filter(functools.partial(self._validLastMsg, irc), reversed(irc.state.history)) if skipfirst: # Drop the first message only if our current channel is the same as # the channel we've been instructed to look at. next(iterable) predicates = list(utils.iter.flatten(predicates.values())) # Make sure the user can't get messages from channels they aren't in def userInChannel(m): return m.args[0] in irc.state.channels \ and msg.nick in irc.state.channels[m.args[0]].users predicates.append(userInChannel) # Make sure the user can't get messages from a +s channel unless # they're calling the command from that channel or from a query # TODO: support statusmsg, but be careful about leaking scopes. def notSecretMsg(m): return not irc.isChannel(msg.args[0]) \ or msg.args[0] == m.args[0] \ or (m.args[0] in irc.state.channels \ and 's' not in irc.state.channels[m.args[0]].modes) predicates.append(notSecretMsg) resp = [] if irc.nested and not \ self.registryValue('last.nested.includeTimestamp'): tsf = None else: tsf = self.registryValue('timestampFormat') if irc.nested and not self.registryValue('last.nested.includeNick'): showNick = False else: showNick = True candidates = [] # Run predicates that filter on individual messages for m in iterable: for predicate in predicates: if not predicate(m): break else: candidates.append(m) # Run predicates that filter lists of messages for predicate in final_predicates: candidates = predicate(candidates) for m in candidates: if nolimit: resp.append(ircmsgs.prettyPrint(m, timestampFormat=tsf, showNick=showNick)) else: irc.reply(ircmsgs.prettyPrint(m, timestampFormat=tsf, showNick=showNick)) return if not resp: irc.error(_('I couldn\'t find a message matching that criteria in ' 'my history of %s messages.') % len(irc.state.history)) else: irc.reply(format('%L', resp)) last = wrap(last, [getopts({'nolimit': '', 'on': 'something', 'with': 'something', 'from': 'something', 'without': 'something', 'in': 'callerInGivenChannel', 'regexp': 'regexpMatcher',})]) def _tell(self, irc, msg, args, target, text, notice): if irc.nested: irc.error('This command cannot be nested.', Raise=True) if target.lower() == 'me': target = msg.nick if irc.isChannel(target): irc.error(_('Hey, just give the command. No need for the tell.')) return if not ircutils.isNick(target): irc.errorInvalid('nick', target) if ircutils.nickEqual(target, irc.nick): irc.error(_('You just told me, why should I tell myself?'), Raise=True) if target not in irc.state.nicksToHostmasks and \ not ircdb.checkCapability(msg.prefix, 'owner'): # We'll let owners do this. s = _('I haven\'t seen %s, I\'ll let you do the telling.') % target irc.error(s, Raise=True) if irc.action: irc.action = False text = '* %s %s' % (irc.nick, text) s = _('%s wants me to tell you: %s') % (msg.nick, text) irc.replySuccess() irc.reply(s, to=target, private=True, notice=notice) @internationalizeDocstring def tell(self, *args): """ Tells the whatever is. Use nested commands to your benefit here. """ self._tell(*args, notice=False) tell = wrap(tell, ['something', 'text']) @internationalizeDocstring def noticetell(self, *args): """ Tells the whatever is, in a notice. Use nested commands to your benefit here. """ self._tell(*args, notice=True) noticetell = wrap(noticetell, ['something', 'text']) @internationalizeDocstring def ping(self, irc, msg, args): """takes no arguments Checks to see if the bot is alive. """ irc.reply(_('pong'), prefixNick=False) @internationalizeDocstring def completenick(self, irc, msg, args, channel, beginning, optlist): """[] [--match-case] Returns the nick of someone on the channel whose nick begins with the given . defaults to the current channel.""" if channel not in irc.state.channels: irc.error(_('I\'m not even in %s.') % channel, Raise=True) if ('match-case', True) in optlist: def match(nick): return nick.startswith(beginning) else: beginning = beginning.lower() def match(nick): return nick.lower().startswith(beginning) for nick in irc.state.channels[channel].users: if match(nick): irc.reply(nick) return irc.error(_('No such nick.')) completenick = wrap(completenick, ['channel', 'something', getopts({'match-case':''})]) @internationalizeDocstring def clearmores(self, irc, msg, args): """takes no arguments Clears all mores for the current network.""" irc._mores.clear() irc.replySuccess() clearmores = wrap(clearmores, ['admin']) Class = Misc # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Misc/test.py������������������������������������������������������������0000644�0001750�0001750�00000042172�14535072470�016033� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re from supybot.test import * class MiscTestCase(PluginTestCase): plugins = ('Misc', 'Utilities') def testMore(self): newprefix = 'fooo!bar@baaaaaaaaaaaaaaz' # just for the sale of filling irc.state.nicksToHostmasks: self.irc.feedMsg(ircmsgs.IrcMsg(command='CHGHOST', args=(self.nick, 'baz'), prefix=self.prefix)) self.irc.feedMsg(ircmsgs.IrcMsg(command='CHGHOST', args=('foo', 'baz'), prefix='foo!bar@oldbaz')) self.assertResponse('echo %s' % ('abc '*400), 'abc '*112 + ' \x02(3 more messages)\x02', frm=newprefix) m = self.assertResponse('more', 'abc '*112 + ' \x02(2 more messages)\x02', frm=newprefix) self.assertResponse('more', 'abc '*112 + ' \x02(1 more message)\x02', frm=newprefix) self.assertResponse('more', ' '.join(['abc']*(400-112*3)), frm=newprefix) self.assertResponse('more', "Error: That's all, there is no more.", frm=newprefix) class MiscTestCase(ChannelPluginTestCase): plugins = ('Misc', 'Utilities', 'Anonymous', 'Plugin', 'Channel', 'Dict', 'User', 'String') def testReplyWhenNotCommand(self): try: original = conf.supybot.reply.whenNotCommand() conf.supybot.reply.whenNotCommand.setValue(True) self.prefix = 'somethingElse!user@host.domain.tld' self.assertRegexp('foo', 'not.*command') self.assertRegexp('foo bar baz', 'not.*command') finally: conf.supybot.reply.whenNotCommand.setValue(original) def testReplyWhenNotCommandButFirstCommandIsPluginName(self): try: original = conf.supybot.reply.whenNotCommand() conf.supybot.reply.whenNotCommand.setValue(True) self.assertRegexp('misc foo', '"list Misc"') finally: conf.supybot.reply.whenNotCommand.setValue(original) # if network: # def testNotReplyWhenRegexpsMatch(self): # try: # orig = conf.supybot.reply.whenNotCommand() # gk = conf.supybot.plugins.Gameknot.gameSnarfer() # conf.supybot.reply.whenNotCommand.setValue(True) # conf.supybot.plugins.Gameknot.gameSnarfer.setValue(True) # self.prefix = 'somethingElse!user@host.domain.tld' # self.assertSnarfNotError( # 'http://gameknot.com/chess.pl?bd=1019508') # finally: # conf.supybot.reply.whenNotCommand.setValue(orig) # conf.supybot.plugins.Gameknot.gameSnarfer.setValue(gk) def testNotReplyWhenNotCanonicalName(self): try: original = str(conf.supybot.reply.whenNotCommand) conf.supybot.reply.whenNotCommand.set('True') self.prefix = 'somethingElse!user@host.domain.tld' self.assertNotRegexp('LeN foobar', 'command') self.assertResponse('lEn foobar', '6') finally: conf.supybot.reply.whenNotCommand.set(original) def testHelp(self): self.assertHelp('help list') self.assertRegexp('help help', r'^\(\x02help') #self.assertRegexp('help misc help', r'^\(\x02misc help') self.assertError('help nonExistentCommand') def testPluginHelp(self): self.assertResponse('help Misc', 'Error: There is no command "misc". ' "However, 'Misc' is the name of a loaded plugin, " "and you may be able to find its help using " "'plugin help Misc' and its provided " "commands using 'list Misc'.") self.assertNotError('unload Plugin') self.assertResponse('help Misc', 'Error: There is no command "misc". ' "However, 'Misc' is the name of a loaded plugin, " "and you may be able to find its provided commands using " "'list Misc'.") def testHelpIncludeFullCommandName(self): self.assertHelp('help channel capability add') m = self.getMsg('help channel capability add') self.assertIn('channel capability add', m.args[1]) def testHelpDoesAmbiguityWithDefaultPlugins(self): m = self.getMsg('help list') # Misc.list and User.list. self.assertFalse(m.args[1].startswith('Error')) def testHelpIsCaseInsensitive(self): self.assertHelp('help LIST') def testList(self): self.assertNotError('list') self.assertNotError('list Misc') self.assertRegexp('list --unloaded', 'Ctcp') def testListIsCaseInsensitive(self): self.assertNotError('list misc') def testListPrivate(self): # If Anonymous changes to public, these tests will break. So if # the next assert fails, change the plugin we test for public/private # to some other non-public plugin. name = 'Anonymous' conf.supybot.plugins.Anonymous.public.setValue(False) self.assertNotRegexp('list', name) self.assertRegexp('list --private', name) conf.supybot.plugins.Anonymous.public.setValue(True) self.assertRegexp('list', name) self.assertNotRegexp('list --private', name) def testListUnloaded(self): unloadedPlugin = 'Alias' loadedPlugin = 'Anonymous' self.assertRegexp('list --unloaded', 'Alias') self.assertNotRegexp('list --unloaded', 'Anonymous') def testListDoesNotIncludeNonCanonicalName(self): self.assertNotRegexp('list Owner', '_exec') def testListNoIncludeDispatcher(self): self.assertNotRegexp('list Misc', 'misc') def testListIncludesDispatcherIfThereIsAnOriginalCommand(self): self.assertRegexp('list Dict', r'\bdict\b') if network: def testVersion(self): print('*** This test should start passing when we have our '\ 'threaded issues resolved.') self.assertNotError('version') def testSource(self): self.assertNotError('source') def testTell(self): # This test fails because the test is seeing us as owner and Misc.tell # allows the owner to send messages to people the bot hasn't seen. oldprefix, self.prefix = self.prefix, 'tester!foo@bar__no_testcap__baz' self.nick = 'tester' m = self.getMsg('tell aljsdkfh [plugin tell]') self.assertIn('let you do', m.args[1]) m = self.getMsg('tell #foo [plugin tell]') self.assertIn('No need for', m.args[1]) m = self.getMsg('tell me you love me') m = self.irc.takeMsg() self.assertEqual(m.args[0], self.nick) def testNoNestedTell(self): self.assertRegexp('echo [tell %s foo]' % self.nick, 'nested') def testTellDoesNotPropogateAction(self): m = self.getMsg('tell foo [action bar]') self.assertFalse(ircmsgs.isAction(m)) def testLast(self): orig = conf.supybot.plugins.Misc.timestampFormat() try: conf.supybot.plugins.Misc.timestampFormat.setValue('') self.feedMsg('foo bar baz') self.assertResponse('last', '<%s> foo bar baz' % self.nick) self.assertRegexp('last', r'<%s> @last' % self.nick) self.assertResponse('last --with foo', '<%s> foo bar baz' % \ self.nick) self.assertResponse('last --without foo', '<%s> @last' % self.nick) self.assertRegexp(r'last --regexp m/\s+/', r'last --without foo') self.assertResponse('last --regexp m/bar/', '<%s> foo bar baz' % self.nick) self.assertResponse('last --from %s' % self.nick.upper(), '<%s> @last --regexp m/bar/' % self.nick) self.assertResponse('last --from %s*' % self.nick[0], '<%s> @last --from %s' % (self.nick, self.nick.upper())) conf.supybot.plugins.Misc.timestampFormat.setValue('foo') self.assertSnarfNoResponse('foo bar baz', 1) self.assertResponse('last', 'foo <%s> foo bar baz' % self.nick) finally: conf.supybot.plugins.Misc.timestampFormat.setValue(orig) def testNestedLastTimestampConfig(self): tsConfig = conf.supybot.plugins.Misc.last.nested.includeTimestamp orig = tsConfig() try: tsConfig.setValue(True) self.getMsg('foo bar baz') chars = conf.supybot.reply.whenAddressedBy.chars() chars = re.escape(chars) self.assertRegexp('echo [last]', r'[%s]foo bar baz' % chars) finally: tsConfig.setValue(orig) def testNestedLastNickConfig(self): nickConfig = conf.supybot.plugins.Misc.last.nested.includeNick orig = nickConfig() try: nickConfig.setValue(True) self.getMsg('foo bar baz') chars = conf.supybot.reply.whenAddressedBy.chars() chars = re.escape(chars) self.assertRegexp('echo [last]', '<%s> [%s]foo bar baz' % (self.nick, chars)) finally: nickConfig.setValue(orig) def testMore(self): self.assertResponse('echo %s' % ('abc '*400), 'abc '*112 + ' \x02(3 more messages)\x02') self.assertResponse('more', 'abc '*112 + ' \x02(2 more messages)\x02') self.assertResponse('more', 'abc '*112 + ' \x02(1 more message)\x02') self.assertResponse('more', ' '.join(['abc']*(400-112*3))) self.assertResponse('more', "Error: That's all, there is no more.") def testMoreMores(self): with conf.supybot.plugins.Misc.mores.context(2): self.assertResponse('echo %s' % ('abc '*400), 'abc '*112 + ' \x02(3 more messages)\x02') self.assertResponse('more', 'abc '*112 + ' \x02(2 more messages)\x02') m = self.irc.takeMsg() self.assertIsNotNone(m) self.assertEqual( m.args[1], 'abc '*112 + ' \x02(1 more message)\x02') self.assertResponse('more', ' '.join(['abc']*(400-112*3))) self.assertResponse('more', "Error: That's all, there is no more.") def testMoreBatch(self): self.irc.state.capabilities_ack.add('batch') self.irc.state.capabilities_ack.add('draft/multiline') self.irc.state.capabilities_ls['draft/multiline'] = 'max-bytes=4096' with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Misc.mores.context(2): self.assertResponse('echo %s' % ('abc '*400), 'abc '*112 + ' \x02(3 more messages)\x02') self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@more", prefix=self.prefix)) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "abc " * 112 + " \x02(2 more messages)\x02"), server_tags={'batch': batch_name})) # Third message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "abc " * 112 + " \x02(1 more message)\x02"), server_tags={'batch': batch_name})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testMoreBatchMaxLines(self): self.irc.state.capabilities_ack.add('batch') self.irc.state.capabilities_ack.add('draft/multiline') self.irc.state.capabilities_ls['draft/multiline'] = \ 'max-bytes=4096,max-lines=2' with conf.supybot.protocols.irc.experimentalExtensions.context(True): with conf.supybot.plugins.Misc.mores.context(3): self.assertResponse('echo %s' % ('abc '*400), 'abc '*112 + ' \x02(3 more messages)\x02') self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@more", prefix=self.prefix)) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "abc " * 112 + " \x02(2 more messages)\x02"), server_tags={'batch': batch_name})) # Third message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "abc " * 112 + " \x02(1 more message)\x02"), server_tags={'batch': batch_name})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testClearMores(self): self.assertRegexp('echo %s' % ('abc'*700), 'more') self.assertRegexp('more', 'more') self.assertNotError('clearmores') self.assertError('more') def testInvalidCommand(self): self.assertError('echo []') def testInvalidCommands(self): with conf.supybot.abuse.flood.command.invalid.maximum.context(3): self.assertNotRegexp('foo', 'given me', frm='f!f@__no_testcap__') self.assertNotRegexp('bar', 'given me', frm='f!f@__no_testcap__') self.assertNotRegexp('baz', 'given me', frm='f!f@__no_testcap__') self.assertRegexp('qux', 'given me', frm='f!f@__no_testcap__') def testMoreIsCaseInsensitive(self): self.assertNotError('echo %s' % ('abc'*2000)) self.assertNotError('more') nick = ircutils.nickFromHostmask(self.prefix) self.assertNotError('more %s' % nick) self.assertNotError('more %s' % nick.upper()) self.assertNotError('more %s' % nick.lower()) def testApropos(self): self.assertNotError('apropos f') self.assertRegexp('apropos asldkfjasdlkfja', 'No appropriate commands') def testAproposIsNotCaseSensitive(self): self.assertNotRegexp('apropos LIST', 'No appropriate commands') def testAproposDoesntReturnNonCanonicalNames(self): self.assertNotRegexp('apropos exec', '_exec') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/���������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016517� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/__init__.py����������������������������������������������0000644�0001750�0001750�00000005130�14535072470�020625� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ Moobot factoid compatibility module. Moobot's factoids were originally designed to emulate Blootbot's factoids, so in either case, you should find this plugin comfortable. """ 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__ = "0.1" __author__ = supybot.authors.strike __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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=8 expandtab textwidth=78: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/config.py������������������������������������������������0000644�0001750�0001750�00000005264�14535072470�020343� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('MoobotFactoids') 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('MoobotFactoids', True) MoobotFactoids = conf.registerPlugin('MoobotFactoids') conf.registerChannelValue(MoobotFactoids, 'showFactoidIfOnlyOneMatch', registry.Boolean(True, _("""Determines whether or not the factoid value will be shown when a listkeys search returns only one factoid key."""))) conf.registerChannelValue(MoobotFactoids, 'mostCount', registry.Integer(10, _("""Determines how many items are shown when the 'most' command is called."""))) # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/locales/�������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�020141� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/locales/fi.po��������������������������������������������0000644�0001750�0001750�00000025731�14535072470�021105� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: MoobotFactoids plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:02+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 "" "Determines whether\n" " or not the factoid value will be shown when a listkeys search returns " "only\n" " one factoid key." msgstr "" "Määrittää näytetöönkö\n" " vaiko eikö factoidin arvo kun \"listkeys\" haku\n" " palauttaa vain yhden factoidin avaimen." #: config.py:51 msgid "" "Determines how many items are shown\n" " when the 'most' command is called." msgstr "" "Määrittää montako asiaa näytetään\n" " kun 'most' komentoa kutsutaan." #: plugin.py:288 msgid "" "\n" " An alternative to the Factoids plugin, this plugin keeps factoids in\n" " your bot.\n" "\n" " To add factoid say\n" " ``@something is something`` And when you call ``@something`` the bot " "says\n" " ``something is something``.\n" "\n" " If you want factoid to be in different format say (for example):\n" " ``@Hi is Hello`` And when you call ``@hi`` the bot says ``Hello." "``\n" "\n" " If you want the bot to use /mes with Factoids, that is possible too.\n" " ``@test is tests.`` and everytime when someone calls for\n" " ``test`` the bot answers ``* bot tests.``\n" " " msgstr "" #: plugin.py:358 msgid "%s is %s" msgstr "%s on %s" #: plugin.py:377 msgid "Factoid %q is locked." msgstr "Factoidi %q on lukittu." #: plugin.py:384 msgid "Factoid %q not found." msgstr "Factoidia %q ei löytynyt." #: plugin.py:394 msgid "Missing an 'is' or '_is_'." msgstr "Puuttuva 'is' tai '_is_'." #: plugin.py:410 msgid "Factoid %q already exists." msgstr "Factoidi %q on jo olemassa." #: plugin.py:444 msgid "%s, or %s" msgstr "%s, tai %s" #: plugin.py:465 msgid "" "[] \n" "\n" " Returns the literal factoid for the given factoid key. No parsing " "of\n" " the factoid value is done as it is with normal retrieval. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa kirjaimellisen factoidin annetulle factoidi avaimelle. " "Factoidin jäsentämistä\n" " ei tehdä, koska se on normaalilla palautuksella. on " "vaadittu vain, jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:478 msgid "" "[] \n" "\n" " Returns the various bits of info on the factoid for the given key.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa muutamia tiedonmurusia factoidista annetulle avaimelle.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:489 plugin.py:529 msgid "No such factoid: %q" msgstr "Ei sellaista factoidia: %q" #: plugin.py:498 msgid "Created by %s on %s." msgstr "luonut %s kello %s." #: plugin.py:504 msgid " Last modified by %s on %s." msgstr "Viimeeksi muokattu %s kello %s." #: plugin.py:512 msgid " Last requested by %s on %s, requested %n." msgstr "Viimeeksi pyytänyt %s kello %s, pyytänyt %n." #: plugin.py:519 msgid " Locked by %s on %s." msgstr "%s:än lukitsema %s:llä." #: plugin.py:534 msgid "Factoid %q is already locked." msgstr "Factoidi %q on jo lukittu." #: plugin.py:537 msgid "Factoid %q is not locked." msgstr "Factoidi %q is not locked." #: plugin.py:547 msgid "Cannot %s someone else's factoid unless you are an admin." msgstr "Et voi %s jonkun muun factoidia paitsi, jos olet admin." #: plugin.py:559 msgid "" "[] \n" "\n" " Locks the factoid with the given factoid key. Requires that the " "user\n" " be registered and have created the factoid originally. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Lukitsee factoidin annetulla factoidi avaimella. Vaatii käyttäjän " "olevan rekisteröitynyt\n" " ja että hän on alunperin luonut sen. on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:570 msgid "" "[] \n" "\n" " Unlocks the factoid with the given factoid key. Requires that the\n" " user be registered and have locked the factoid. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Avaa factoidin annetulla factoidi avaimella. Vaatii, että käyttäjä\n" " on rekisteröitynyt ja lukinnut factoidin. on vaadittu\n" " vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:581 msgid "" "[] {popular|authored|recent}\n" "\n" " Lists the most {popular|authored|recent} factoids. \"popular\" " "lists the\n" " most frequently requested factoids. \"authored\" lists the author " "with\n" " the most factoids. \"recent\" lists the most recently created " "factoids.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] {popular|authored|recent}\n" "\n" " Luettelee {suosituimmat (\"popular\")|kirjoitetuimmat (\"authored\")|" "viimeisimmät (\"recent\")} factoidit. \"popular\" luettelee\n" " useimmiten pyydetyt factoidit. \"authored\" luettelee kirjoittajat, " "joilla on eniten\n" " factoideja. \"recent\" luettelee uusimmat kirjoitetut factoidit.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:603 msgid "author" msgstr "kirjoittaja" #: plugin.py:605 msgid "authors" msgstr "kirjoittajat" #: plugin.py:606 msgid "Most prolific %s: %L" msgstr "Tuottoisimmat %s: %L" #: plugin.py:608 plugin.py:620 msgid "There are no factoids in my database." msgstr "Tietokannassani ei ole factoideja." #: plugin.py:615 msgid "latest factoid" msgstr "viimeisin factoidi" #: plugin.py:617 msgid "latest factoids" msgstr "viimeisimmät factoidit" #: plugin.py:618 msgid "%i %s: %L" msgstr "%i %s: %L" #: plugin.py:627 msgid "requested factoid" msgstr "pyydetty factoidi" #: plugin.py:629 msgid "requested factoids" msgstr "pyydetyt factoidit" #: plugin.py:630 msgid "Top %i %s: %L" msgstr "Huippu %i %s: %L" #: plugin.py:632 msgid "No factoids have been requested from my database." msgstr "Factoideja ei ole pyydetty tietokannastani." #: plugin.py:636 msgid "" "[] \n" "\n" " Lists the keys of the factoids with the given author. Note that if " "an\n" " author has an integer name, you'll have to use that author's id to " "use\n" " this function (so don't use integer usernames!). is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Luettelee kaikki annetun kirjailijan kirjoittamat factoidit. " "Huomaa, että\n" " jos kirjoittajalla on kokonaisluku nimi, sinun täytyy käyttää " "kirjoittajan id:tä käyttääksesi\n" " tätä toimintoa (joten älä käytä kokonaisluku käyttäjätunnuksia!). " " on vaadittu vain jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:649 msgid "No factoids by %q found." msgstr "%q:n kirjoittamia factoideja ei löytynyt." #: plugin.py:652 msgid "Author search for %q (%i found): %L" msgstr "Kirjoittaja haku %q:lle (%i löytynyt): %L" #: plugin.py:659 msgid "" "[] \n" "\n" " Lists the keys of the factoids whose key contains the provided " "text.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Luettelee kaikki factoidit, joiden avain sisältää annetun tekstin.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:667 msgid "No keys matching %q found." msgstr "Avaimia, jotka täsmäävät %q:un ei löytynyt." #: plugin.py:675 msgid "Key search for %q (%i found): %L" msgstr "Avain haku %q:lle (%i löytynyt): %L" #: plugin.py:682 msgid "" "[] \n" "\n" " Lists the keys of the factoids whose value contains the provided " "text.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Luettelee factoidien avaimet, joiden arvo sisältää annetun tekstin.\n" " on vaadittu vain, jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:690 msgid "No values matching %q found." msgstr "%q:un täsmääviä avaimia ei löytynyt." #: plugin.py:693 msgid "Value search for %q (%i found): %L" msgstr "Arvo haku %q:lle (%i löytynyt): %L" #: plugin.py:700 msgid "" "[] \n" "\n" " Deletes the factoid with the given key. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Poistaa factoidin annetulla avaimella. on vaadittu vain, " "jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:713 msgid "" "[]\n" "\n" " Displays a random factoid (along with its key) from the database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Näyttää satunnaisen factoidin (avaimensa kanssa) tietokannasta.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:721 msgid "No factoids in the database." msgstr "Tietokannassa ei ole factoideja." #~ msgid "" #~ "An alternative to the Factoids plugin, this plugin keeps factoids in\n" #~ " your bot." #~ msgstr "" #~ "Vaihtoehto Factoids-pluginille, tämä plugin säilyttää factoideja " #~ "botissasi." #~ msgid "" #~ "Add the help for \"help MoobotFactoids\" here (assuming you don't " #~ "implement a MoobotFactoids\n" #~ " command). This should describe *how* to use this plugin." #~ msgstr "" #~ "Lisää ohje \"help MoobotFactoids\":lle tähän (olettaen ettet luo " #~ "MoobotFactoids\n" #~ " komentoa). Tämän pitäisi kertoa *kuinka* tätä laajennusta käytetään." ���������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/locales/fr.po��������������������������������������������0000644�0001750�0001750�00000023436�14535072470�021116� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:47 msgid "" "Determines whether\n" " or not the factoid value will be shown when a listkeys search returns " "only\n" " one factoid key." msgstr "" "Détermine si la valeur de la factoid sera affichée lorsqu'une recherche de " "clef ne retournera qu'une seule clef." #: config.py:51 msgid "" "Determines how many items are shown\n" " when the 'most' command is called." msgstr "" "Détermine combien d'éléments sont affichés lorsque la commande 'most' est " "appelée." #: plugin.py:288 msgid "" "\n" " An alternative to the Factoids plugin, this plugin keeps factoids in\n" " your bot.\n" "\n" " To add factoid say\n" " ``@something is something`` And when you call ``@something`` the bot " "says\n" " ``something is something``.\n" "\n" " If you want factoid to be in different format say (for example):\n" " ``@Hi is Hello`` And when you call ``@hi`` the bot says ``Hello." "``\n" "\n" " If you want the bot to use /mes with Factoids, that is possible too.\n" " ``@test is tests.`` and everytime when someone calls for\n" " ``test`` the bot answers ``* bot tests.``\n" " " msgstr "" #: plugin.py:358 msgid "%s is %s" msgstr "%s est %s" #: plugin.py:377 msgid "Factoid %q is locked." msgstr "La factoid %q est verrouillée" #: plugin.py:384 msgid "Factoid %q not found." msgstr "Factoid %q non trouvée." #: plugin.py:394 msgid "Missing an 'is' or '_is_'." msgstr "Il manque un 'is' ou un '_is_'" #: plugin.py:410 msgid "Factoid %q already exists." msgstr "La factoid %q existe déjà." #: plugin.py:444 msgid "%s, or %s" msgstr "%s, ou %s" #: plugin.py:465 msgid "" "[] \n" "\n" " Returns the literal factoid for the given factoid key. No parsing " "of\n" " the factoid value is done as it is with normal retrieval. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Retourne la factoid littérale pour la clef donnée. Aucun parsage n'est " "effecté sur la valeur de la factoid. n'est nécesaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:478 msgid "" "[] \n" "\n" " Returns the various bits of info on the factoid for the given key.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Retourne différentes informations sur la factoid ayant la clef donnée. " " n'est nécesaire que si le message n'est pas envoyé sur le canal lui-" "même." #: plugin.py:489 plugin.py:529 msgid "No such factoid: %q" msgstr "Cette factoid n'existe pas : %q" #: plugin.py:498 msgid "Created by %s on %s." msgstr "Créé par %s le %s" #: plugin.py:504 msgid " Last modified by %s on %s." msgstr "Dernière modification par %s le %s" #: plugin.py:512 msgid " Last requested by %s on %s, requested %n." msgstr "Dernière requete par %s le %s ; a demandé %n." #: plugin.py:519 msgid " Locked by %s on %s." msgstr "Verrouillé par %s le %s" #: plugin.py:534 msgid "Factoid %q is already locked." msgstr "La factoid %q est déjà bloquée." #: plugin.py:537 msgid "Factoid %q is not locked." msgstr "La factoid %q n'est pas bloquée." #: plugin.py:547 msgid "Cannot %s someone else's factoid unless you are an admin." msgstr "" "Impossible de %s la factoid de quelqu'un d'autre à moins d'être un admin." #: plugin.py:559 msgid "" "[] \n" "\n" " Locks the factoid with the given factoid key. Requires that the " "user\n" " be registered and have created the factoid originally. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Verrouille la factoid ayant la clef donnée. Requiert que l'utilisateur soit " "enregistré et ait créé la factoid. n'est nécesaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:570 msgid "" "[] \n" "\n" " Unlocks the factoid with the given factoid key. Requires that the\n" " user be registered and have locked the factoid. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Verrouille la factoid ayant la clef donnée. Requiert que l'utilisateur soit " "enregistré et ait verrouillé la factoid. n'est nécesaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:581 msgid "" "[] {popular|authored|recent}\n" "\n" " Lists the most {popular|authored|recent} factoids. \"popular\" " "lists the\n" " most frequently requested factoids. \"authored\" lists the author " "with\n" " the most factoids. \"recent\" lists the most recently created " "factoids.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] {popular|authored|recent}\n" "\n" "Liste les factoids selon un classement. \"popular\" correspond aux plus " "affichées ; \"authored\" liste les auteurs qui envoient le plus de factoids " "\"recent\" liste les factoids les plus récentes. n'est nécesaire que " "si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:603 msgid "author" msgstr "auteur" #: plugin.py:605 msgid "authors" msgstr "auteurs" #: plugin.py:606 msgid "Most prolific %s: %L" msgstr "%s ayant posté le plus de factoids : %L" #: plugin.py:608 plugin.py:620 msgid "There are no factoids in my database." msgstr "Il n'y a pas de factoid dans ma base de données." #: plugin.py:615 msgid "latest factoid" msgstr "dernière factoid" #: plugin.py:617 msgid "latest factoids" msgstr "dernières factoids" #: plugin.py:618 msgid "%i %s: %L" msgstr "%i %s : %L" #: plugin.py:627 msgid "requested factoid" msgstr "factoid la plus demandée" #: plugin.py:629 msgid "requested factoids" msgstr "factoids les plus demandées" #: plugin.py:630 msgid "Top %i %s: %L" msgstr "Top des %i %s : %L" #: plugin.py:632 msgid "No factoids have been requested from my database." msgstr "Aucune factoid n'a été demandée dans ma base de données." #: plugin.py:636 msgid "" "[] \n" "\n" " Lists the keys of the factoids with the given author. Note that if " "an\n" " author has an integer name, you'll have to use that author's id to " "use\n" " this function (so don't use integer usernames!). is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Liste les clefs de l'auteur donné. Notez que si l'auteur a un nom qui est un " "nombre entier, vous devrez donner l'ID de l'auteur pour utiliser cette " "fonction (donc, n'utilisez pas de nombres entiers comme noms " "d'utilisateur !) n'est nécesaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:649 msgid "No factoids by %q found." msgstr "Aucune factoid par %q ne peut être trouvée." #: plugin.py:652 msgid "Author search for %q (%i found): %L" msgstr "Recherche d'auteur pour %q (%i trouvé(s)) : %L" #: plugin.py:659 msgid "" "[] \n" "\n" " Lists the keys of the factoids whose key contains the provided " "text.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Liste les clefs des factoids dont la clef contient le texte fourni. " "n'est nécessaire que si la commande n'est pas envoyée sur le canal lui-même." #: plugin.py:667 msgid "No keys matching %q found." msgstr "Aucune factoid correspondant à %q trouvée." #: plugin.py:675 msgid "Key search for %q (%i found): %L" msgstr "Recherche de clef pour %q (%i trouvée(s)) : %L" #: plugin.py:682 msgid "" "[] \n" "\n" " Lists the keys of the factoids whose value contains the provided " "text.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Liste les clefs dont la valeur contient le texte recherché. n'est " "nécesaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:690 msgid "No values matching %q found." msgstr "Aucune valeur correspondant à %q trouvée." #: plugin.py:693 msgid "Value search for %q (%i found): %L" msgstr "Recherche de valeurs pour %q (%i trouvée(s)) : %L" #: plugin.py:700 msgid "" "[] \n" "\n" " Deletes the factoid with the given key. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Supprime la factoid avec la clef donnée. n'est nécesaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:713 msgid "" "[]\n" "\n" " Displays a random factoid (along with its key) from the database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Affiche une factoid aléatoire (avec sa clef) de la base de données. " "n'est nécesaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:721 msgid "No factoids in the database." msgstr "Aucune factoid dans la base de données." ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/locales/it.po��������������������������������������������0000644�0001750�0001750�00000023513�14535072470�021117� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-17 16:39+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:47 msgid "" "Determines whether\n" " or not the factoid value will be shown when a listkeys search returns " "only\n" " one factoid key." msgstr "" "Determina se il valore del factoid verrà mostrato o meno quando una ricerca " "restituisce una sola chiave." #: config.py:51 msgid "" "Determines how many items are shown\n" " when the 'most' command is called." msgstr "Determina quanti elementi mostrare con il comando \"most\"." #: plugin.py:288 msgid "" "\n" " An alternative to the Factoids plugin, this plugin keeps factoids in\n" " your bot.\n" "\n" " To add factoid say\n" " ``@something is something`` And when you call ``@something`` the bot " "says\n" " ``something is something``.\n" "\n" " If you want factoid to be in different format say (for example):\n" " ``@Hi is Hello`` And when you call ``@hi`` the bot says ``Hello." "``\n" "\n" " If you want the bot to use /mes with Factoids, that is possible too.\n" " ``@test is tests.`` and everytime when someone calls for\n" " ``test`` the bot answers ``* bot tests.``\n" " " msgstr "" #: plugin.py:358 msgid "%s is %s" msgstr "%s è %s" #: plugin.py:377 msgid "Factoid %q is locked." msgstr "Il factoid %q è bloccato." #: plugin.py:384 msgid "Factoid %q not found." msgstr "Factoid %q non trovato." #: plugin.py:394 msgid "Missing an 'is' or '_is_'." msgstr "Manca un 'is' o un '_is_'." #: plugin.py:410 msgid "Factoid %q already exists." msgstr "Il factoid %q esiste già." #: plugin.py:444 msgid "%s, or %s" msgstr "%s, o %s" #: plugin.py:465 msgid "" "[] \n" "\n" " Returns the literal factoid for the given factoid key. No parsing " "of\n" " the factoid value is done as it is with normal retrieval. " "\n" " is only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Restituisce l'esatto factoid per la chiave specificata; non viene " "effettuata nessuna analisi del\n" " suo valore. è necessario solo se se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:478 msgid "" "[] \n" "\n" " Returns the various bits of info on the factoid for the given key.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Riporta varie informazioni sul factoid per la chiave specificata. " "\n" " è necessario solo se se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:489 plugin.py:529 msgid "No such factoid: %q" msgstr "Nessun factoid: %q" #: plugin.py:498 msgid "Created by %s on %s." msgstr "Creato da %s il %s." #: plugin.py:504 msgid " Last modified by %s on %s." msgstr " Ultima modifica da %s il %s." #: plugin.py:512 msgid " Last requested by %s on %s, requested %n." msgstr " Ultima richiesta da %s il %s, richiesto %n." #: plugin.py:519 msgid " Locked by %s on %s." msgstr " Bloccato da %s il %s." #: plugin.py:534 msgid "Factoid %q is already locked." msgstr "Il factoid %q è già bloccato." #: plugin.py:537 msgid "Factoid %q is not locked." msgstr "Il factoid %q non è bloccato." #: plugin.py:547 msgid "Cannot %s someone else's factoid unless you are an admin." msgstr "" "Impossibile %s il factoid di qualcun altro a meno che non sei un " "amministratore." #: plugin.py:559 msgid "" "[] \n" "\n" " Locks the factoid with the given factoid key. Requires that the " "user\n" " be registered and have created the factoid originally. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Blocca il factoid con la chiave specificata. Necessita che l'utente " "sia\n" " registrato e abbia creato il factoid. è necessario solo se " "se il\n" " messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:570 msgid "" "[] \n" "\n" " Unlocks the factoid with the given factoid key. Requires that the\n" " user be registered and have locked the factoid. is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Sblocca il factoid con la chiave specificata. Necessita che l'utente " "sia\n" " registrato e abbia bloccato il factoid. è necessario solo " "se se\n" " il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:581 msgid "" "[] {popular|authored|recent}\n" "\n" " Lists the most {popular|authored|recent} factoids. \"popular\" " "lists the\n" " most frequently requested factoids. \"authored\" lists the author " "with\n" " the most factoids. \"recent\" lists the most recently created " "factoids.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] {popular|authored|recent}\n" "\n" " Elenca i factoid secondo un certo criterio. \"popular\" corrisponde " "ai più richiesti;\n" " \"authored\" mostra l'autore che ha creato più factoid; \"recent\" " "elenca quelli creati\n" " più di recente. è necessario solo se se il messaggio non " "viene inviato nel canale stesso.\n" " " #: plugin.py:603 msgid "author" msgstr "autore" #: plugin.py:605 msgid "authors" msgstr "autori" #: plugin.py:606 msgid "Most prolific %s: %L" msgstr "%s più prolifico: %L" #: plugin.py:608 plugin.py:620 msgid "There are no factoids in my database." msgstr "Non ci sono factoid nel mio database." #: plugin.py:615 msgid "latest factoid" msgstr "ultimo factoid" #: plugin.py:617 msgid "latest factoids" msgstr "ultimi factoid" #: plugin.py:618 #, fuzzy msgid "%i %s: %L" msgstr "%s: %L" #: plugin.py:627 msgid "requested factoid" msgstr "factoid più richiesto" #: plugin.py:629 msgid "requested factoids" msgstr "factoid più richiesti" #: plugin.py:630 #, fuzzy msgid "Top %i %s: %L" msgstr "%s: %L" #: plugin.py:632 msgid "No factoids have been requested from my database." msgstr "Non è stato richiesto alcun factoid dal mio database." #: plugin.py:636 msgid "" "[] \n" "\n" " Lists the keys of the factoids with the given author. Note that if " "an\n" " author has an integer name, you'll have to use that author's id to " "use\n" " this function (so don't use integer usernames!). is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Elenca le chiavi dell'autore specificato. Se un autore ha un nome " "intero, per\n" " usare questa funzione sarà necessario utilizzare il suo ID (per cui " "non usare nomi\n" " interi!). è necessario solo se se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:649 msgid "No factoids by %q found." msgstr "Nessun factoid di %q trovato." #: plugin.py:652 msgid "Author search for %q (%i found): %L" msgstr "Ricerca di autori per %q (trovati %i): %L" #: plugin.py:659 msgid "" "[] \n" "\n" " Lists the keys of the factoids whose key contains the provided " "text.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Elenca le chiavi dei factoid le quali contengono il testo " "specificato.\n" " è necessario solo se se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:667 msgid "No keys matching %q found." msgstr "Nessun factoid corrispondente a %q trovato." #: plugin.py:675 msgid "Key search for %q (%i found): %L" msgstr "Ricerca di chiavi per %q (trovate %i): %L" #: plugin.py:682 msgid "" "[] \n" "\n" " Lists the keys of the factoids whose value contains the provided " "text.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Elenca le chiavi dei factoid le quali contengono il testo " "specificato.\n" " è necessario solo se se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:690 msgid "No values matching %q found." msgstr "Nessun valore corrispondente a %q trovato." #: plugin.py:693 msgid "Value search for %q (%i found): %L" msgstr "Ricerca di valori per %q (trovati %i): %L" #: plugin.py:700 msgid "" "[] \n" "\n" " Deletes the factoid with the given key. is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Cancella il factoid con la chiave specificata. è " "necessario\n" " solo se se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:713 msgid "" "[]\n" "\n" " Displays a random factoid (along with its key) from the database.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Mostra un factoid casuale (con la sua chiave) dal database. " "è\n" " necessario solo se se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:721 msgid "No factoids in the database." msgstr "Nessun factoid nel database." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/plugin.py������������������������������������������������0000755�0001750�0001750�00000071352�14535072470�020400� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 sys import time import string import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils import supybot.shlex as shlex from supybot.commands import * import supybot.utils.minisix as minisix import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('MoobotFactoids') class OptionList(object): separators = '|()' def _insideParens(self, lexer): ret = [] while True: token = lexer.get_token() if not token: return '(%s' % ''.join(ret) #) elif token == ')': if '|' in ret: L = list(map(''.join, utils.iter.split('|'.__eq__, ret, yieldEmpty=True))) return utils.iter.choice(L) else: return '(%s)' % ''.join(ret) elif token == '(': ret.append(self._insideParens(lexer)) elif token == '|': ret.append(token) else: ret.append(token) def tokenize(self, s): lexer = shlex.shlex(minisix.io.StringIO(s)) lexer.commenters = '' lexer.quotes = '' lexer.whitespace = '' lexer.separators += self.separators ret = [] while True: token = lexer.get_token() if not token: break elif token == '(': ret.append(self._insideParens(lexer)) else: ret.append(token) return ''.join(ret) def pickOptions(s): return OptionList().tokenize(s) class SqliteMoobotDB(object): def __init__(self, filename): self.filename = filename self.dbs = ircutils.IrcDict() def close(self): for db in self.dbs.values(): db.close() self.dbs.clear() def _getDb(self, channel): import sqlite3 if channel in self.dbs: return self.dbs[channel] filename = plugins.makeChannelFilename(self.filename, channel) if os.path.exists(filename): db = sqlite3.connect(filename, check_same_thread=False) if minisix.PY2: db.text_factory = str self.dbs[channel] = db return db db = sqlite3.connect(filename, check_same_thread=False) if minisix.PY2: db.text_factory = str self.dbs[channel] = db cursor = db.cursor() cursor.execute("""CREATE TABLE factoids ( key TEXT PRIMARY KEY, created_by INTEGER, created_at TIMESTAMP, modified_by INTEGER, modified_at TIMESTAMP, locked_at TIMESTAMP, locked_by INTEGER, last_requested_by TEXT, last_requested_at TIMESTAMP, fact TEXT, requested_count INTEGER )""") db.commit() return db def getFactoid(self, channel, key): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT fact FROM factoids WHERE key LIKE ?""", (key,)) results = cursor.fetchall() if len(results) == 0: return None else: return results[0] def getFactinfo(self, channel, key): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT created_by, created_at, modified_by, modified_at, last_requested_by, last_requested_at, requested_count, locked_by, locked_at FROM factoids WHERE key LIKE ?""", (key,)) results = cursor.fetchall() if len(results) == 0: return None else: return results[0] def randomFactoid(self, channel): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT fact, key FROM factoids ORDER BY random() LIMIT 1""") results = cursor.fetchall() if len(results) == 0: return None else: return results[0] def addFactoid(self, channel, key, value, creator_id): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""INSERT INTO factoids VALUES (?, ?, ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, 0)""", (key, creator_id, int(time.time()), value)) db.commit() def updateFactoid(self, channel, key, newvalue, modifier_id): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""UPDATE factoids SET fact=?, modified_by=?, modified_at=? WHERE key LIKE ?""", (newvalue, modifier_id, int(time.time()), key)) db.commit() def updateRequest(self, channel, key, hostmask): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""UPDATE factoids SET last_requested_by = ?, last_requested_at = ?, requested_count = requested_count + 1 WHERE key = ?""", (hostmask, int(time.time()), key)) db.commit() def removeFactoid(self, channel, key): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""DELETE FROM factoids WHERE key LIKE ?""", (key,)) db.commit() def locked(self, channel, key): db = self._getDb(channel) cursor = db.cursor() cursor.execute ("""SELECT locked_by FROM factoids WHERE key LIKE ?""", (key,)) if cursor.fetchone()[0] is None: return False else: return True def lock(self, channel, key, locker_id): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""UPDATE factoids SET locked_by=?, locked_at=? WHERE key LIKE ?""", (locker_id, int(time.time()), key)) db.commit() def unlock(self, channel, key): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""UPDATE factoids SET locked_by=?, locked_at=? WHERE key LIKE ?""", (None, None, key)) db.commit() def mostAuthored(self, channel, limit): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT created_by, count(key) FROM factoids GROUP BY created_by ORDER BY count(key) DESC LIMIT ?""", (limit,)) return cursor.fetchall() def mostRecent(self, channel, limit): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT key FROM factoids ORDER BY created_at DESC LIMIT ?""", (limit,)) return cursor.fetchall() def mostPopular(self, channel, limit): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT key, requested_count FROM factoids WHERE requested_count > 0 ORDER BY requested_count DESC LIMIT ?""", (limit,)) results = cursor.fetchall() return results def getKeysByAuthor(self, channel, authorId): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT key FROM factoids WHERE created_by=? ORDER BY key""", (authorId,)) results = cursor.fetchall() return results def getKeysByGlob(self, channel, glob): db = self._getDb(channel) cursor = db.cursor() glob = '%%%s%%' % glob cursor.execute("""SELECT key FROM factoids WHERE key LIKE ? ORDER BY key""", (glob,)) results = cursor.fetchall() return results def getKeysByValueGlob(self, channel, glob): db = self._getDb(channel) cursor = db.cursor() glob = '%%%s%%' % glob cursor.execute("""SELECT key FROM factoids WHERE fact LIKE ? ORDER BY key""", (glob,)) results = cursor.fetchall() return results MoobotDB = plugins.DB('MoobotFactoids', {'sqlite3': SqliteMoobotDB}) class MoobotFactoids(callbacks.Plugin): """ An alternative to the Factoids plugin, this plugin keeps factoids in your bot. To add factoid say ``@something is something`` And when you call ``@something`` the bot says ``something is something``. If you want the factoid to be in different format say (for example): ``@Hi is Hello`` And when you call ``@hi`` the bot says ``Hello.`` If you want the bot to use /mes with Factoids, that is possible too. ``@test is tests.`` and everytime when someone calls for ``test`` the bot answers ``* bot tests.`` If you want the factoid to have random answers say (for example): ``@fruit is (orange|apple|banana)``. So when ``@fruit`` is called the bot will reply with one of the listed fruits (random): ``orange``. If you want to replace the value of the factoid, for example: ``@no Hi is Hey`` when you call ``@hi`` the bot says ``Hey``. If you want to append to the current value of a factoid say: ``@Hi is also Hello``, so that when you call ``@hi`` the bot says ``Hey, or Hello.`` """ callBefore = ['Dunno'] def __init__(self, irc): self.db = MoobotDB() self.__parent = super(MoobotFactoids, self) self.__parent.__init__(irc) def die(self): self.__parent.die() self.db.close() def reset(self): self.db.close() _replyTag = '' _actionTag = '' def _parseFactoid(self, irc, msg, fact): type = 'define' # Default is to just spit the factoid back as a # definition of what the key is (i.e., "foo is bar") newfact = pickOptions(fact) if newfact.startswith(self._replyTag): newfact = newfact[len(self._replyTag):] type = 'reply' elif newfact.startswith(self._actionTag): newfact = newfact[len(self._actionTag):] type = 'action' newfact = newfact.strip() newfact = ircutils.standardSubstitute(irc, msg, newfact) return (type, newfact) def invalidCommand(self, irc, msg, tokens): if '=~' in tokens: self.changeFactoid(irc, msg, tokens) elif tokens and tokens[0] in ('no', 'no,'): self.replaceFactoid(irc, msg, tokens) elif ['is', 'also'] in utils.seq.window(tokens, 2): self.augmentFactoid(irc, msg, tokens) else: key = ' '.join(tokens) key = self._sanitizeKey(key) channel = plugins.getChannel(msg.channel or msg.args[0]) fact = self.db.getFactoid(channel, key) if fact: self.db.updateRequest(channel, key, msg.prefix) # getFactoid returns "all results", so we need to extract the # first one. fact = fact[0] # Update the requested count/requested by for this key hostmask = msg.prefix # Now actually get the factoid and respond accordingly (type, text) = self._parseFactoid(irc, msg, fact) if type == 'action': irc.reply(text, action=True) elif type == 'reply': irc.reply(text, prefixNick=False) elif type == 'define': irc.reply(format(_('%s is %s'), key, text), prefixNick=False) else: assert False, 'Spurious type from _parseFactoid' else: if 'is' in tokens or '_is_' in tokens: self.addFactoid(irc, msg, tokens) def _getUserId(self, irc, prefix): try: return ircdb.users.getUserId(prefix) except KeyError: irc.errorNotRegistered(Raise=True) def _sanitizeKey(self, key): return key.rstrip('!? ') def _checkNotLocked(self, irc, channel, key): if self.db.locked(channel, key): irc.error(format(_('Factoid %q is locked.'), key), Raise=True) def _getFactoid(self, irc, channel, key): fact = self.db.getFactoid(channel, key) if fact is not None: return fact else: irc.error(format(_('Factoid %q not found.'), key), Raise=True) def _getKeyAndFactoid(self, tokens): if '_is_' in tokens: p = '_is_'.__eq__ elif 'is' in tokens: p = 'is'.__eq__ else: self.log.debug('Invalid tokens for {add,replace}Factoid: %s.', tokens) s = _('Missing an \'is\' or \'_is_\'.') raise ValueError(s) (key, newfact) = list(map(' '.join, utils.iter.split(p, tokens, maxsplit=1))) key = self._sanitizeKey(key) return (key, newfact) def addFactoid(self, irc, msg, tokens): # First, check and see if the entire message matches a factoid key channel = plugins.getChannel(msg.channel or msg.args[0]) id = self._getUserId(irc, msg.prefix) try: (key, fact) = self._getKeyAndFactoid(tokens) except ValueError as e: irc.error(str(e), Raise=True) # Check and make sure it's not in the DB already if self.db.getFactoid(channel, key): irc.error(format(_('Factoid %q already exists.'), key), Raise=True) self.db.addFactoid(channel, key, fact, id) irc.replySuccess() def changeFactoid(self, irc, msg, tokens): id = self._getUserId(irc, msg.prefix) (key, regexp) = list(map(' '.join, utils.iter.split('=~'.__eq__, tokens, maxsplit=1))) channel = plugins.getChannel(msg.channel or msg.args[0]) # Check and make sure it's in the DB fact = self._getFactoid(irc, channel, key) self._checkNotLocked(irc, channel, key) # It's fair game if we get to here try: r = utils.str.perlReToReplacer(regexp) except ValueError as e: irc.errorInvalid('regexp', regexp, Raise=True) fact = fact[0] new_fact = r(fact) self.db.updateFactoid(channel, key, new_fact, id) irc.replySuccess() def augmentFactoid(self, irc, msg, tokens): # Must be registered! id = self._getUserId(irc, msg.prefix) pairs = list(utils.seq.window(tokens, 2)) isAlso = pairs.index(['is', 'also']) key = ' '.join(tokens[:isAlso]) new_text = ' '.join(tokens[isAlso+2:]) channel = plugins.getChannel(msg.channel or msg.args[0]) fact = self._getFactoid(irc, channel, key) self._checkNotLocked(irc, channel, key) # It's fair game if we get to here fact = fact[0] new_fact = format(_('%s, or %s'), fact, new_text) self.db.updateFactoid(channel, key, new_fact, id) irc.replySuccess() def replaceFactoid(self, irc, msg, tokens): # Must be registered! channel = plugins.getChannel(msg.channel or msg.args[0]) id = self._getUserId(irc, msg.prefix) del tokens[0] # remove the "no," try: (key, fact) = self._getKeyAndFactoid(tokens) except ValueError as e: irc.error(str(e), Raise=True) _ = self._getFactoid(irc, channel, key) self._checkNotLocked(irc, channel, key) self.db.removeFactoid(channel, key) self.db.addFactoid(channel, key, fact, id) irc.replySuccess() @internationalizeDocstring def literal(self, irc, msg, args, channel, key): """[] Returns the literal factoid for the given factoid key. No parsing of the factoid value is done as it is with normal retrieval. is only necessary if the message isn't sent in the channel itself. """ fact = self._getFactoid(irc, channel, key) fact = fact[0] irc.reply(fact) literal = wrap(literal, ['channeldb', 'text']) @internationalizeDocstring def factinfo(self, irc, msg, args, channel, key): """[] Returns the various bits of info on the factoid for the given key. is only necessary if the message isn't sent in the channel itself. """ # Start building the response string s = key + ': ' # Next, get all the info and build the response piece by piece info = self.db.getFactinfo(channel, key) if not info: irc.error(format(_('No such factoid: %q'), key)) return (created_by, created_at, modified_by, modified_at, last_requested_by, last_requested_at, requested_count, locked_by, locked_at) = info # First, creation info. # Map the integer created_by to the username created_by = plugins.getUserName(created_by) created_at = time.strftime(conf.supybot.reply.format.time(), time.localtime(int(created_at))) s += format(_('Created by %s on %s.'), created_by, created_at) # Next, modification info, if any. if modified_by is not None: modified_by = plugins.getUserName(modified_by) modified_at = time.strftime(conf.supybot.reply.format.time(), time.localtime(int(modified_at))) s += format(_(' Last modified by %s on %s.'), modified_by, modified_at) # Next, last requested info, if any if last_requested_by is not None: last_by = last_requested_by # not an int user id last_at = time.strftime(conf.supybot.reply.format.time(), time.localtime(int(last_requested_at))) req_count = requested_count s += format(_(' Last requested by %s on %s, requested %n.'), last_by, last_at, (requested_count, 'time')) # Last, locked info if locked_at is not None: lock_at = time.strftime(conf.supybot.reply.format.time(), time.localtime(int(locked_at))) lock_by = plugins.getUserName(locked_by) s += format(_(' Locked by %s on %s.'), lock_by, lock_at) irc.reply(s) factinfo = wrap(factinfo, ['channeldb', 'text']) def _lock(self, irc, msg, channel, user, key, locking=True): #self.log.debug('in _lock') #self.log.debug('id: %s', id) id = user.id info = self.db.getFactinfo(channel, key) if not info: irc.error(format(_('No such factoid: %q'), key)) return (created_by, a, a, a, a, a, a, locked_by, a) = info # Don't perform redundant operations if locking and locked_by is not None: irc.error(format(_('Factoid %q is already locked.'), key)) return if not locking and locked_by is None: irc.error(format(_('Factoid %q is not locked.'), key)) return # Can only lock/unlock own factoids unless you're an admin #self.log.debug('admin?: %s', ircdb.checkCapability(id, 'admin')) #self.log.debug('created_by: %s', created_by) if not (ircdb.checkCapability(id, 'admin') or created_by == id): if locking: s = 'lock' else: s = 'unlock' irc.error(format(_('Cannot %s someone else\'s factoid unless you ' 'are an admin.'), s)) return # Okay, we're done, ready to lock/unlock if locking: self.db.lock(channel, key, id) else: self.db.unlock(channel, key) irc.replySuccess() @internationalizeDocstring def lock(self, irc, msg, args, channel, user, key): """[] Locks the factoid with the given factoid key. Requires that the user be registered and have created the factoid originally. is only necessary if the message isn't sent in the channel itself. """ self._lock(irc, msg, channel, user, key, True) lock = wrap(lock, ['channeldb', 'user', 'text']) @internationalizeDocstring def unlock(self, irc, msg, args, channel, user, key): """[] Unlocks the factoid with the given factoid key. Requires that the user be registered and have locked the factoid. is only necessary if the message isn't sent in the channel itself. """ self._lock(irc, msg, channel, user, key, False) unlock = wrap(unlock, ['channeldb', 'user', 'text']) @internationalizeDocstring def most(self, irc, msg, args, channel, method): """[] {popular|authored|recent} Lists the most {popular|authored|recent} factoids. "popular" lists the most frequently requested factoids. "authored" lists the author with the most factoids. "recent" lists the most recently created factoids. is only necessary if the message isn't sent in the channel itself. """ method = method.capitalize() method = getattr(self, '_most%s' % method, None) if method is None: raise callbacks.ArgumentError limit = self.registryValue('mostCount', channel, irc.network) method(irc, channel, limit) most = wrap(most, ['channeldb', ('literal', ('popular', 'authored', 'recent'))]) def _mostAuthored(self, irc, channel, limit): results = self.db.mostAuthored(channel, limit) L = ['%s (%s)' % (plugins.getUserName(t[0]), int(t[1])) for t in results] if L: author = _('author') if len(L) != 1: author = _('authors') irc.reply(format(_('Most prolific %s: %L'), author, L)) else: irc.error(_('There are no factoids in my database.')) def _mostRecent(self, irc, channel, limit): results = self.db.mostRecent(channel, limit) L = [format('%q', t[0]) for t in results] if L: if len(L) < 2: latest = _('latest factoid') else: latest = _('latest factoids') irc.reply(format(_('%i %s: %L'), len(L), latest, L)) else: irc.error(_('There are no factoids in my database.')) def _mostPopular(self, irc, channel, limit): results = self.db.mostPopular(channel, limit) L = [format('%q (%s)', t[0], t[1]) for t in results] if L: if len(L) < 2: requested = _('requested factoid') else: requested = _('requested factoids') irc.reply(format(_('Top %i %s: %L'), len(L), requested, L)) else: irc.error(_('No factoids have been requested from my database.')) @internationalizeDocstring def listauth(self, irc, msg, args, channel, author): """[] Lists the keys of the factoids with the given author. Note that if an author has an integer name, you'll have to use that author's id to use this function (so don't use integer usernames!). is only necessary if the message isn't sent in the channel itself. """ try: id = ircdb.users.getUserId(author) except KeyError: irc.errorNoUser(name=author, Raise=True) results = self.db.getKeysByAuthor(channel, id) if not results: irc.reply(format(_('No factoids by %q found.'), author)) return keys = [format('%q', t[0]) for t in results] s = format(_('Author search for %q (%i found): %L'), author, len(keys), keys) irc.reply(s) listauth = wrap(listauth, ['channeldb', 'something']) @internationalizeDocstring def listkeys(self, irc, msg, args, channel, search): """[] Lists the keys of the factoids whose key contains the provided text. is only necessary if the message isn't sent in the channel itself. """ results = self.db.getKeysByGlob(channel, search) if not results: irc.reply(format(_('No keys matching %q found.'), search)) elif len(results) == 1 and \ self.registryValue('showFactoidIfOnlyOneMatch', channel, irc.network): key = results[0][0] self.invalidCommand(irc, msg, [key]) else: keys = [format('%q', tup[0]) for tup in results] s = format(_('Key search for %q (%i found): %L'), search, len(keys), keys) irc.reply(s) listkeys = wrap(listkeys, ['channeldb', 'text']) @internationalizeDocstring def listvalues(self, irc, msg, args, channel, search): """[] Lists the keys of the factoids whose value contains the provided text. is only necessary if the message isn't sent in the channel itself. """ results = self.db.getKeysByValueGlob(channel, search) if not results: irc.reply(format(_('No values matching %q found.'), search)) return keys = [format('%q', tup[0]) for tup in results] s = format(_('Value search for %q (%i found): %L'), search, len(keys), keys) irc.reply(s) listvalues = wrap(listvalues, ['channeldb', 'text']) @internationalizeDocstring def remove(self, irc, msg, args, channel, _, key): """[] Deletes the factoid with the given key. is only necessary if the message isn't sent in the channel itself. """ _ = self._getFactoid(irc, channel, key) self._checkNotLocked(irc, channel, key) self.db.removeFactoid(channel, key) irc.replySuccess() remove = wrap(remove, ['channeldb', 'user', 'text']) @internationalizeDocstring def random(self, irc, msg, args, channel): """[] Displays a random factoid (along with its key) from the database. is only necessary if the message isn't sent in the channel itself. """ results = self.db.randomFactoid(channel) if not results: irc.error(_('No factoids in the database.')) return (fact, key) = results irc.reply(format('Random factoid: %q is %q', key, fact)) random = wrap(random, ['channeldb']) MoobotFactoids = internationalizeDocstring(MoobotFactoids) Class = MoobotFactoids # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/MoobotFactoids/test.py��������������������������������������������������0000644�0001750�0001750�00000041113�14535072470�020046� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- encoding: utf-8 -*- ### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 time from supybot.test import * #import supybot.plugin as plugin import supybot.ircutils as ircutils from supybot.utils.minisix import u # import sqlite3, so it's in sys.modules and conf.supybot.databases includes # sqlite3. try: import sqlite3 except ImportError: sqlite3 = None from . import plugin MFconf = conf.supybot.plugins.MoobotFactoids class OptionListTestCase(SupyTestCase): maxIterations = 267 def _testOptions(self, s, L): max = self.maxIterations original = L[:] while max and L: max -= 1 option = plugin.pickOptions(s) self.assertIn( option, original, 'Option %s not in %s' % (option, original) ) if option in L: L.remove(option) self.assertFalse(L, 'Some options never seen: %s' % L) def testPickOptions(self): self._testOptions('(a|b)', ['a', 'b']) self._testOptions('a', ['a']) self._testOptions('(a|b (c|d))', ['a', 'b c', 'b d']) self._testOptions('(a|(b|)c)', ['a', 'bc', 'c']) self._testOptions('(a(b|)|(c|)d)', ['a', 'ab', 'cd', 'd']) self._testOptions('(a|)', ['a', '']) self._testOptions('(|a)', ['a', '']) self._testOptions('((a)|(b))', ['(a)', '(b)']) self._testOptions('^\\%(\\%(foo\\)\\@moo is moo') self.assertResponse('bar', 'moo is moo') # Check substitution self.assertNotError('who is $who') self.assertResponse('who', ircutils.nickFromHostmask(self.prefix)) # Check that actions ("\x01ACTION...") don't match m = ircmsgs.action(self.channel, 'is doing something') self.irc.feedMsg(m) self.assertNoResponse(' ', 1) class FactoidsTestCase(ChannelPluginTestCase): plugins = ('MoobotFactoids', 'User', 'String', 'Utilities', 'Web') config = {'reply.whenNotCommand': False} def setUp(self): ChannelPluginTestCase.setUp(self) # Create a valid user to use self.prefix = 'mf!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.nick, 'register tester moo', prefix=self.prefix)) m = self.irc.takeMsg() # Response to register. def testAddFactoid(self): self.assertNotError('moo is foo') # Check stripping punctuation self.assertError('moo!? is foo') # 'moo' already exists self.assertNotError('foo!? is foo') self.assertResponse('foo', 'foo is foo') self.assertNotError('bar is moo is moo') self.assertResponse('bar', 'moo is moo') # Check substitution self.assertNotError('who is $who') self.assertResponse('who', ircutils.nickFromHostmask(self.prefix)) # Check that actions ("\x01ACTION...") don't match m = ircmsgs.action(self.channel, 'is doing something') self.irc.feedMsg(m) self.assertNoResponse(' ', 1) def testLiteral(self): self.assertError('literal moo') # no factoids yet self.assertNotError('moo is foo') self.assertResponse('literal moo', 'foo') self.assertNotError('moo2 is moo!') self.assertResponse('literal moo2', 'moo!') self.assertNotError('moo3 is foo') self.assertResponse('literal moo3', 'foo') def testGetFactoid(self): self.assertNotError('moo is foo') self.assertResponse('moo', 'foo') self.assertNotError('moo2 is moo!') self.assertResponse('moo2', 'moo2 is moo!') self.assertNotError('moo3 is foo') self.assertAction('moo3', 'foo') # Test and make sure it's parsing self.assertNotError('moo4 is (1|2|3)') self.assertRegexp('moo4', r'^(1|2|3)$') # Check case-insensitivity self.assertResponse('MOO', 'foo') self.assertResponse('mOo', 'foo') self.assertResponse('MoO', 'foo') # Check the "_is_" ability self.assertNotError('remove moo') self.assertNotError('moo _is_ foo') self.assertResponse('moo', 'foo') self.assertNotError('foo is bar _is_ baz') self.assertResponse('foo is bar', 'foo is bar is baz') def testFactinfo(self): self.assertNotError('moo is foo') self.assertRegexp('factinfo moo', r'^moo: Created by tester on.*$') self.assertNotError('moo') self.assertRegexp('factinfo moo', self.prefix + r'.*1 time') self.assertNotError('moo') self.assertRegexp('factinfo moo', self.prefix + r'.*2 times') self.assertNotError('moo =~ s/foo/bar/') self.assertRegexp('factinfo moo', r'^moo: Created by tester on' r'.*?\. Last modified by tester on .*?\. ' r'Last requested by %s on .*?, ' r'requested 2 times.$' % self.prefix) self.assertNotError('lock moo') self.assertRegexp('factinfo moo', r'^moo: Created by tester on' r'.*?\. Last modified by tester on .*?\. ' r'Last requested by %s on .*?, ' r'requested 2 times. ' r'Locked by tester on .*\.$' % self.prefix) self.assertNotError('unlock moo') self.assertRegexp('factinfo moo', r'^moo: Created by tester on' r'.*?\. Last modified by tester on .*?\. ' r'Last requested by %s on .*?, ' r'requested 2 times.$' % self.prefix) # Make sure I solved this bug # Check and make sure all the other stuff is reset self.assertNotError('foo is bar') self.assertNotError('foo =~ s/bar/blah/') self.assertNotError('foo') self.assertNotError('no foo is baz') self.assertRegexp('factinfo foo', r'^foo: Created by tester on' r'(?!(request|modif)).*?\.$') def testLockUnlock(self): # disable world.testing since we want new users to not # magically be endowed with the admin capability try: world.testing = False self.assertNotError('moo is moo') self.assertNotError('lock moo') self.assertRegexp('factinfo moo', r'^moo: Created by tester on' r'.*?\. Locked by tester on .*?\.') # switch user original = self.prefix self.prefix = 'moo!moo@moo' self.assertNotError('register nottester moo', private=True) self.assertError('unlock moo') self.assertRegexp('factinfo moo', r'^moo: Created by tester on' r'.*?\. Locked by tester on .*?\.') # switch back self.prefix = original self.assertNotError('identify tester moo', private=True) self.assertNotError('unlock moo') self.assertRegexp('factinfo moo', r'^moo: Created by tester on.*?\.') finally: world.testing = True def testChangeFactoid(self): self.assertNotError('moo is moo') self.assertNotError('moo =~ s/moo/moos/') self.assertResponse('moo', 'moos') self.assertNotError('moo =~ s/reply/action/') self.assertAction('moo', 'moos') self.assertNotError('moo =~ s/moos/(moos|woofs)/') self.assertActionRegexp('moo', '^(moos|woofs)$') self.assertError('moo =~ s/moo/') def testMost(self): userPrefix1 = 'moo!bar@baz'; userNick1 = 'moo' userPrefix2 = 'boo!bar@baz'; userNick2 = 'boo' self.assertNotError('register %s bar' % userNick1, frm=userPrefix1, private=True) self.assertNotError('register %s bar' % userNick2, frm=userPrefix2, private=True) # Check an empty database self.assertError('most popular') self.assertError('most authored') self.assertError('most recent') # Check singularity response self.prefix = userPrefix1 self.assertNotError('moogle is moo') self.assertError('most popular') self.assertResponse('most authored', 'Most prolific author: moo (1)') self.assertRegexp('most recent', r"1 latest factoid:.*moogle") self.assertResponse('moogle', 'moo') self.assertRegexp('most popular', r"Top 1 requested factoid:.*moogle.*(1)") # Check plural response time.sleep(1) self.prefix = userPrefix2 self.assertNotError('mogle is mo') self.assertRegexp('most authored', (r'Most prolific authors: .*' r'(moo.*\(1\).*boo.*\(1\)' r'|boo.*\(1\).*moo.*\(1\))')) self.assertRegexp('most recent', r"2 latest factoids:.*mogle.*moogle.*") self.assertResponse('moogle', 'moo') self.assertRegexp('most popular', r"Top 1 requested factoid:.*moogle.*(2)") self.assertResponse('mogle', 'mo') self.assertRegexp('most popular', r"Top 2 requested factoids:.*" r"moogle.*(2).*mogle.*(1)") # Check most author ordering self.assertNotError('moo is oom') self.assertRegexp('most authored', r'Most prolific authors:.*boo.*(2).*moo.*(1)') def testListkeys(self): self.assertResponse('listkeys %', 'No keys matching "%" found.') self.assertNotError('moo is moo') # With this set, if only one key matches, it should respond with # the factoid orig = MFconf.showFactoidIfOnlyOneMatch() try: MFconf.showFactoidIfOnlyOneMatch.setValue(True) self.assertResponse('listkeys moo', 'moo') self.assertResponse('listkeys foo', 'No keys matching "foo" ' 'found.') # Throw in a bunch more for i in range(10): self.assertNotError('moo%s is moo' % i) self.assertRegexp('listkeys moo', r'^Key search for "moo" ' r'\(11 found\): ("moo\d*", )+and "moo9"$') self.assertNotError('foo is bar') self.assertRegexp('listkeys %', r'^Key search for "\%" ' r'\(12 found\): "foo", ("moo\d*", )+and ' r'"moo9"$') # Check quoting self.assertNotError('foo\' is bar') self.assertResponse('listkeys foo', 'Key search for "foo" ' '(2 found): "foo" and "foo\'"') # Check unicode stuff self.assertResponse(u('listkeys Б'), 'No keys matching "Б" found.') self.assertNotError(u('АБВГДЕЖ is foo')) self.assertNotError(u('АБВГДЕЖЗИ is foo')) self.assertResponse(u('listkeys Б'), 'Key search for "Б" ' '(2 found): "АБВГДЕЖ" and "АБВГДЕЖЗИ"') finally: MFconf.showFactoidIfOnlyOneMatch.setValue(orig) def testListvalues(self): self.assertNotError('moo is moo') self.assertResponse('listvalues moo', 'Value search for "moo" (1 found): "moo"') def testListauth(self): self.assertNotError('moo is moo') self.assertRegexp('listauth tester', r'tester.*\(1 found\):.*moo') self.assertError('listauth moo') def testRemove(self): self.assertNotError('moo is moo') self.assertNotError('lock moo') self.assertError('remove moo') self.assertNotError('unlock moo') self.assertNotError('remove moo') def testAugmentFactoid(self): self.assertNotError('moo is foo') self.assertNotError('moo is also bar') self.assertResponse('moo', 'moo is foo, or bar') self.assertNotError('moo is bar _is_ foo') self.assertNotError('moo is bar is also foo') self.assertResponse('moo is bar', 'moo is bar is foo, or foo') def testReplaceFactoid(self): self.assertNotError('moo is foo') self.assertNotError('no moo is bar') self.assertResponse('moo', 'moo is bar') self.assertNotError('no, moo is baz') self.assertResponse('moo', 'moo is baz') self.assertNotError('lock moo') self.assertError('no moo is qux') self.assertNotError('foo is bar _is_ foo') self.assertNotError('no foo is bar _is_ baz') self.assertResponse('foo is bar', 'foo is bar is baz') def testRegexpNotCalledIfAlreadyHandled(self): self.assertResponse('echo foo is bar', 'foo is bar') self.assertNoResponse(' ', 3) def testNoResponseToCtcp(self): self.assertNotError('foo is bar') self.assertResponse('foo', 'foo is bar') self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, '\x01VERSION\x01')) m = self.irc.takeMsg() self.assertFalse(m) def testAddFactoidNotCalledWithBadNestingSyntax(self): self.assertError('re s/Error:.*/foo/ ]') self.assertNoResponse(' ', 3) def testConfigShowFactoidIfOnlyOneMatch(self): # these are long MFconf = conf.supybot.plugins.MoobotFactoids self.assertNotError('foo is bar') # Default to saying the factoid value self.assertResponse('listkeys foo', 'foo is bar') # Check the False setting MFconf.showFactoidIfOnlyOneMatch.setValue(False) self.assertResponse('listkeys foo', 'Key search for "foo" ' '(1 found): "foo"') def testRandom(self): self.assertNotError('foo is bar') self.assertNotError('bar is baz') self.assertRegexp('random', r'bar|baz') # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3457546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/����������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015234� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/__init__.py�����������������������������������������������������0000644�0001750�0001750�00000004722�14535072470�017350� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Includes commands for connecting, disconnecting, and reconnecting to multiple networks, as well as several other utility functions related to IRC networks like showing the latency. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ����������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/config.py�������������������������������������������������������0000644�0001750�0001750�00000004740�14535072470�017056� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Network') 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('Network', True) Network = conf.registerPlugin('Network') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Network, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/locales/��������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016656� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/locales/de.po���������������������������������������������������0000644�0001750�0001750�00000016015�14535072470�017607� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-29 20:20+0100\n" "Last-Translator: Florian Besser \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:47 msgid "" "Provides network-related commands, such as connecting to multiple networks\n" " and checking latency to the server." msgstr "" #: plugin.py:59 #, fuzzy msgid "" "[--nossl] [] []\n" "\n" " Connects to another network (which will be represented by the name\n" " provided in ) at . If port is not provided, it\n" " defaults to 6697, the default port for IRC with SSL. If password " "is\n" " provided, it will be sent to the server in a PASS command. If --" "nossl is\n" " provided, an SSL connection will not be attempted, and the port " "will\n" " default to 6667.\n" " " msgstr "" "[--ssl] [] []\n" "\n" "Verbindet zu einem anderen Netzwerk, welches durch den Namen in " "auf festgelegt wird. Falls Port nicht angegeben wird, wird " "standardgemäß 6667 verwendet, der Standardport für IRC. Falls Passwort " "angegeben wird, wird es an den Server per PASS Befehl gesendet. Falls --ssl " "angegeben wird, wird versucht eine SSL Verbindung aufzubauen." #: plugin.py:74 msgid "I'm already connected to %s." msgstr "Ich bin schon verbunden mit %s." #: plugin.py:96 msgid "A server must be provided if the network is not already registered." msgstr "" "Es muss ein Server angegeben werden, falls das netwerk noch net registriert " "ist." #: plugin.py:104 msgid "Connection to %s initiated." msgstr "Verbindung zu %s wurde eingeleitet." #: plugin.py:110 #, fuzzy msgid "" " []\n" "\n" " Disconnects from the network represented by the network .\n" " If is given, quits the network with the given quit\n" " message.\n" " " msgstr "[] []" #: plugin.py:124 msgid "Disconnection to %s initiated." msgstr "Verbindungstrennung zu %s wurde eingeleitet." #: plugin.py:129 msgid "" "[] []\n" "\n" " Disconnects and then reconnects to . If no network is " "given,\n" " disconnects and then reconnects to the network the command was " "given\n" " on. If no quit message is given, uses the configured one\n" " (supybot.plugins.Owner.quitMsg) or the nick of the person giving " "the\n" " command.\n" " " msgstr "" "[] []\n" "\n" "Trennt die Verbindung von und baut diese neu auf, falls kein " "Netzwerk angegeben wird, wird die Verbindung des Netzwerks getrennt und " "wieder aufgebaut, auf dem der Befehl gegeben wurde. Falls keine " "Beendennachricht angegeben wird, wird die konfigurierte (supybot.plugins." "Owner.quitMsg) oder der Nick des Nutzer, der den Befehl gegeben hat, benutzt." #: plugin.py:148 msgid "" " [ ...]\n" "\n" " Gives the bot (with its associated s) on .\n" " " msgstr "" " [ ...]\n" "\n" "Gibt dem Bot den (mit den zugehörigen en) auf dem " "." #: plugin.py:156 #, fuzzy msgid "" " [ ...]\n" "\n" " Perform (with its associated s) on all networks.\n" " " msgstr "" " [ ...]\n" "\n" "Gibt dem Bot den (mit den zugehörigen en) auf dem " "." #: plugin.py:209 #, fuzzy msgid "There is no user %s on %s." msgstr "Kein %s auf %s." #: plugin.py:211 #, fuzzy msgid "There was no user %s on %s." msgstr "Kein %s auf %s." #: plugin.py:218 plugin.py:231 msgid "" "[] \n" "\n" " Returns the WHOIS response gives for . " "is\n" " only necessary if the network is different than the network the " "command\n" " is sent on.\n" " " msgstr "" "[] \n" "\n" "Gibt die WHOIS Antwort aus, die das für gibt. " "wird nur benötigt wenn der Befehl nicht im Netzwerk gesendet wird auf dem " "der Befehl ausgeführt werden soll." #: plugin.py:244 msgid "" "[--all]\n" "\n" " Returns the networks to which the bot is currently connected.\n" " If --all is given, also includes networks known by the bot,\n" " but not connected to.\n" " " msgstr "" #: plugin.py:255 msgid "disconnected" msgstr "" #: plugin.py:264 msgid "%.2f seconds." msgstr "%.2f Sekunden." #: plugin.py:267 msgid "" "[]\n" "\n" " Returns the current latency to . is only " "necessary\n" " if the message isn't sent on the network to which this command is " "to\n" " apply.\n" " " msgstr "" "[]\n" "\n" "Gibt die momenten Latenz zu an. wird nur benötigt wenn " "der Befehl nicht im Netzwerk gesendet wird auf dem der Befehl ausgeführt " "werden soll." #: plugin.py:273 msgid "Latency check (from %s)." msgstr "Latenzprüfung (von %s)." #: plugin.py:280 msgid "" "[]\n" "\n" " Returns the current network driver for . is " "only\n" " necessary if the message isn't sent on the network to which this\n" " command is to apply.\n" " " msgstr "" "[]\n" "\n" "Gibt den momentanen Netzwerktreiber für an. wird nur " "benötigt wenn der Befehl nicht im Netzwerk gesendet wird auf dem der Befehl " "ausgeführt werden soll." #: plugin.py:290 msgid "" "[]\n" "\n" " Returns the time duration since the connection was established.\n" " " msgstr "" #: plugin.py:297 #, fuzzy msgid "I've been connected to %s for %s." msgstr "Ich bin schon verbunden mit %s." #: plugin.py:302 msgid "" "[]\n" "\n" " Returns the list of IRCv3 capabilities available on the network.\n" " " msgstr "" #~ msgid "is an op on %L" #~ msgstr "ist ein Operator in %L" #~ msgid "is a halfop on %L" #~ msgstr "ist ein Halboperator in %L" #~ msgid "is voiced on %L" #~ msgstr "hat Sprechrechte in %L" #~ msgid "is also on %L" #~ msgstr "ist auch in %L" #~ msgid "is on %L" #~ msgstr "ist in %l" #~ msgid "isn't on any non-secret channels" #~ msgstr "" #~ "ist nicht in irgendwelchen Kanälen die nicht als geheim eingestuft sind." #~ msgid "" #~ msgstr "" #~ msgid " identified" #~ msgstr " identifiziert" #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s) and %s.%s" #~ msgstr "" #~ "%s (%s) ist%s auf dem Server % seit %s (im Leerlauf für %s) und %s.%s" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the networks to which the bot is currently connected.\n" #~ " " #~ msgstr "" #~ "hat keine Argumente\n" #~ "\n" #~ "Gibt an zu welchen Netzwerken der Bot momentan verbunden ist." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/locales/fi.po���������������������������������������������������0000644�0001750�0001750�00000020644�14535072470�017620� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Network plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11:34+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" #: plugin.py:47 msgid "" "Provides network-related commands, such as connecting to multiple networks\n" " and checking latency to the server." msgstr "" "Tarjoaa verkkoon liittyviä komentoja, kuten useaan verkkoon yhdistämisen ja " "botin ja palvelimen\n" " väillä olevan viiveen tarkistksen." #: plugin.py:59 #, fuzzy msgid "" "[--nossl] [] []\n" "\n" " Connects to another network (which will be represented by the name\n" " provided in ) at . If port is not provided, it\n" " defaults to 6697, the default port for IRC with SSL. If password " "is\n" " provided, it will be sent to the server in a PASS command. If --" "nossl is\n" " provided, an SSL connection will not be attempted, and the port " "will\n" " default to 6667.\n" " " msgstr "" "[--ssl] [] []\n" "\n" " Yhdistää toiseen verkkoon (joka näytetään nimellä, joka on annettu\n" " ) . Jos portti ei ole annettu, se\n" " on oletuksena 6667, oletusportti IRC:lle. Jos salasana on\n" " annettu, se lähetetään palvelimelle PASS komennossa. Jos --ssl on\n" " annettu, SSL yhteys muodostetaan.\n" " " #: plugin.py:74 msgid "I'm already connected to %s." msgstr "Olen jo verkkoon %s." #: plugin.py:96 msgid "A server must be provided if the network is not already registered." msgstr "Palvelin täytyy antaa, mikäli verkko ei ole jo rekisteröity." #: plugin.py:104 msgid "Connection to %s initiated." msgstr "Yhdistäminen verkkoon %s aloitettu." #: plugin.py:110 #, fuzzy msgid "" " []\n" "\n" " Disconnects from the network represented by the network .\n" " If is given, quits the network with the given quit\n" " message.\n" " " msgstr "" "[] []\n" "\n" " Katkaisee yhteyden verkkoon, jonka määrittää .\n" " Jos on annettu, poistuu verkosta annetulla lopetus\n" " viestillä. on vaadittu vain jos verkko on eri, kuin se, " "verkko josta\n" " viesti on lähetetty.\n" " " #: plugin.py:124 msgid "Disconnection to %s initiated." msgstr "Yhteyden katkaisu verkosta %s aloitettu." #: plugin.py:129 msgid "" "[] []\n" "\n" " Disconnects and then reconnects to . If no network is " "given,\n" " disconnects and then reconnects to the network the command was " "given\n" " on. If no quit message is given, uses the configured one\n" " (supybot.plugins.Owner.quitMsg) or the nick of the person giving " "the\n" " command.\n" " " msgstr "" "[] []\n" "\n" " Katkaisee yhteyden ja yhdistää uudelleen . Jos verkkoa ei " "ole annettu,\n" " katkaisee yhteyden ja yhdistää uudelleen verkkoon, jossa komento " "annettiin.\n" " Jos lopetus viestiä ei annettu, käyttää määritettyä lopetus viestiä\n" " (supybot.plugins.Owner.quitMsg) tai henkilön joka antoi komennon\n" " nimimerkkiä.\n" " " #: plugin.py:148 msgid "" " [ ...]\n" "\n" " Gives the bot (with its associated s) on .\n" " " msgstr "" " [ ...]\n" "\n" " Antaa botille (siihen liitetyillä ) " ".\n" " " #: plugin.py:156 #, fuzzy msgid "" " [ ...]\n" "\n" " Perform (with its associated s) on all networks.\n" " " msgstr "" " [ ...]\n" "\n" " Suotrittaa (siihen liitetyillä ) kaikissa " "verkoissa.\n" " " #: plugin.py:209 #, fuzzy msgid "There is no user %s on %s." msgstr "%s:ää ei ole verkossa %s." #: plugin.py:211 #, fuzzy msgid "There was no user %s on %s." msgstr "Nimimerkkiä %s ei ole verkossa %s." #: plugin.py:218 plugin.py:231 msgid "" "[] \n" "\n" " Returns the WHOIS response gives for . " "is\n" " only necessary if the network is different than the network the " "command\n" " is sent on.\n" " " msgstr "" "[] \n" "\n" " Palauttaa WHOIS vastauksen, jonka antaa . " " on\n" " vaadittu vain jos verkko on eri kuin se verkko, josta komento\n" " lähetettiin.\n" " " #: plugin.py:244 msgid "" "[--all]\n" "\n" " Returns the networks to which the bot is currently connected.\n" " If --all is given, also includes networks known by the bot,\n" " but not connected to.\n" " " msgstr "" "[--all]\n" " Palauttaa verkot joihin botti on yhteydessä. Mikäli --all annetaan, " "sisällyttää myös verkot, jotka botti\n" " tuntee, mutta joihin botti ei ole yhteydessä juuri nyt." #: plugin.py:255 msgid "disconnected" msgstr "yhteys katkaistu" #: plugin.py:264 msgid "%.2f seconds." msgstr "%.2f sekuntia." #: plugin.py:267 msgid "" "[]\n" "\n" " Returns the current latency to . is only " "necessary\n" " if the message isn't sent on the network to which this command is " "to\n" " apply.\n" " " msgstr "" "[]\n" "\n" " Palauttaa nykyisen viiveen . on vaadittu vain " "jos\n" " viestiä ei lähetetä verkossa, jolle tämä komento on tarkoitettu\n" " vaikuttamaan.\n" " " #: plugin.py:273 msgid "Latency check (from %s)." msgstr "Viiveen tarkistus (%s:ltä)." #: plugin.py:280 msgid "" "[]\n" "\n" " Returns the current network driver for . is " "only\n" " necessary if the message isn't sent on the network to which this\n" " command is to apply.\n" " " msgstr "" "[]\n" "\n" " Palauttaa nykyisen verkkoajurin, joka on käytössä . " " on vaadittu\n" " vain jos komentoa ei lähetetä verkossa, johon tämän komennon on " "tarkoitus\n" " vaikuttaa.\n" " " #: plugin.py:290 #, fuzzy msgid "" "[]\n" "\n" " Returns the time duration since the connection was established.\n" " " msgstr "" "[\n" "\n" " Palauttaa ajan, joka on kulunut siitä, kun yhteys muodostettiin.]" #: plugin.py:297 msgid "I've been connected to %s for %s." msgstr "Olen ollut yhteydessä verkkoon %s ajan %s." #: plugin.py:302 #, fuzzy msgid "" "[]\n" "\n" " Returns the list of IRCv3 capabilities available on the network.\n" " " msgstr "" "[\n" "\n" " Palauttaa ajan, joka on kulunut siitä, kun yhteys muodostettiin.]" #~ msgid "is an op on %L" #~ msgstr "on kanavaoperaattori %L:llä" #~ msgid "is a halfop on %L" #~ msgstr "on puolioperaattori %L:llä." #~ msgid "is voiced on %L" #~ msgstr "on ääni %L:llä" #~ msgid "is also on %L" #~ msgstr "on myös %L:llä" #~ msgid "is on %L" #~ msgstr "on %L:llä." #, fuzzy #~ msgid "" #~ "isn't on any non-secret channels or is using a channel-list hiding umode." #~ msgstr "Ei ole yhdelläkään ei-salaisella kanavalla." #~ msgid "" #~ msgstr "" #~ msgid " identified" #~ msgstr "tunnistautunut" #, fuzzy #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s). %s %s.%s" #~ msgstr "" #~ "%s (%s) on ollut %s palvelimella %s %s lähtien (idlannut %s) ja %s.%s" #, fuzzy #~ msgid "%s (%s) has been%s on server %s and disconnected on %s." #~ msgstr "%s (%s) on ollut %s palvelimella %s %s lähtien (jouten %s) ja %s.%s" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the networks to which the bot is currently connected.\n" #~ " " #~ msgstr "" #~ "ei ota parametrejä\n" #~ "\n" #~ " Palauttaa verkot, joihin botti on tällä hetkellä muodostanut " #~ "yhteyden.\n" #~ " " ��������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/locales/fr.po���������������������������������������������������0000644�0001750�0001750�00000016171�14535072470�017631� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:47 msgid "" "Provides network-related commands, such as connecting to multiple networks\n" " and checking latency to the server." msgstr "" #: plugin.py:59 #, fuzzy msgid "" "[--nossl] [] []\n" "\n" " Connects to another network (which will be represented by the name\n" " provided in ) at . If port is not provided, it\n" " defaults to 6697, the default port for IRC with SSL. If password " "is\n" " provided, it will be sent to the server in a PASS command. If --" "nossl is\n" " provided, an SSL connection will not be attempted, and the port " "will\n" " default to 6667.\n" " " msgstr "" "[--ssl] [] []\n" "\n" "Se connecter à un autre réseau (représenté par le ) au . Si " "le port n'est pas fourni, il s'agit du 6667, celui par défaut pour IRC. Si " "le est fourni, l'envoie au serveur par la commande PASS. Si --" "ssl est fourni, une connexion SSL sera requise." #: plugin.py:74 msgid "I'm already connected to %s." msgstr "Je suis déjà connecté à %s." #: plugin.py:96 msgid "A server must be provided if the network is not already registered." msgstr "Un serveur doit être fourni si le réseau n'est pas déjà enregistré." #: plugin.py:104 msgid "Connection to %s initiated." msgstr "Connexion à %s initialisée." #: plugin.py:110 #, fuzzy msgid "" " []\n" "\n" " Disconnects from the network represented by the network .\n" " If is given, quits the network with the given quit\n" " message.\n" " " msgstr "" "[] []\n" "\n" "Se déconnecte du . Si le est fourni, quitte le " "réseau avec ce message. Le n'est nécessaire que s'il ne s'agit pas " "du réseau sur lequel la commande est envoyée." #: plugin.py:124 msgid "Disconnection to %s initiated." msgstr "Déconnexion à %s initialisée." #: plugin.py:129 msgid "" "[] []\n" "\n" " Disconnects and then reconnects to . If no network is " "given,\n" " disconnects and then reconnects to the network the command was " "given\n" " on. If no quit message is given, uses the configured one\n" " (supybot.plugins.Owner.quitMsg) or the nick of the person giving " "the\n" " command.\n" " " msgstr "" "[] []\n" "\n" "Se déconnecte du et s'y reconnecte. Si le est " "fourni, quitte le réseau avec ce message. Le n'est nécessaire que " "s'il ne s'agit pas du réseau sur lequel la commande est envoyée." #: plugin.py:148 msgid "" " [ ...]\n" "\n" " Gives the bot (with its associated s) on .\n" " " msgstr "" " [ ...]\n" "\n" "Envoie la au bot (avec les arguments) sur le ." #: plugin.py:156 #, fuzzy msgid "" " [ ...]\n" "\n" " Perform (with its associated s) on all networks.\n" " " msgstr "" " [ ...]\n" "\n" "Envoie la au bot (avec les arguments) sur le ." #: plugin.py:209 #, fuzzy msgid "There is no user %s on %s." msgstr "Il n'y a pas de %s sur %s." #: plugin.py:211 #, fuzzy msgid "There was no user %s on %s." msgstr "Il n'y a pas eu de %s sur %s." #: plugin.py:218 plugin.py:231 msgid "" "[] \n" "\n" " Returns the WHOIS response gives for . " "is\n" " only necessary if the network is different than the network the " "command\n" " is sent on.\n" " " msgstr "" "[] \n" "\n" "Retourne les réponses WHOIS du pour le . Le n'est " "nécessaire que s'il ne s'agit pas du réseau sur lequel la commande est " "envoyée." #: plugin.py:244 msgid "" "[--all]\n" "\n" " Returns the networks to which the bot is currently connected.\n" " If --all is given, also includes networks known by the bot,\n" " but not connected to.\n" " " msgstr "" #: plugin.py:255 msgid "disconnected" msgstr "" #: plugin.py:264 msgid "%.2f seconds." msgstr "%.2f secondes" #: plugin.py:267 msgid "" "[]\n" "\n" " Returns the current latency to . is only " "necessary\n" " if the message isn't sent on the network to which this command is " "to\n" " apply.\n" " " msgstr "" "[]\n" "\n" "Retourne la latence actuelle du . Le n'est nécessaire que " "s'il ne s'agit pas du réseau sur lequel la commande est envoyée." #: plugin.py:273 msgid "Latency check (from %s)." msgstr "Vérification de lance (de %s)." #: plugin.py:280 msgid "" "[]\n" "\n" " Returns the current network driver for . is " "only\n" " necessary if the message isn't sent on the network to which this\n" " command is to apply.\n" " " msgstr "" "[]\n" "\n" "Retourne le 'driver' actuel pour le . Le n'est nécessaire " "que s'il ne s'agit pas du réseau sur lequel la commande est envoyée." #: plugin.py:290 #, fuzzy msgid "" "[]\n" "\n" " Returns the time duration since the connection was established.\n" " " msgstr "" "[]\n" "\n" "Indique depuis combien de temps la connexion est établie." #: plugin.py:297 msgid "I've been connected to %s for %s." msgstr "Je suis connecté à %s depuis %s." #: plugin.py:302 #, fuzzy msgid "" "[]\n" "\n" " Returns the list of IRCv3 capabilities available on the network.\n" " " msgstr "" "[]\n" "\n" "Indique depuis combien de temps la connexion est établie." #~ msgid "is an op on %L" #~ msgstr "est op sur %L" #~ msgid "is a halfop on %L" #~ msgstr "est halfop sur %L" #~ msgid "is voiced on %L" #~ msgstr "est voicé sur %L" #~ msgid "is also on %L" #~ msgstr "est aussi sur %L" #~ msgid "is on %L" #~ msgstr "est sur %L" #~ msgid "isn't on any non-secret channels" #~ msgstr "n'est sur aucun canal non secret" #~ msgid "" #~ msgstr "" #~ msgid " identified" #~ msgstr " identifié" #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s) and %s.%s" #~ msgstr "" #~ "%s (%s) a été%s sur le serveur %s depuis %s (idle depuis %s) et %s.%s" #~ msgid "%s (%s) has been%s on server %s and disconnect on %s." #~ msgstr "%s (%s) a été%s sur le serveur %s puis c’est déconnecté à %s." #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the networks to which the bot is currently connected.\n" #~ " " #~ msgstr "" #~ "ne prend pas d'argument\n" #~ " \n" #~ "Retourne la liste des réseaux auxquels le bot est actuellement connecté." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/locales/it.po���������������������������������������������������0000644�0001750�0001750�00000016460�14535072470�017637� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-07-04 19:51+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:47 msgid "" "Provides network-related commands, such as connecting to multiple networks\n" " and checking latency to the server." msgstr "" #: plugin.py:59 #, fuzzy msgid "" "[--nossl] [] []\n" "\n" " Connects to another network (which will be represented by the name\n" " provided in ) at . If port is not provided, it\n" " defaults to 6697, the default port for IRC with SSL. If password " "is\n" " provided, it will be sent to the server in a PASS command. If --" "nossl is\n" " provided, an SSL connection will not be attempted, and the port " "will\n" " default to 6667.\n" " " msgstr "" "[--ssl] [] []\n" "\n" " Si connette a un'altra rete (rappresentata dal nome dato a ) " "su\n" " . Se la porta non è fornita usa la 6667, quella " "predefinita per\n" " IRC. Se viene specificata la password sarà inviata al server tramite " "il comando\n" " PASS. Se --ssl è specificato, sarà richiesta una connessione SSL.\n" " " #: plugin.py:74 msgid "I'm already connected to %s." msgstr "Sono già connesso a %s." #: plugin.py:96 msgid "A server must be provided if the network is not already registered." msgstr "Se la rete non è già registrata è necessario fornire un server." #: plugin.py:104 msgid "Connection to %s initiated." msgstr "Connessione a %s iniziata." #: plugin.py:110 #, fuzzy msgid "" " []\n" "\n" " Disconnects from the network represented by the network .\n" " If is given, quits the network with the given quit\n" " message.\n" " " msgstr "" "[] []\n" "\n" " Si disconnette dalla rete (rappresentata dal nome dato a ).\n" " Se è specificato, esce con tale messaggio.\n" " è necessaria solo se la rete è differente da quella sulla " "quale è inviato il comando.\n" " " #: plugin.py:124 msgid "Disconnection to %s initiated." msgstr "Disconnessione a %s iniziata." #: plugin.py:129 msgid "" "[] []\n" "\n" " Disconnects and then reconnects to . If no network is " "given,\n" " disconnects and then reconnects to the network the command was " "given\n" " on. If no quit message is given, uses the configured one\n" " (supybot.plugins.Owner.quitMsg) or the nick of the person giving " "the\n" " command.\n" " " msgstr "" "[] []\n" "\n" " Si disconnette da e si riconnette. è necessaria solo " "se la rete\n" " è differente da quella sulla quale è inviato il comando. Se non " "viene specificato un\n" " messaggio di uscita, utilizza quello configurato in supybot.plugins." "Owner.quitMsg\n" " o il nick di chi ha dato il comando.\n" " " #: plugin.py:148 msgid "" " [ ...]\n" "\n" " Gives the bot (with its associated s) on .\n" " " msgstr "" " [ ...]\n" "\n" " Invia al bot (con i suoi argomenti) su .\n" " " #: plugin.py:156 #, fuzzy msgid "" " [ ...]\n" "\n" " Perform (with its associated s) on all networks.\n" " " msgstr "" " [ ...]\n" "\n" " Invia al bot (con i suoi argomenti) su .\n" " " #: plugin.py:209 #, fuzzy msgid "There is no user %s on %s." msgstr "Non c'è nessun %s su %s." #: plugin.py:211 #, fuzzy msgid "There was no user %s on %s." msgstr "Non c'è nessun %s su %s." #: plugin.py:218 plugin.py:231 msgid "" "[] \n" "\n" " Returns the WHOIS response gives for . " "is\n" " only necessary if the network is different than the network the " "command\n" " is sent on.\n" " " msgstr "" "[] \n" "\n" " Restituisce la risposta di WHOIS per sulla " "specificata. è\n" " necessario solo se la rete è differente da quella sulla quale è " "inviato il comando.\n" " " #: plugin.py:244 msgid "" "[--all]\n" "\n" " Returns the networks to which the bot is currently connected.\n" " If --all is given, also includes networks known by the bot,\n" " but not connected to.\n" " " msgstr "" #: plugin.py:255 msgid "disconnected" msgstr "" #: plugin.py:264 msgid "%.2f seconds." msgstr "%.2f secondi." #: plugin.py:267 msgid "" "[]\n" "\n" " Returns the current latency to . is only " "necessary\n" " if the message isn't sent on the network to which this command is " "to\n" " apply.\n" " " msgstr "" "[]\n" "\n" " Riporta l'attuale latenza di . è necessaria solo se la " "rete\n" " è differente da quella sulla quale è inviato il comando.\n" " " #: plugin.py:273 msgid "Latency check (from %s)." msgstr "Verifica di latenza (da %s)." #: plugin.py:280 msgid "" "[]\n" "\n" " Returns the current network driver for . is " "only\n" " necessary if the message isn't sent on the network to which this\n" " command is to apply.\n" " " msgstr "" "[]\n" "\n" " Riporta l'attuale driver di rete per . è necessaria\n" " solo se la rete è differente da quella sulla quale è inviato il " "comando.\n" " " #: plugin.py:290 #, fuzzy msgid "" "[]\n" "\n" " Returns the time duration since the connection was established.\n" " " msgstr "" "[]\n" " \n" " Riporta da quanto tempo è connesso il bot.\n" " " #: plugin.py:297 msgid "I've been connected to %s for %s." msgstr "Sono connesso a %s da %s." #: plugin.py:302 #, fuzzy msgid "" "[]\n" "\n" " Returns the list of IRCv3 capabilities available on the network.\n" " " msgstr "" "[]\n" " \n" " Riporta da quanto tempo è connesso il bot.\n" " " #~ msgid "is an op on %L" #~ msgstr "è un op su %L" #~ msgid "is a halfop on %L" #~ msgstr "è un halfop su %L" #~ msgid "is voiced on %L" #~ msgstr "ha il voice su %L" #~ msgid "is also on %L" #~ msgstr "è anche su %L" #~ msgid "is on %L" #~ msgstr "è su %L" #~ msgid "isn't on any non-secret channels" #~ msgstr "non è in alcun canale non segreto" #~ msgid "" #~ msgstr "" #~ msgid " identified" #~ msgstr " identificato" #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s) and %s.%s" #~ msgstr "%s (%s) è%s sul server %s dalle %s (inattivo da %s) ed %s.%s" #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns the networks to which the bot is currently connected.\n" #~ " " #~ msgstr "" #~ "non necessita argomenti\n" #~ "\n" #~ " Restituisce le reti alle quali è attualmente connesso il bot.\n" #~ " " ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/plugin.py�������������������������������������������������������0000644�0001750�0001750�00000031254�14535072470�017107� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010,2015 James McCoy # Copyright (c) 2010-2021, 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 time import functools import supybot.conf as conf import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization _ = PluginInternationalization('Network') class Network(callbacks.Plugin): """Provides network-related commands, such as connecting to multiple networks and checking latency to the server.""" _whois = {} _latency = {} def _getIrc(self, network): irc = world.getIrc(network) if irc: return irc else: raise callbacks.Error('I\'m not currently connected to %s.' % network) def connect(self, irc, msg, args, opts, network, server, password): """[--nossl] [] [] Connects to another network (which will be represented by the name provided in ) at . If port is not provided, it defaults to 6697, the default port for IRC with SSL. If password is provided, it will be sent to the server in a PASS command. If --nossl is provided, an SSL connection will not be attempted, and the port will default to 6667. """ if '.' in network: irc.error("Network names cannot have a '.' in them. " "Remember, this is the network name, not the actual " "server you plan to connect to.", Raise=True) try: otherIrc = self._getIrc(network) irc.error(_('I\'m already connected to %s.') % network) return # We've gotta return here. This is ugly code, but I'm not # quite sure what to do about it. except callbacks.Error: pass ssl = True for (opt, arg) in opts: if opt == 'nossl': ssl = False if server: if ':' in server: (server, port) = server.rsplit(':', 1) port = int(port) elif ssl: port = 6697 else: port = 6667 serverPort = (server, port) else: try: serverPort = conf.supybot.networks.get(network).servers()[0] except (registry.NonExistentRegistryEntry, IndexError): irc.error(_('A server must be provided if the network is not ' 'already registered.')) return Owner = irc.getCallback('Owner') newIrc = Owner._connect(network, serverPort=serverPort, password=password, ssl=ssl) conf.supybot.networks().add(network) assert newIrc.callbacks is irc.callbacks, 'callbacks list is different' irc.replySuccess(_('Connection to %s initiated.') % network) connect = wrap(connect, ['owner', getopts({'nossl': ''}), 'something', additional('something'), additional('something', '')]) def disconnect(self, irc, msg, args, otherIrc, quitMsg): """ [] Disconnects from the network represented by the network . If is given, quits the network with the given quit message. """ standard_msg = conf.supybot.plugins.Owner.quitMsg() if standard_msg: standard_msg = ircutils.standardSubstitute(irc, msg, standard_msg) quitMsg = quitMsg or standard_msg or msg.nick otherIrc.queueMsg(ircmsgs.quit(quitMsg)) otherIrc.die() conf.supybot.networks().discard(otherIrc.network) if otherIrc != irc: irc.replySuccess(_('Disconnection to %s initiated.') % otherIrc.network) disconnect = wrap(disconnect, ['owner', ('networkIrc', True), additional('text')]) def reconnect(self, irc, msg, args, otherIrc, quitMsg): """[] [] Disconnects and then reconnects to . If no network is given, disconnects and then reconnects to the network the command was given on. If no quit message is given, uses the configured one (supybot.plugins.Owner.quitMsg) or the nick of the person giving the command. """ standard_msg = conf.supybot.plugins.Owner.quitMsg() if standard_msg: standard_msg = ircutils.standardSubstitute(irc, msg, standard_msg) quitMsg = quitMsg or standard_msg or msg.nick otherIrc.queueMsg(ircmsgs.quit(quitMsg)) if otherIrc != irc: # No need to reply if we're reconnecting ourselves. irc.replySuccess() reconnect = wrap(reconnect, ['owner', 'networkIrc', additional('text')]) def command(self, irc, msg, args, otherIrc, commandAndArgs): """ [ ...] Gives the bot (with its associated s) on . """ self.Proxy(otherIrc, msg, commandAndArgs, replyIrc=irc) command = wrap(command, ['admin', ('networkIrc', True), many('anything')]) def cmdall(self, irc, msg, args, commandAndArgs): """ [ ...] Perform (with its associated s) on all networks. """ ircs = world.ircs for ircd in ircs: self.Proxy(ircd, msg, commandAndArgs) cmdall = wrap(cmdall, ['admin', many('anything')]) ### # whois command-related stuff. ### def do311(self, irc, msg): nick = ircutils.toLower(msg.args[1]) if (irc, nick) not in self._whois: return elif msg.command == '319': if '319' not in self._whois[(irc, nick)][2]: self._whois[(irc, nick)][2][msg.command] = [] self._whois[(irc, nick)][2][msg.command].append(msg) else: self._whois[(irc, nick)][2][msg.command] = msg # These are all sent by a WHOIS response. do301 = do311 do312 = do311 do314 = do311 do317 = do311 do319 = do311 do320 = do311 def do318(self, irc, msg): nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d, command) = self._whois[(irc, loweredNick)] d['318'] = msg s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, channel=replyMsg.args[0], command=command) replyIrc.reply(s) del self._whois[(irc, loweredNick)] do369 = do318 def do402(self, irc, msg): nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d, command) = self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)] if command == 'whois': template = _('There is no user %s on %s.') else: template = _('There was no user %s on %s.') s = template % (nick, irc.network) replyIrc.reply(s) do401 = do402 do406 = do402 def whois(self, irc, msg, args, otherIrc, nick): """[] Returns the WHOIS response gives for . is only necessary if the network is different than the network the command is sent on. """ # Here we use a remote server whois (double nick) to get idle/signon time. otherIrc.queueMsg(ircmsgs.whois(nick, nick)) nick = ircutils.toLower(nick) self._whois[(otherIrc, nick)] = (irc, msg, {}, 'whois') whois = wrap(whois, ['networkIrc', 'nick']) def whowas(self, irc, msg, args, otherIrc, nick): """[] Returns the WHOIS response gives for . is only necessary if the network is different than the network the command is sent on. """ # Here we use a remote server whois (double nick) to get idle/signon time. otherIrc.queueMsg(ircmsgs.whowas(nick, nick)) nick = ircutils.toLower(nick) self._whois[(otherIrc, nick)] = (irc, msg, {}, 'whowas') whowas = wrap(whowas, ['networkIrc', 'nick']) def networks(self, irc, msg, args, opts): """[--all] Returns the networks to which the bot is currently connected. If --all is given, also includes networks known by the bot, but not connected to. """ opts = dict(opts) L = ['%s: %s' % (ircd.network, ircd.server) for ircd in world.ircs] if 'all' in opts: for net in conf.supybot.networks._children.keys(): if net not in [ircd.network for ircd in world.ircs]: L.append('%s: (%s)' % (net, _('disconnected'))) utils.sortBy(str.lower, L) irc.reply(format('%L', L)) networks = wrap(networks, [getopts({'all': ''})]) def doPong(self, irc, msg): now = time.time() if irc in self._latency: (replyIrc, when) = self._latency.pop(irc) replyIrc.reply(_('%.2f seconds.') % (now-when)) def latency(self, irc, msg, args, otherIrc): """[] Returns the current latency to . is only necessary if the message isn't sent on the network to which this command is to apply. """ otherIrc.queueMsg(ircmsgs.ping(_('Latency check (from %s).') % msg.nick)) self._latency[otherIrc] = (irc, time.time()) irc.noReply() latency = wrap(latency, ['networkIrc']) def driver(self, irc, msg, args, otherIrc): """[] Returns the current network driver for . is only necessary if the message isn't sent on the network to which this command is to apply. """ irc.reply(otherIrc.driver.__class__.__module__[8:]) driver = wrap(driver, ['networkIrc']) def uptime(self, irc, msg, args, otherIrc): """[] Returns the time duration since the connection was established. """ network = otherIrc.network now = time.time() started = otherIrc.startedAt irc.reply(_("I've been connected to %s for %s.") % (network, utils.timeElapsed(now - started))) uptime = wrap(uptime, ['networkIrc']) def capabilities(self, irc, msg, args, otherIrc): """[] Returns the list of IRCv3 capabilities available on the network. """ irc.reply(format("%L", sorted(otherIrc.state.capabilities_ls))) capabilities = wrap(capabilities, ['networkIrc']) def authenticate(self, irc, msg, args): """takes no arguments Manually initiate SASL authentication. """ if 'sasl' in irc.state.capabilities_ack: irc.startSasl(msg) irc.replySuccess() else: irc.error(_('SASL not supported')) authenticate = wrap(authenticate) Class = Network # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Network/test.py���������������������������������������������������������0000644�0001750�0001750�00000006031�14535072470�016563� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class NetworkTestCase(PluginTestCase): plugins = ['Network', 'Utilities', 'String', 'Misc'] def testNetworks(self): self.assertNotError('networks') def testCommand(self): self.assertResponse('network command %s echo 1' % self.irc.network, '1') # empty args should be allowed, see # https://github.com/progval/Limnoria/issues/1541 self.assertResponse('network command %s len ""' % self.irc.network, '0') def testCommandRoutesBackToCaller(self): self.otherIrc = getTestIrc("testnet1") # This will fail with timeout if the response never comes back self.assertResponse( 'network command testnet1 echo $network', 'testnet1') def testCommandRoutesErrorsBackToCaller(self): self.otherIrc = getTestIrc("testnet2") self.assertRegexp( f'network command testnet2 re s/.*// test', 'I tried to send you an empty message') def testCommandRoutesMoreBackToCaller(self): self.otherIrc = getTestIrc("testnet3") self.assertNotError('clearmores') self.assertError('more') self.assertRegexp( f'network command testnet3 echo {"Hello"*300}', r'Hello.*\(\d+ more messages\)') self.assertRegexp( 'more', r'Hello.*\(\d+ more messages\)') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/�������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014517� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/__init__.py��������������������������������������������������������0000644�0001750�0001750�00000005331�14535072470�016630� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ A module to allow each channel to have "news". News items may have expiration dates. It was partially inspired by the news system used on #debian's bot. """ 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 __maintainer__ = supybot.authors.limnoria_core # 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/News/download' from . import config from . import plugin from importlib 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: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/config.py����������������������������������������������������������0000644�0001750�0001750�00000004727�14535072470�016346� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('News') 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('News', True) News = conf.registerPlugin('News') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(News, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/locales/�����������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016141� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/locales/fi.po������������������������������������������������������0000644�0001750�0001750�00000011000�14535072470�017065� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: News plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11: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" #: plugin.py:58 msgid "%s (Subject: %q, added by %s on %s)" msgstr "%s (Aihe: %q, lisännyt %s kanavalla %s)" #: plugin.py:62 msgid "%s (Subject: %q, added by %s on %s, expires at %s)" msgstr "%s (Aihe: %q, lisännyt %s kanavalla %s, vanhenee %s)" #: plugin.py:110 msgid "This plugin provides a means of maintaining News for a channel." msgstr "Tämä plugin sallii uutisten ylläpidon kanavalla." #: plugin.py:122 msgid "" "[] : \n" "\n" " Adds a given news item of to a channel with the given " ".\n" " If isn't 0, that news item will expire seconds " "from\n" " now. is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] : \n" "\n" " Lisää annetun uutisaiheen, kanavalle annetulla " ".\n" " Jos ei ole 0, se sekunteja tästä\n" " lähtien. on vaadittu vain jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:134 msgid "(News item #%i added)" msgstr "(Uutisaihe #%i lisätty)" #: plugin.py:139 msgid "" "[] []\n" "\n" " Display the news items for in the format of '(#id) " "subject'.\n" " If is given, retrieve only that news item; otherwise retrieve " "all\n" " news items. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Näyttää uutisaiheet muodossa '(#id) otsikko'.\n" " Jos on annettu, vain se uutisaihe haetaan; muutoin hakee " "kaikki\n" " uutisaiheet. on vaadittu vain, jos viestiä ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:150 msgid "News for %s: %s" msgstr "Uutisia %s::lle %s" #: plugin.py:153 msgid "No news for %s." msgstr "Ei uutisia %s:lle." #: plugin.py:159 plugin.py:173 plugin.py:189 plugin.py:205 msgid "news item id" msgstr "uutisaihe id" #: plugin.py:164 msgid "" "[] \n" "\n" " Removes the news item with from . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Poistaa uutisaiheen . on vaadittu\n" " vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:178 msgid "" "[] \n" "\n" " Changes the news item with from according to the\n" " regular expression . should be of the form\n" " s/text/replacement/flags. is only necessary if the " "message\n" " isn't sent on the channel itself.\n" " " msgstr "" "[] \n" "\n" " Vaihtaa uutisaiheen \n" " mukaan. pitäisi " "olla muotoa \n" " s/teksti/korvaus/liput. on vaadittu vain jos viestiä ei " "lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:194 msgid "" "[] []\n" "\n" " Returns the old news item for with . If no number is\n" " given, returns all the old news items in reverse order. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Palauttaa vanhan uutisaiheen . Jos numeroa ei " "ole\n" " annettu, palauttaa kaikki uutisaiheet käänteisessä järjestyksessä. " " on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:210 msgid "Old news for %s: %s" msgstr "Vanhoja uutisia %s:lle %s" #: plugin.py:213 msgid "No old news for %s." msgstr "Ei vanhoja uutisia %s:lle." ././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/locales/fr.po������������������������������������������������������0000644�0001750�0001750�00000010175�14535072470�017112� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:58 msgid "%s (Subject: %q, added by %s on %s)" msgstr "%s (Sujet : %q, ajouté par %s le %s)" #: plugin.py:62 msgid "%s (Subject: %q, added by %s on %s, expires at %s)" msgstr "%s (Sujet : %q, ajouté par %s le %s, expire à %s)" #: plugin.py:110 msgid "This plugin provides a means of maintaining News for a channel." msgstr "" #: plugin.py:122 msgid "" "[] : \n" "\n" " Adds a given news item of to a channel with the given " ".\n" " If isn't 0, that news item will expire seconds " "from\n" " now. is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] : \n" "\n" "Ajoute la news donnée, contenant le à un , avec le " "donné. Si l' n'est pas 0, la news expirera dans le nombre donné " "de secondes. n'est nécessaire que si le message n'est pas envoyé sur " "le canal lui-même." #: plugin.py:134 msgid "(News item #%i added)" msgstr "(News #%i ajoutée)" #: plugin.py:139 msgid "" "[] []\n" "\n" " Display the news items for in the format of '(#id) " "subject'.\n" " If is given, retrieve only that news item; otherwise retrieve " "all\n" " news items. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Affiche une news sur le dans le format'(#id) sujet'. Si l' est " "donné, ne récupère que la news correspondante ; sinon, récupère toutes les " "news. n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:150 msgid "News for %s: %s" msgstr "News pour %s : %s" #: plugin.py:153 msgid "No news for %s." msgstr "Pas de news pour %s." #: plugin.py:159 plugin.py:173 plugin.py:189 plugin.py:205 msgid "news item id" msgstr "id de news" #: plugin.py:164 msgid "" "[] \n" "\n" " Removes the news item with from . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Retourne la news avec l' du . n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:178 msgid "" "[] \n" "\n" " Changes the news item with from according to the\n" " regular expression . should be of the form\n" " s/text/replacement/flags. is only necessary if the " "message\n" " isn't sent on the channel itself.\n" " " msgstr "" "[] \n" "\n" "Change la news ayant cet sur le , en accord avec l'expression " "régulière . La doit être de la forme s/text/replacement/" "flags. n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:194 msgid "" "[] []\n" "\n" " Returns the old news item for with . If no number is\n" " given, returns all the old news items in reverse order. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Retourne l'ancienne news du avec l'. Si aucun nombre n'est " "donné, retourne toutes les anciennes news, dans l'ordre inverse. " "n'est nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:210 msgid "Old news for %s: %s" msgstr "Anciennes news pour %s : %s" #: plugin.py:213 msgid "No old news for %s." msgstr "Pas d'ancienne news pour %s." ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/locales/it.po������������������������������������������������������0000644�0001750�0001750�00000010332�14535072470�017112� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-19 12:51+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:58 msgid "%s (Subject: %q, added by %s on %s)" msgstr "%s (Soggetto: %q, aggiunto da %s il %s)" #: plugin.py:62 msgid "%s (Subject: %q, added by %s on %s, expires at %s)" msgstr "%s (Soggetto: %q, aggiunto da %s il %s, scade il %s)" #: plugin.py:110 msgid "This plugin provides a means of maintaining News for a channel." msgstr "" #: plugin.py:122 msgid "" "[] : \n" "\n" " Adds a given news item of to a channel with the given " ".\n" " If isn't 0, that news item will expire seconds " "from\n" " now. is only necessary if the message isn't sent in the\n" " channel itself.\n" " " msgstr "" "[] : \n" "\n" " Aggiunge ad un canale una notizia contenente con il " " fornito.\n" " Se non è uguale a 0, la notizia scadrà entro quel numero " "di secondi.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:134 msgid "(News item #%i added)" msgstr "(Notizia #%i aggiunta)" #: plugin.py:139 msgid "" "[] []\n" "\n" " Display the news items for in the format of '(#id) " "subject'.\n" " If is given, retrieve only that news item; otherwise retrieve " "all\n" " news items. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Visualizza le notizie per nel formato '(#id) soggetto'.\n" " Se è fornito, riporta solo quella notizia; altrimenti le " "riporta tutte.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:150 msgid "News for %s: %s" msgstr "Notizia per %s: %s" #: plugin.py:153 msgid "No news for %s." msgstr "Nessuna notizia per %s." #: plugin.py:159 plugin.py:173 plugin.py:189 plugin.py:205 msgid "news item id" msgstr "id della notizia" #: plugin.py:164 msgid "" "[] \n" "\n" " Removes the news item with from . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Rimuove da la notizia con il dato . è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:178 msgid "" "[] \n" "\n" " Changes the news item with from according to the\n" " regular expression . should be of the form\n" " s/text/replacement/flags. is only necessary if the " "message\n" " isn't sent on the channel itself.\n" " " msgstr "" "[] \n" "\n" " Modifica da la notiza con il dato in base " "all'espressione\n" " regolare . deve essere nella forma s/text/" "replacement/flags.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:194 msgid "" "[] []\n" "\n" " Returns the old news item for with . If no number is\n" " given, returns all the old news items in reverse order. " "is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Riporta la notizia vecchia con per . Se non viene " "fornito\n" " alcun numero, riporta tutte le notizie vecchie in ordine inverso.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:210 msgid "Old news for %s: %s" msgstr "Notizia vecchia per %s: %s" #: plugin.py:213 msgid "No old news for %s." msgstr "Nessuna notizia vecchia per %s." ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/plugin.py����������������������������������������������������������0000644�0001750�0001750�00000020723�14535072470�016371� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 time import supybot.dbi as dbi import supybot.conf as conf 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.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('News') class DbiNewsDB(plugins.DbiChannelDB): class DB(dbi.DB): class Record(dbi.Record): __fields__ = [ 'subject', 'text', 'at', 'expires', 'by', ] def __str__(self): user = plugins.getUserName(self.by) if self.expires == 0: s = format(_('%s (Subject: %q, added by %s on %s)'), self.text, self.subject, self.by, utils.str.timestamp(self.at)) else: s = format(_('%s (Subject: %q, added by %s on %s, ' 'expires at %s)'), self.text, self.subject, user, utils.str.timestamp(self.at), utils.str.timestamp(self.expires)) return s def __init__(self, filename): # We use self.__class__ here because apparently DB isn't in our # scope. python-- self.__parent = super(self.__class__, self) self.__parent.__init__(filename) def add(self, subject, text, at, expires, by): return self.__parent.add(self.Record(at=at, by=by, text=text, subject=subject, expires=expires)) def getOld(self, id=None): now = time.time() if id: return self.get(id) else: L = [R for R in self if R.expires < now and R.expires != 0] if not L: raise dbi.NoRecordError else: return L def get(self, id=None): now = time.time() if id: return self.__parent.get(id) else: L = [R for R in self if R.expires >= now or R.expires == 0] if not L: raise dbi.NoRecordError return L def change(self, id, f): news = self.get(id) s = '%s: %s' % (news.subject, news.text) s = f(s) (news.subject, news.text) = s.split(': ', 1) self.set(id, news) NewsDB = plugins.DB('News', {'flat': DbiNewsDB}) class News(callbacks.Plugin): """This plugin provides a means of maintaining News for a channel.""" def __init__(self, irc): self.__parent = super(News, self) self.__parent.__init__(irc) self.db = NewsDB() def die(self): self.__parent.die() self.db.close() @internationalizeDocstring def add(self, irc, msg, args, channel, user, at, expires, news): """[] : Adds a given news item of to a channel with the given . If isn't 0, that news item will expire seconds from now. is only necessary if the message isn't sent in the channel itself. """ try: (subject, text) = news.split(': ', 1) except ValueError: raise callbacks.ArgumentError id = self.db.add(channel, subject, text, at, expires, user.id) irc.replySuccess(format(_('(News item #%i added)'), id)) add = wrap(add, ['channeldb', 'user', 'now', 'expiry', 'text']) @internationalizeDocstring def news(self, irc, msg, args, channel, id): """[] [] Display the news items for in the format of '(#id) subject'. If is given, retrieve only that news item; otherwise retrieve all news items. is only necessary if the message isn't sent in the channel itself. """ if not id: try: records = self.db.get(channel) items = [format('(#%i) %s', R.id, R.subject) for R in records] s = format(_('News for %s: %s'), channel, '; '.join(items)) irc.reply(s) except dbi.NoRecordError: irc.reply(format(_('No news for %s.'), channel)) else: try: record = self.db.get(channel, id) irc.reply(str(record)) except dbi.NoRecordError as id: irc.errorInvalid(_('news item id'), id) news = wrap(news, ['channeldb', additional('positiveInt')]) @internationalizeDocstring def remove(self, irc, msg, args, channel, id): """[] Removes the news item with from . is only necessary if the message isn't sent in the channel itself. """ try: self.db.remove(channel, id) irc.replySuccess() except dbi.NoRecordError: irc.errorInvalid(_('news item id'), id) remove = wrap(remove, ['channeldb', 'positiveInt']) @internationalizeDocstring def change(self, irc, msg, args, channel, id, replacer): """[] Changes the news item with from according to the regular expression . should be of the form s/text/replacement/flags. is only necessary if the message isn't sent on the channel itself. """ try: self.db.change(channel, id, replacer) irc.replySuccess() except dbi.NoRecordError: irc.errorInvalid(_('news item id'), id) change = wrap(change, ['channeldb', 'positiveInt', 'regexpReplacer']) @internationalizeDocstring def old(self, irc, msg, args, channel, id): """[] [] Returns the old news item for with . If no number is given, returns all the old news items in reverse order. is only necessary if the message isn't sent in the channel itself. """ if id: try: record = self.db.getOld(channel, id) irc.reply(str(record)) except dbi.NoRecordError as id: irc.errorInvalid(_('news item id'), id) else: try: records = self.db.getOld(channel) items = [format('(#%i) %s', R.id, R.subject) for R in records] s = format(_('Old news for %s: %s'), channel, '; '.join(items)) irc.reply(s) except dbi.NoRecordError: irc.reply(format(_('No old news for %s.'), channel)) old = wrap(old, ['channeldb', additional('positiveInt')]) Class = News # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/News/test.py������������������������������������������������������������0000644�0001750�0001750�00000007040�14535072470�016047� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003-2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 __future__ import print_function import time from supybot.test import * class NewsTestCase(ChannelPluginTestCase): plugins = ('News','User') def setUp(self): ChannelPluginTestCase.setUp(self) # Create a valid user to use self.prefix = 'news!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.nick, 'register tester moo', prefix=self.prefix)) m = self.irc.takeMsg() # Response to register. def testAddnews(self): self.assertNotError('add 0 subject: foo') self.assertRegexp('news', 'subject') self.assertNotError('add 0 subject2: foo2') self.assertRegexp('news', 'subject.*subject2') self.assertNotError('add 5 subject3: foo3') self.assertRegexp('news', 'subject3') timeFastForward(6) self.assertNotRegexp('news', 'subject3') def testNews(self): # These should both fail first, as they will have nothing in the DB self.assertRegexp('news', 'no news') self.assertRegexp('news #channel', 'no news') # Now we'll add news and make sure listnews doesn't fail self.assertNotError('add #channel 0 subject: foo') self.assertNotError('news #channel') self.assertNotError('add 0 subject: foo') self.assertRegexp('news', '#1') self.assertNotError('news 1') def testChangenews(self): self.assertNotError('add 0 Foo: bar') self.assertNotError('change 1 s/bar/baz/') self.assertNotRegexp('news 1', 'bar') self.assertRegexp('news 1', 'baz') def testOldnews(self): self.assertRegexp('old', 'No old news') self.assertNotError('add 0 a: b') self.assertRegexp('old', 'No old news') self.assertNotError('add 5 foo: bar') self.assertRegexp('old', 'No old news') timeFastForward(6) self.assertNotError('old') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/���������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015311� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/__init__.py����������������������������������������������������0000644�0001750�0001750�00000005016�14535072470�017422� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2012-2021, 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. ### """ Support authentication based on nicks and network services. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.progval __maintainer__ = supybot.authors.limnoria_core # 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 importlib 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 tabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/config.py������������������������������������������������������0000644�0001750�0001750�00000004677�14535072470�017144� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2012-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('NickAuth') 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('NickAuth', True) NickAuth = conf.registerPlugin('NickAuth') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(NickAuth, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: �����������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/locales/�������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016733� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/locales/de.po��������������������������������������������������0000644�0001750�0001750�00000011345�14535072470�017665� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# NickAuth plugin in Limnoria. # Copyright (C) 2013 Limnoria # Alexander Minges , 2013. # msgid "" msgstr "" "Project-Id-Version: NickAuth plugin in Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2013-01-25 23:59+0100\n" "Last-Translator: Alexander Minges \n" "Language-Team: Deutsch \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" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 2.91.6\n" #: plugin.py:47 msgid "" "\n" " This plugin allows users to use their network services account to\n" " authenticate to the bot.\n" "\n" " They first have to use ``@nickauth nick add `` while being\n" " identified to the bot and then use ``@auth`` when they want to\n" " identify to the bot.\n" " " msgstr "" #: plugin.py:62 plugin.py:67 msgid "You are not authenticated." msgstr "Du bist nicht authentifiziert." #: plugin.py:70 msgid "You must be owner to do that." msgstr "Du musst der Eigentümer sein, um dies tun zu können." #: plugin.py:75 msgid "" "[] \n" "\n" " Add to the list of nicks owned by the on the\n" " . You have to register this nick to the network\n" " services to be authenticated.\n" " defaults to the current network.\n" " " msgstr "" "[] \n" "\n" " Füge der Liste von Nicknamen zu, die dem Benutzer " " im\n" " gehören. Du musst diesen Nicknamen beim Netzwerk-" "Dienst\n" " angemeldet haben, um authentifiziert werden zu können.\n" " ist standardmäßig das aktuelle Netzwerk.\n" " " #: plugin.py:88 msgid "This nick is already used by someone on this network." msgstr "Dieser Nick wird bereits von jemandem in diesem Netzwerk genutzt." #: plugin.py:97 msgid "" "[] \n" "\n" " Remove from the list of nicks owned by the on the\n" " .\n" " defaults to the current network.\n" " " msgstr "" "[] \n" "\n" " Entferne aus der Liste von Nicknamen, die dem " "Benutzer im\n" " gehören.\n" " ist standardmäßig das aktuelle Netzwerk.\n" " " #: plugin.py:109 msgid "This nick is not registered to you on this network." msgstr "Dieser Nick ist nicht von Dir in diesem Netzwerk registriert." #: plugin.py:118 msgid "" "[] []\n" "\n" " Lists nicks of the on the network.\n" " defaults to the current network.\n" " " msgstr "" "[] []\n" "\n" " Listet die Nicknamen des im Netzwerk auf.\n" " ist standardmäßig das aktuelle Netzwerk.\n" " " #: plugin.py:127 msgid "You are not identified and is not given." msgstr "" #: plugin.py:138 msgid "You have no recognized nick on this network." msgstr "Du hast keinen bekannten Nick in diesem Netzwerk." #: plugin.py:141 #, fuzzy msgid "%s has no recognized nick on this network." msgstr "Du hast keinen bekannten Nick in diesem Netzwerk." #: plugin.py:148 msgid "" "takes no argument\n" "\n" " Tries to authenticate you using network services.\n" " If you get no reply, it means you are not authenticated to the\n" " network services." msgstr "" "akzeptiert keine Argumente\n" "\n" " Versucht Dich mit Hilfe des Netzwerk-Dienstes zu authentifizieren.\n" " Falls Du keine Antwort erhältst, bedeutet dies, dass Du nicht beim\n" " Netzwerk-Dienst bekannt bist.\n" " " #: plugin.py:159 msgid "" "If the messages has a server tag with account name, tries to\n" " authenticate it." msgstr "" #: plugin.py:180 msgid "" "Your secure flag is true and your hostmask doesn't match any of your known " "hostmasks." msgstr "" #: plugin.py:184 msgid "You are now authenticated as %s." msgstr "Du bist nun als %s authentifiziert." #: plugin.py:186 msgid "" "No user claimed the nick %s on this network. If this is you, you should " "connect with an other method and use the \"nickauth nick add\" command, or " "ask the owner of the bot to do it." msgstr "" #~ msgid "Support authentication based on nicks and network services." #~ msgstr "" #~ "Unterstützt die Authentifizierung basierend auf Nicknamen und Netzwerk-" #~ "Diensten." #~ msgid "No user has this nick on this network." #~ msgstr "Kein Benutzer hat diesen Nick in diesem Netzwerk." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/locales/fi.po��������������������������������������������������0000644�0001750�0001750�00000011346�14535072470�017674� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# NickAuth plugin in Limnoria. # Copyright (C) 2012-2014 Limnoria # Mikaela Suomalainen , 2012-2014. # msgid "" msgstr "" "Project-Id-Version: NickAuth plugin in Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14:16+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: suomi <>\n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: \n" "X-Generator: Poedit 1.6.10\n" #: plugin.py:47 msgid "" "\n" " This plugin allows users to use their network services account to\n" " authenticate to the bot.\n" "\n" " They first have to use ``@nickauth nick add `` while being\n" " identified to the bot and then use ``@auth`` when they want to\n" " identify to the bot.\n" " " msgstr "" #: plugin.py:62 plugin.py:67 msgid "You are not authenticated." msgstr "Et ole tunnistautunut." #: plugin.py:70 msgid "You must be owner to do that." msgstr "Vain omistaja voi tehdä tuon." #: plugin.py:75 msgid "" "[] \n" "\n" " Add to the list of nicks owned by the on the\n" " . You have to register this nick to the network\n" " services to be authenticated.\n" " defaults to the current network.\n" " " msgstr "" "[] \n" "\n" "Lisää omistamiin nimimerkkeihin\n" " . Tämä nimimerkki täytyy rekisteröidä verkkopalveluille, jotta " "sillä\n" " voidaan tunnistautua. on oletuksena nykyinen verkko." #: plugin.py:88 msgid "This nick is already used by someone on this network." msgstr "Joku muu käyttää jo tuota nimimerkkiä tässä verkossa." #: plugin.py:97 msgid "" "[] \n" "\n" " Remove from the list of nicks owned by the on the\n" " .\n" " defaults to the current network.\n" " " msgstr "" "[] \n" "\n" " Poistaa omistamista nimimerkeistä .\n" " on oletuksena nykyinen verkko." #: plugin.py:109 msgid "This nick is not registered to you on this network." msgstr "Tuota nimimerkkiä ei ole rekisteröity käyttöösi tässä verkossa." #: plugin.py:118 msgid "" "[] []\n" "\n" " Lists nicks of the on the network.\n" " defaults to the current network.\n" " " msgstr "" "[] []\n" "\n" " Luettelee nimimerkit .\n" " on oletuksena nykyinen verkko." #: plugin.py:127 msgid "You are not identified and is not given." msgstr "Et ole tunnistautunut, etkä antanut parametriä." #: plugin.py:138 msgid "You have no recognized nick on this network." msgstr "Sinulla ei ole tunnettua nimimerkkiä tässä verkossa." #: plugin.py:141 msgid "%s has no recognized nick on this network." msgstr "Käyttäjällä %s ei ole tunnistettua nimimerkkiä tässä verkossa." #: plugin.py:148 msgid "" "takes no argument\n" "\n" " Tries to authenticate you using network services.\n" " If you get no reply, it means you are not authenticated to the\n" " network services." msgstr "" "ei ota parametrejä\n" "\n" " Yrittää tunnistaa käyttäjän käyttäen verkkopalveluita.\n" " Mikäli et saa vastausta, se tarkoittaa ettet ole tunnistautunut " "verkkopalveluille." #: plugin.py:159 msgid "" "If the messages has a server tag with account name, tries to\n" " authenticate it." msgstr "" #: plugin.py:180 msgid "" "Your secure flag is true and your hostmask doesn't match any of your known " "hostmasks." msgstr "" "Secure-asetuksesi on true ja hostmaskisi ei täsmää yhteenkään\n" " tunnettuun hostmaskiisi." #: plugin.py:184 msgid "You are now authenticated as %s." msgstr "Olet nyt tunnistautunut käyttäjäksi %s." #: plugin.py:186 msgid "" "No user claimed the nick %s on this network. If this is you, you should " "connect with an other method and use the \"nickauth nick add\" command, or " "ask the owner of the bot to do it." msgstr "" # I added explaining on what this does to brackets as the explanation was a little unclear. #~ msgid "Supports authentication based on nicks and network services." #~ msgstr "" #~ "Tukee tunnistautumista käyttäen nimimerkkejä ja verkkopalveluita.\n" #~ " (Kirjautuessasi sisään NickServille botti tunnistaa sinut, mikäli olet \n" #~ " kertonut botille nimimerkkisi)." #~ msgid "No user has this nick on this network." #~ msgstr "Yhdelläkään käyttäjällä ei ole nimimerkkiä tässä verkossa." ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/plugin.py������������������������������������������������������0000644�0001750�0001750�00000021775�14535072470�017173� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2012-2021, 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 time import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb 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, internationalizeDocstring _ = PluginInternationalization('NickAuth') @internationalizeDocstring class NickAuth(callbacks.Plugin): """ This plugin allows users to use their network services account to authenticate to the bot. They first have to use ``@nickauth nick add `` while being identified to the bot and then use ``@auth`` when they want to identify to the bot. """ def __init__(self, irc): super(NickAuth, self).__init__(irc) self._requests = {} class nick(callbacks.Commands): def _check_auth(self, irc, msg, user): if user is None: irc.error(_('You are not authenticated.'), Raise=True) if not user.checkHostmask(msg.prefix): try: u = ircdb.users.getUser(msg.prefix) except KeyError: irc.error(_('You are not authenticated.'), Raise=True) if not u._checkCapability('owner'): irc.error(_('You must be owner to do that.'), Raise=True) @internationalizeDocstring def add(self, irc, msg, args, network, user, nick): """[] Add to the list of nicks owned by the on the . You have to register this nick to the network services to be authenticated. defaults to the current network. """ network = network.network or irc.network user = user or ircdb.users.getUser(msg.prefix) self._check_auth(irc, msg, user) try: user.addNick(network, nick) except KeyError: irc.error(_('This nick is already used by someone on this ' 'network.'), Raise=True) irc.replySuccess() add = wrap(add, [optional('networkIrc'), optional('otherUser'), 'nick']) @internationalizeDocstring def remove(self, irc, msg, args, network, user, nick): """[] Remove from the list of nicks owned by the on the . defaults to the current network. """ network = network.network or irc.network user = user or ircdb.users.getUser(msg.prefix) self._check_auth(irc, msg, user) try: user.removeNick(network, nick) except KeyError: irc.error(_('This nick is not registered to you on this ' 'network.'), Raise=True) irc.replySuccess() remove = wrap(remove, [optional('networkIrc'), optional('otherUser'), 'nick']) @internationalizeDocstring def list(self, irc, msg, args, network, user): """[] [] Lists nicks of the on the network. defaults to the current network. """ network = network.network or irc.network try: user = user or ircdb.users.getUser(msg.prefix) except KeyError: irc.error(_('You are not identified and is not given.'), Raise=True) self._check_auth(irc, msg, user) try: list_ = user.nicks[network] if list_: irc.reply(format('%L', list_)) else: raise KeyError except KeyError: if user == ircdb.users.getUser(msg.prefix): irc.error(_('You have no recognized nick on this ' 'network.'), Raise=True) else: irc.error(_('%s has no recognized nick on this ' 'network.') % user.name, Raise=True) list = wrap(list, [optional('networkIrc'), optional('otherUser')]) @internationalizeDocstring def auth(self, irc, msg, args): """takes no argument Tries to authenticate you using network services. If you get no reply, it means you are not authenticated to the network services.""" nick = ircutils.toLower(msg.nick) self._requests[(irc.network, msg.nick)] = (time.time(), msg.prefix, irc) irc.queueMsg(ircmsgs.whois(nick, nick)) auth = wrap(auth, []) def inFilter(self, irc, msg): """If the messages has a server tag with account name, tries to authenticate it.""" if msg.server_tags and 'account' in msg.server_tags: self._auth(irc, msg.prefix, msg.server_tags['account']) return msg def do330(self, irc, msg): # RPL_WHOISACCOUNT mynick, theirnick, theiraccount, garbage = msg.args now = time.time() self._requests = { x: y for x,y in self._requests.items() if y[0]+60 > now} try: (timestamp, prefix, irc) = self._requests.pop((irc.network, theirnick)) except KeyError: return user = ircdb.users.getUserFromNick(irc.network, theiraccount) if user: try: user.addAuth(prefix) except ValueError: irc.error(_('Your secure flag is true and your hostmask ' 'doesn\'t match any of your known hostmasks.'), Raise=True) ircdb.users.setUser(user, flush=False) irc.reply(_('You are now authenticated as %s.') % user.name) else: irc.error(_('No user claimed the nick %s on this network. ' 'If this is you, you should connect with an other ' 'method and use the "nickauth nick add" command, ' 'or ask the owner of the bot to do it.') % (theiraccount,)) def doAccount(self, irc, msg): account = msg.args[0] if account != '*': self._auth(irc, msg.prefix, account) def doJoin(self, irc, msg): if 'extended-join' not in irc.state.capabilities_ack: return account = msg.args[1] if account != '*': self._auth(irc, msg.prefix, account) def do354(self, irc, msg): # RPL_WHOSPCRPL, ie. 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, ident, ip, host, nick, status, account, gecos) = msg.args prefix = '%s!%s@%s' % (nick, ident, host) if account != '0': self._auth(irc, prefix, account) def _auth(self, irc, prefix, account): user = ircdb.users.getUserFromNick(irc.network, account) if not user: try: user = ircdb.users.getUser(prefix) except KeyError: user = None if user: user.addAuth(prefix) ircdb.users.setUser(user, flush=False) Class = NickAuth # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickAuth/test.py��������������������������������������������������������0000644�0001750�0001750�00000014242�14535072470�016643� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2012-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.ircdb as ircdb import supybot.ircutils as ircutils from supybot.test import * class NickAuthTestCase(PluginTestCase): plugins = ('NickAuth', 'User') prefix1 = 'something!user@host.tld' def _register(self): self.assertNotError('register foobar 123') self.assertResponse('user list', 'foobar') self.assertNotError('hostmask remove foobar %s' % self.prefix) self.assertNotError('identify foobar 123') self.assertNotError('nick add foobar baz') self.assertNotError('unidentify') def _procedure(self, nickserv_reply): self._register() self.prefix = self.prefix1 self.assertError('nick add foobar qux') self.nick = self.prefix.split('!')[0] self.assertError('hostmask list') self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'auth', prefix=self.prefix)) self.assertEqual(self.irc.takeMsg().command, 'WHOIS') self.assertError('hostmask list') self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'auth', prefix=self.prefix)) self.assertEqual(self.irc.takeMsg().command, 'WHOIS') if nickserv_reply: self.irc.feedMsg(ircmsgs.IrcMsg(':leguin.freenode.net 330 pgjrgrg ' '%s baz :is logged in as' % self.nick)) msg = self.irc.takeMsg() self.assertNotEqual(msg, None) self.assertEqual(msg.args[1], 'You are now authenticated as foobar.') self.assertResponse('hostmask list', 'foobar has no registered hostmasks.') else: msg = self.irc.takeMsg() self.assertEqual(msg, None) self.assertError('hostmask list') def testAuth(self): self._procedure(True) def testNoAuth(self): self._procedure(False) def testUserJoin(self): self.irc.feedMsg(ircmsgs.IrcMsg('CAP * NEW extended-join')) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP') self.assertEqual(m.args, ('REQ', 'extended-join')) self.irc.feedMsg(ircmsgs.IrcMsg('CAP * ACK extended-join')) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP') self.assertEqual(m.args, ('END',)) self.assertEqual(self.irc.takeMsg(), None) channel = '#test' self.irc.feedMsg(ircmsgs.join(channel, prefix=self.prefix)) self.assertEqual(self.irc.takeMsg().command, 'MODE') self.assertEqual(self.irc.takeMsg().command, 'MODE') self.assertEqual(self.irc.takeMsg().command, 'WHO') self.assertEqual(self.irc.takeMsg(), None) self._register() self.assertRegexp( 'whoami', "I don't recognize you", frm=self.prefix1) self.irc.feedMsg(ircmsgs.IrcMsg( ':%s JOIN %s baz :Real name' % (self.prefix1, channel))) self.assertResponse('ping', 'pong') self.assertResponse('whoami', 'foobar', frm=self.prefix1) def testBotJoin(self): channel = '#test' self.irc.feedMsg(ircmsgs.join(channel, prefix=self.prefix)) self.assertEqual(self.irc.takeMsg().command, 'MODE') self.assertEqual(self.irc.takeMsg().command, 'MODE') self.assertEqual(self.irc.takeMsg().command, 'WHO') self.assertEqual(self.irc.takeMsg(), None) self._register() self.assertRegexp( 'whoami', "I don't recognize you", frm=self.prefix1) (nick, ident, host) = ircutils.splitHostmask(self.prefix1) self.irc.feedMsg(ircmsgs.IrcMsg( ':card.freenode.net 354 pgjrgrg 1 %(ident)s 255.255.255.255 ' '%(host)s %(nick)s H baz :gecos' % dict(nick=nick, ident=ident, host=host))) self.assertResponse('ping', 'pong') self.assertResponse('whoami', 'foobar', frm=self.prefix1) def testList(self): self.assertNotError('register foobar 123') self.assertRegexp('nick list', 'You have no recognized nick') self.assertNotError('nick add foo') self.assertRegexp('nick list', 'foo') self.assertNotError('nick add %s bar' % self.nick) self.assertRegexp('nick list', 'foo and bar') self.assertNotError('nick add %s %s baz' % (self.irc.network, self.nick)) self.assertRegexp('nick list', 'foo, bar, and baz') self.assertRegexp('nick list %s' % self.irc.network, 'foo, bar, and baz') self.assertRegexp('nick list %s foobar' % self.irc.network, 'foo, bar, and baz') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016013� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/__init__.py�������������������������������������������������0000644�0001750�0001750�00000005026�14535072470�020125� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This module attempts to capture the bot's nick, watching for an opportunity to switch to that nick. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/config.py���������������������������������������������������0000644�0001750�0001750�00000005262�14535072470�017635� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('NickCapture') 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('NickCapture', True) NickCapture = conf.registerPlugin('NickCapture') conf.registerPlugin('NickCapture') conf.registerGlobalValue(NickCapture, 'ison', registry.Boolean(True, _("""Determines whether the bot will check occasionally if its preferred nick is in use via the ISON command."""))) conf.registerGlobalValue(NickCapture.ison, 'period', registry.PositiveInteger(600, _("""Determines how often (in seconds) the bot will check whether its nick ISON."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/locales/����������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�017435� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/locales/de.po�����������������������������������������������0000644�0001750�0001750�00000003076�14535072477�020400� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-31 13:02+0100\n" "Last-Translator: Florian Besser \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: German\n" "X-Poedit-Country: GERMANY\n" #: config.py:48 msgid "" "Determines whether the bot will check\n" " occasionally if its preferred nick is in use via the ISON command." msgstr "" "Legt fest ob der Bot bei Bedarf, mit dem ISON Befehl, checken soll ob sein " "geschwünschter Nick in benutzung ist." #: config.py:51 msgid "" "Determines how often (in seconds) the bot\n" " will check whether its nick ISON." msgstr "" "Legt fest wie oft (in Sekunden) der Bot checkt ob der Nick verfügbar ist." #: plugin.py:41 msgid "" "This plugin constantly tries to take whatever nick is configured as\n" " supybot.nick. Just make sure that's set appropriately, and this plugin\n" " will do the rest." msgstr "" "Dieses Plugin versucht dauernd den Nick der in supybot.nick konfiguriert ist " "zu bekommen. Stelle nur sicher das dieser richtig gesetzt wurde und das " "Plugin macht den Rest." #: plugin.py:106 msgid "This is returned by the ISON command." msgstr "Dies wird vom ISON Befehl zurückgegeben." #: plugin.py:113 msgid "This is sent by the MONITOR when a nick goes offline." msgstr "" #: plugin.py:123 msgid "Nick/channel is temporarily unavailable" msgstr "" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/locales/fi.po�����������������������������������������������0000644�0001750�0001750�00000003264�14535072477�020405� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# NickCapture plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-10 15:06+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: config.py:48 msgid "" "Determines whether the bot will check\n" " occasionally if its preferred nick is in use via the ISON command." msgstr "" "Määrittää yrittääkö botti tarkistaa\n" " silloin tällöin onko sen haluama nimimerkki käytössä ISON komennolla." #: config.py:51 msgid "" "Determines how often (in seconds) the bot\n" " will check whether its nick ISON." msgstr "" "Määrittää kuinka usein (sekunteina) botti\n" " tarkistaa nimimerkkinsä ISON komennolla." #: plugin.py:41 msgid "" "This plugin constantly tries to take whatever nick is configured as\n" " supybot.nick. Just make sure that's set appropriately, and this plugin\n" " will do the rest." msgstr "" "Tämä lisäosa yrittää jatkuvasti ottaa sen nimimerkin, joka on määritetty\n" " asetusarvossa supybot.nick. Tee vain varmaksi, että se on määritetty " "kunnolla\n" " ja tämä lisäosa hoitaa loput." #: plugin.py:106 msgid "This is returned by the ISON command." msgstr "Tämä on ISON komennon palauttama." #: plugin.py:113 msgid "This is sent by the MONITOR when a nick goes offline." msgstr "" #: plugin.py:123 msgid "Nick/channel is temporarily unavailable" msgstr "" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/locales/fr.po�����������������������������������������������0000644�0001750�0001750�00000003070�14535072477�020411� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:48 msgid "" "Determines whether the bot will check\n" " occasionally if its preferred nick is in use via the ISON command." msgstr "" "Détermine si le bot vérifiera occasionnellement si son nick préféré est en " "cours d'utilisation grâce à la commande ISON." #: config.py:51 msgid "" "Determines how often (in seconds) the bot\n" " will check whether its nick ISON." msgstr "" "Détermine tous les combien de temps (en secondes) le bot vérifiera son nick " "via ISON" #: plugin.py:41 msgid "" "This plugin constantly tries to take whatever nick is configured as\n" " supybot.nick. Just make sure that's set appropriately, and this plugin\n" " will do the rest." msgstr "" "Ce plugin essaye constament de récupérer le nick configuré dans supybot." "nick. Assurez-vous de le configurer correctement, et ce plugin fera le reste." #: plugin.py:106 msgid "This is returned by the ISON command." msgstr "Ceci est retourné par la commande ISON." #: plugin.py:113 msgid "This is sent by the MONITOR when a nick goes offline." msgstr "" #: plugin.py:123 msgid "Nick/channel is temporarily unavailable" msgstr "" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/locales/it.po�����������������������������������������������0000644�0001750�0001750�00000002734�14535072477�020424� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-10 02:13+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:48 msgid "" "Determines whether the bot will check\n" " occasionally if its preferred nick is in use via the ISON command." msgstr "" "Determina se di tanto in tanto il bot controllerà tramite\n" " il comando ISON se il suo nick sia in uso." #: config.py:51 msgid "" "Determines how often (in seconds) the bot\n" " will check whether its nick ISON." msgstr "" "Determina quanto spesso (in secondi) il bot controllerà il suo nick con ISON." #: plugin.py:41 msgid "" "This plugin constantly tries to take whatever nick is configured as\n" " supybot.nick. Just make sure that's set appropriately, and this plugin\n" " will do the rest." msgstr "" "Questo plugin cerca costantemente di ottenere qualsiasi nick sia impostato\n" " come supybot.nick. Assicurati che questa sia configurata correttamente\n" " e il plugin farà il resto." #: plugin.py:106 msgid "This is returned by the ISON command." msgstr "Questo è restituito dal comando ISON." #: plugin.py:113 msgid "This is sent by the MONITOR when a nick goes offline." msgstr "" #: plugin.py:123 msgid "Nick/channel is temporarily unavailable" msgstr "" ������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/plugin.py���������������������������������������������������0000644�0001750�0001750�00000012447�14535072477�017700� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 time import supybot.conf as conf import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('NickCapture') class NickCapture(callbacks.Plugin): """This plugin constantly tries to take whatever nick is configured as supybot.nick. Just make sure that's set appropriately, and this plugin will do the rest.""" public = False def __init__(self, irc): self.__parent = super(NickCapture, self) self.__parent.__init__(irc) self.lastIson = 0 self.monitoring = [] def die(self): for irc in self.monitoring: nick = self._getNick(irc.network) irc.unmonitor(nick) def _getNick(self, network): network_nick = conf.supybot.networks.get(network).nick() if network_nick == '': return conf.supybot.nick() else: return network_nick def __call__(self, irc, msg): if irc.afterConnect: nick = self._getNick(irc.network) if nick and not ircutils.strEqual(nick, irc.nick): # We used to check this, but nicksToHostmasks is never cleared # except on reconnects, which can cause trouble. # if nick not in irc.state.nicksToHostmasks: if 'monitor' in irc.state.supported: if irc not in self.monitoring: irc.monitor(nick) self.monitoring.append(irc) else: self._ison(irc, nick) self.__parent.__call__(irc, msg) def _ison(self, irc, nick): assert 'monitor' not in irc.state.supported if self.registryValue('ison'): now = time.time() if now - self.lastIson > self.registryValue('ison.period'): self.lastIson = now self._sendIson(irc, nick) def _sendIson(self, irc, nick): self.log.info('Checking if %s ISON %s.', nick, irc.network) irc.queueMsg(ircmsgs.ison(nick)) def _sendNick(self, irc, nick): self.log.info('Attempting to switch to nick %s on %s.', nick, irc.network) irc.sendMsg(ircmsgs.nick(nick)) def doQuit(self, irc, msg): nick = self._getNick(irc.network) if ircutils.strEqual(msg.nick, nick): self._sendNick(irc, nick) def doNick(self, irc, msg): nick = self._getNick(irc.network) if ircutils.strEqual(msg.nick, nick): self._sendNick(irc, nick) def do303(self, irc, msg): """This is returned by the ISON command.""" if not msg.args[1]: nick = self._getNick(irc.network) if nick: self._sendNick(irc, nick) def do731(self, irc, msg): """This is sent by the MONITOR when a nick goes offline.""" nick = self._getNick(irc.network) for target in msg.args[1].split(','): if nick == target: self._sendNick(irc, nick) self.monitoring.remove(irc) irc.unmonitor(nick) break def do437(self, irc, msg): """Nick/channel is temporarily unavailable""" if irc.isChannel(msg.args[1]): return self.log.info('Nick %s is unavailable; attempting NickServ release ' 'on %s.' % (msg.args[1], irc.network)) irc.sendMsg(ircmsgs.privmsg('NickServ', 'release %s' % msg.args[1])) NickCapture = internationalizeDocstring(NickCapture) Class = NickCapture # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/NickCapture/test.py�����������������������������������������������������0000644�0001750�0001750�00000003407�14535072470�017346� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class NickCaptureTestCase(PluginTestCase): plugins = ('NickCapture',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/�������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015703� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/__init__.py��������������������������������������������������0000644�0001750�0001750�00000006261�14535072470�020017� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, William Robinson. # Derived from work (c) 1998, Adam Spiers # Copyright (c) 2010-2021, 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. ### ### # This algorithm is almost a direct from a the perl nickometer from # blootbot. Hardly any of the original code has been used, though most of # the comments, I copy-pasted. As a matter of courtesy, the original copyright # message follows: # # # # # Lame-o-Nickometer backend # # # # (c) 1998 Adam Spiers # # # # You may do whatever you want with this code, but give me credit. # # # # $Id: Nickometer.py,v 1.13 2004/10/22 22:19:30 jamessan Exp $ # # ### """ A port of Infobot's nickometer command from Perl. This plugin provides one command (called nickometer) which will tell you how 'lame' an IRC nick is. It's an elitist hacker thing, but quite fun. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.baggins __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/config.py����������������������������������������������������0000644�0001750�0001750�00000005070�14535072470�017522� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, William Robinson. # Derived from work (c) 1998, Adam Spiers # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Nickometer') def configure(advanced): # This will be called by setup.py 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('Nickometer', True) Nickometer = conf.registerPlugin('Nickometer') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Nickometer, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/locales/�����������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�017325� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/locales/fi.po������������������������������������������������0000644�0001750�0001750�00000002472�14535072470�020266� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Nickometer plugin for Limnoria\n" "POT-Creation-Date: 2014-12-20 14:04+EET\n" "PO-Revision-Date: 2014-12-20 14:19+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" #: plugin.py:81 msgid "Will tell you how lame a nick is by the command 'nickometer [nick]'." msgstr "" "Kertoo kuinka laimea nimimerkki on komennolla\n" " 'nickometer [nimimerkki]'" #: plugin.py:88 msgid "" "[]\n" "\n" " Tells you how lame said nick is. If is not given, uses the\n" " nick of the person giving the command.\n" " " msgstr "" "[]\n" "\n" " Kertoo sinulle kuinka laimea sanottu nimimerkki on. Jos " " ei ole annettu, käyttää\n" " komennon antaneen henkilön nimimerkkiä.\n" " " #: plugin.py:231 msgid "The \"lame nick-o-meter\" reading for \"%s\" is %s%%." msgstr "\"Laimea nimimerkki-maatti\" lukema \"%s\":lle on %s%%." ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/locales/fr.po������������������������������������������������0000644�0001750�0001750�00000002003�14535072470�020265� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:81 msgid "Will tell you how lame a nick is by the command 'nickometer [nick]'." msgstr "" #: plugin.py:88 msgid "" "[]\n" "\n" " Tells you how lame said nick is. If is not given, uses the\n" " nick of the person giving the command.\n" " " msgstr "" "[]\n" "\n" "Mesure la décrédibilité du nick. Si le nick n'est pas donné, utilise le nick " "de la personne donnant la commande." #: plugin.py:231 msgid "The \"lame nick-o-meter\" reading for \"%s\" is %s%%." msgstr "Le \"décrédibilit-o-mètre\" pour \"%s\" donne %s%%." �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/locales/it.po������������������������������������������������0000644�0001750�0001750�00000001670�14535072470�020303� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 17:05+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:81 msgid "Will tell you how lame a nick is by the command 'nickometer [nick]'." msgstr "" #: plugin.py:88 msgid "" "[]\n" "\n" " Tells you how lame said nick is. If is not given, uses the\n" " nick of the person giving the command.\n" " " msgstr "" "[]\n" "\n" " Misura quanto sia lamer un nick. Se non è fornito, utilizza\n" " quello della persona che ha dato il comando.\n" " " #: plugin.py:231 msgid "The \"lame nick-o-meter\" reading for \"%s\" is %s%%." msgstr "Il \"nick-o-meter lamer\" per \"%s\" è %s%%." ������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/plugin.py����������������������������������������������������0000644�0001750�0001750�00000022737�14535072470�017564� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, William Robinson. # Derived from work (c) 1998, Adam Spiers # Copyright (c) 2010-2021, 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. ### ### # This algorithm is almost a direct from a the perl nickometer from # blootbot. Hardly any of the original code has been used, though most of # the comments, I copy-pasted. As a matter of courtesy, the original copyright # message follows: # # # # # Lame-o-Nickometer backend # # # # (c) 1998 Adam Spiers # # # # You may do whatever you want with this code, but give me credit. # # # # $Id: Nickometer.py,v 1.13 2004/10/22 22:19:30 jamessan Exp $ # # ### import supybot import re import math import string import supybot.utils as utils import supybot.utils.minisix as minisix import supybot.callbacks as callbacks from supybot.commands import wrap, additional from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Nickometer') def slowExponent(x): return 1.3 * x * (1 - math.atan(x / 6.0) * 2 / math.pi) def slowPow(x, y): return math.pow(x, slowExponent(y)) def caseShifts(s): s=re.sub('[^a-zA-Z]', '', s) s=re.sub('[A-Z]+', 'U', s) s=re.sub('[a-z]+', 'l', s) return len(s)-1 def numberShifts(s): s=re.sub('[^a-zA-Z0-9]', '', s) s=re.sub('[a-zA-Z]+', 'l', s) s=re.sub('[0-9]+', 'n', s) return len(s)-1 class Nickometer(callbacks.Plugin): """Will tell you how lame a nick is by the command 'nickometer [nick]'.""" def punish(self, damage, reason): self.log.debug('%s lameness points awarded: %s', damage, reason) return damage @internationalizeDocstring def nickometer(self, irc, msg, args, nick): """[] Tells you how lame said nick is. If is not given, uses the nick of the person giving the command. """ score = minisix.L(0) if not nick: nick = msg.nick originalNick = nick if not nick: irc.error('Give me a nick to judge as the argument, please.') return specialCost = [('69', 500), ('dea?th', 500), ('dark', 400), ('n[i1]ght', 300), ('n[i1]te', 500), ('fuck', 500), ('sh[i1]t', 500), ('coo[l1]', 500), ('kew[l1]', 500), ('lame', 500), ('dood', 500), ('dude', 500), ('[l1](oo?|u)[sz]er', 500), ('[l1]eet', 500), ('e[l1]ite', 500), ('[l1]ord', 500), ('pron', 1000), ('warez', 1000), ('xx', 100), ('\\[rkx]0', 1000), ('\\0[rkx]', 1000)] letterNumberTranslator = utils.str.MultipleReplacer(dict(list(zip( '023457+8', 'ozeasttb')))) for special in specialCost: tempNick = nick if special[0][0] != '\\': tempNick = letterNumberTranslator(tempNick) if tempNick and re.search(special[0], tempNick, re.IGNORECASE): score += self.punish(special[1], 'matched special case /%s/' % special[0]) # I don't really know about either of these next two statements, # but they don't seem to do much harm. # Allow Perl referencing nick=re.sub('^\\\\([A-Za-z])', '\1', nick); # C-- ain't so bad either nick=re.sub('^C--$', 'C', nick); # Punish consecutive non-alphas matches=re.findall(r'[^\w\d]{2,}',nick) for match in matches: score += self.punish(slowPow(10, len(match)), '%s consecutive non-alphas ' % len(match)) # Remove balanced brackets ... while True: nickInitial = nick nick = re.sub(r'^([^()]*)(\()(.*)(\))([^()]*)$', r'\1\3\5', nick, 1) nick = re.sub(r'^([^{}]*)(\{)(.*)(\})([^{}]*)$', r'\1\3\5', nick, 1) nick = re.sub(r'^([^\[\]]*)(\[)(.*)(\])([^\[\]]*)$', r'\1\3\5', nick, 1) if nick == nickInitial: break self.log.debug('Removed some matching brackets %r => %r', nickInitial, nick) # ... and punish for unmatched brackets unmatched = re.findall('[][(){}]', nick) if len(unmatched) > 0: score += self.punish(slowPow(10, len(unmatched)), '%s unmatched parentheses' % len(unmatched)) # Punish k3wlt0k k3wlt0k_weights = (5, 5, 2, 5, 2, 3, 1, 2, 2, 2) for i in range(len(k3wlt0k_weights)): hits=re.findall(repr(i), nick) if (hits and len(hits)>0): score += self.punish(k3wlt0k_weights[i] * len(hits) * 30, '%s occurrences of %s ' % (len(hits), i)) # An alpha caps is not lame in middle or at end, provided the first # alpha is caps. nickOriginalCase = nick match = re.search('^([^A-Za-z]*[A-Z].*[a-z].*?)[-_]?([A-Z])', nick) if match: nick = ''.join([nick[:match.start(2)], nick[match.start(2)].lower(), nick[match.start(2)+1:]]) match = re.search('^([^A-Za-z]*)([A-Z])([a-z])', nick) if match: nick = ''.join([nick[:match.start(2)], nick[match.start(2):match.end(2)].lower(), nick[match.end(2):]]) # Punish uppercase to lowercase shifts and vice-versa, modulo # exceptions above # the commented line is the equivalent of the original, but i think # they intended my version, otherwise, the first caps alpha will # still be punished #cshifts = caseShifts(nickOriginalCase); cshifts = caseShifts(nick); if cshifts > 1 and re.match('.*[A-Z].*', nick): score += self.punish(slowPow(9, cshifts), '%s case shifts' % cshifts) # Punish lame endings if re.match('.*[XZ][^a-zA-Z]*$', nickOriginalCase): score += self.punish(50, 'the last alphanumeric character was lame') # Punish letter to numeric shifts and vice-versa nshifts = numberShifts(nick); if nshifts > 1: score += self.punish(slowPow(9, nshifts), '%s letter/number shifts' % nshifts) # Punish extraneous caps caps = re.findall('[A-Z]', nick) if caps and len(caps) > 0: score += self.punish(slowPow(7, len(caps)), '%s extraneous caps' % len(caps)) # one trailing underscore is ok. i also added a - for parasite- nick = re.sub('[-_]$','',nick) # Punish anything that's left remains = re.findall('[^a-zA-Z0-9]', nick) if remains and len(remains) > 0: score += self.punish(50*len(remains) + slowPow(9, len(remains)), '%s extraneous symbols' % len(remains)) # Use an appropriate function to map [0, +inf) to [0, 100) percentage = 100 * (1 + math.tanh((score - 400.0) / 400.0)) * \ (1 - 1 / (1 + score / 5.0)) / 2 # if it's above 99.9%, show as many digits as is interesting score_string=re.sub('(99\\.9*\\d|\\.\\d).*','\\1',repr(percentage)) irc.reply(_('The "lame nick-o-meter" reading for "%s" is %s%%.') % (originalNick, score_string)) self.log.debug('Calculated lameness score for %s as %s ' '(raw score was %s)', originalNick, score_string, score) nickometer = wrap(nickometer, [additional('text')]) Class = Nickometer # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Nickometer/test.py������������������������������������������������������0000644�0001750�0001750�00000004207�14535072470�017235� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, aafshar@gmail.com # Copyright (c) 2010-2021, 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 * class NickometerTestCase(PluginTestCase): plugins = ('Nickometer',) def testNickometer(self): self.assertNotError('nickometer') self.assertResponse( 'nickometer jemfinch', 'The "lame nick-o-meter" reading for "jemfinch" is 0.0%.') nick = 'xXReallyObnoxious1337NickXx' self.assertResponse( 'nickometer %s' % nick, 'The "lame nick-o-meter" reading for "%s" is 99.96%%.' % nick) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/�������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014510� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/__init__.py��������������������������������������������������������0000644�0001750�0001750�00000005125�14535072470�016622� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ A complete messaging system that allows users to leave 'notes' for other users that can be retrieved later. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = { supybot.authors.inkedmn: ['Original implementation.'] } from . import config from . import plugin from importlib 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: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/config.py����������������������������������������������������������0000644�0001750�0001750�00000006625�14535072470�016336� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Note') 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('Note', True) Note = conf.registerPlugin('Note') conf.registerGroup(Note, 'notify') conf.registerGlobalValue(Note.notify, 'onJoin', registry.Boolean(False, """Determines whether the bot will notify people of their new messages when they join the channel. Normally it will notify them when they send a message to the channel, since oftentimes joins are the result of netsplits and not the actual presence of the user.""")) conf.registerGlobalValue(Note.notify.onJoin, 'repeatedly', registry.Boolean(False, """Determines whether the bot will repeatedly notify people of their new messages when they join the channel. That means when they join the channel, the bot will tell them they have unread messages, even if it's told them before.""")) conf.registerGlobalValue(Note.notify, 'autoSend', registry.NonNegativeInteger(0, """Determines the upper limit for automatically sending messages instead of notifications. I.e., if this value is 2 and there are 2 new messages to notify a user about, instead of sending a notification message, the bot will simply send those new messages. If there are 3 new messages, however, the bot will send a notification message.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/locales/�����������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016132� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/locales/fi.po������������������������������������������������������0000644�0001750�0001750�00000007701�14535072470�017073� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Note plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 14:13+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" #: plugin.py:126 msgid "Allows you to send notes to other users." msgstr "Sallii muistiinpanojen lähettämisen toisille käyttäjille." #: plugin.py:184 msgid "" ",[,[...]] \n" "\n" " Sends a new note to the user specified. Multiple recipients may be\n" " specified by separating their names by commas.\n" " " msgstr "" ",[,[...]] \n" "\n" " Lähettää uuden muistiinpanon määritetylle käyttäjälle. Monia " "vastaanottajia voidaan\n" " määrittää erottamalla nimet pilkuilla.\n" " " #: plugin.py:200 msgid "" " \n" "\n" " Sends a note in reply to .\n" " " msgstr "" " \n" "\n" " Lähettää muistiinpanon vastaukseksi .\n" " " #: plugin.py:224 msgid "" "\n" "\n" " Unsends the note with the id given. You must be the\n" " author of the note, and it must be unread.\n" " " msgstr "" "\n" "\n" " Kumoaa muistiinpanon lähetyksen. Sinun täytyy olla\n" " muistiinpanon tekijä, ja sen täytyy olla lukematon.\n" " " #: plugin.py:256 msgid "" "\n" "\n" " Retrieves a single note by its unique note id. Use the 'note list'\n" " command to see what unread notes you have.\n" " " msgstr "" "\n" "\n" " Hakee muistiinpanon sen ainutlaatuisella muistiinpano id:llä. Käytä " "'note list'\n" " komentoa nähdäksesi mitä lukemattomia muistiinpanoja sinulla on.\n" " " #: plugin.py:286 msgid "" "[--{regexp} ] [--sent] []\n" "\n" " Searches your received notes for ones matching . If --regexp " "is\n" " given, its associated value is taken as a regexp and matched " "against\n" " the notes. If --sent is specified, only search sent notes.\n" " " msgstr "" "[--{regexp} ] [--sent] []\n" "\n" " Etsii vastaanotetuista muistiinpanojasi niitä muistiinpanoja, jotka " "täsmäävät . Jos --regexp on\n" " annettu, sen liitetty arvo on otettu säännöllisestä lausekkeesta ja " "täsmätty muistiinpanoihin.\n" " the notes. Jos --sent on määritetty, hakee vain lähetetyistä " "muistiinpanoista .\n" " " #: plugin.py:328 msgid "" "[--{old,sent}] [--{from,to} ]\n" "\n" " Retrieves the ids of all your unread notes. If --old is given, " "list\n" " read notes. If --sent is given, list notes that you have sent. If\n" " --from is specified, only lists notes sent to you from . If\n" " --to is specified, only lists notes sent by you to .\n" " " msgstr "" "[--{old,sent}] [--{from,to} ]\n" "\n" " Hakee kaikki lukemattomat muistiinpanosi. Jos --old on annettu, " "luettelee\n" " luetut muistiinpanot. Jos --sent on annettu, luettelee " "muistiinpanot, jotka olet lähettänyt. Jos\n" " --from on määritetty, luettelee vain muistiinpanot, jotka " "on lähettänyt sinulle. Jos\n" " --to on määritetty, luettelee vain muistiinpanot, jotka ovat " "lähettänyt .\n" " " #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Retrieves your next unread note, if any.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Hakee lukemattomat muistiinpanosi, jos yhtään.\n" " " ���������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/locales/fr.po������������������������������������������������������0000644�0001750�0001750�00000006575�14535072470�017114� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:126 msgid "Allows you to send notes to other users." msgstr "" #: plugin.py:184 msgid "" ",[,[...]] \n" "\n" " Sends a new note to the user specified. Multiple recipients may be\n" " specified by separating their names by commas.\n" " " msgstr "" ",[,...] \n" "\n" "Envoie la nouvelle note aux utilisateurs spécifiés. De multiples " "destainaires peuvent être spécifiés en séparant leur noms par des virgules." #: plugin.py:200 msgid "" " \n" "\n" " Sends a note in reply to .\n" " " msgstr "" " \n" "\n" "envoie une note en réponse à celle ." #: plugin.py:224 msgid "" "\n" "\n" " Unsends the note with the id given. You must be the\n" " author of the note, and it must be unread.\n" " " msgstr "" "\n" "\n" "Désenvoie la note d' donné. Vous devez être l'auteur de la note, et elle " "ne doit pas être lue." #: plugin.py:256 msgid "" "\n" "\n" " Retrieves a single note by its unique note id. Use the 'note list'\n" " command to see what unread notes you have.\n" " " msgstr "" "\n" "\n" "Récupère une seule note par son unique. Utilisez la commande 'note " "list' pour voir combien de notes non lues vous avez." #: plugin.py:286 msgid "" "[--{regexp} ] [--sent] []\n" "\n" " Searches your received notes for ones matching . If --regexp " "is\n" " given, its associated value is taken as a regexp and matched " "against\n" " the notes. If --sent is specified, only search sent notes.\n" " " msgstr "" "[--{regexp} ] [--sent] []\n" "\n" "Recherche les notes correspondant au dans vos notes reçues. Si --" "regexp est donné, sa valeur associée est utilisé comme expression régulière " "et est à nouveau recherchée dans les notes. Si --sent est donné, ne " "recherche que dans les notes envoyées." #: plugin.py:328 msgid "" "[--{old,sent}] [--{from,to} ]\n" "\n" " Retrieves the ids of all your unread notes. If --old is given, " "list\n" " read notes. If --sent is given, list notes that you have sent. If\n" " --from is specified, only lists notes sent to you from . If\n" " --to is specified, only lists notes sent by you to .\n" " " msgstr "" "[--{old,sent}] [--{from,to} ]\n" "\n" "Récupère les id de toutes vos news non lues. Si --old est donné, liste les " "news lues. Si --sent est donné, liste les news que vous avez envoyées. Si --" "from est spécifié, liste seulement les news envoyées par l'. Si " "--to est spécifié, liste seulement les news envoyées par l'." #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Retrieves your next unread note, if any.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Récupère votre note non lue suivant, s'il y en a une." �����������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/locales/it.po������������������������������������������������������0000644�0001750�0001750�00000006507�14535072470�017114� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-15 13:13+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:126 msgid "Allows you to send notes to other users." msgstr "" #: plugin.py:184 msgid "" ",[,[...]] \n" "\n" " Sends a new note to the user specified. Multiple recipients may be\n" " specified by separating their names by commas.\n" " " msgstr "" ",[,[...]] \n" "\n" " Invia una nuova nota all'utente specificato. Possono essere " "specificati\n" " destinatari multipli separando i nomi con una virgola.\n" " " #: plugin.py:200 msgid "" " \n" "\n" " Sends a note in reply to .\n" " " msgstr "" " \n" "\n" " Invia una nota in risposta a .\n" " " #: plugin.py:224 msgid "" "\n" "\n" " Unsends the note with the id given. You must be the\n" " author of the note, and it must be unread.\n" " " msgstr "" "\n" "\n" " Annulla l'invio di una nota tramite l'id specificato.\n" " È necessario esserne l'autore e non deve essere stata letta.\n" " " #: plugin.py:256 msgid "" "\n" "\n" " Retrieves a single note by its unique note id. Use the 'note list'\n" " command to see what unread notes you have.\n" " " msgstr "" "\n" "\n" " Recupera una singola nota tramite il suo id univoco. Per sapere " "quali\n" " note hai da leggere, utilizza il comando \"note list\".\n" " " #: plugin.py:286 msgid "" "[--{regexp} ] [--sent] []\n" "\n" " Searches your received notes for ones matching . If --regexp " "is\n" " given, its associated value is taken as a regexp and matched " "against\n" " the notes. If --sent is specified, only search sent notes.\n" " " msgstr "" "[--{regexp} ] [--sent] []\n" "\n" " Cerca le note ricevute che corrispondono a . Se --regexp è\n" " usato, i valori ad essa associati sono presi come regexp e " "confrontati\n" " con le note. Se si specifica --sent, verranno cercate solo le note " "spedite.\n" " " #: plugin.py:328 msgid "" "[--{old,sent}] [--{from,to} ]\n" "\n" " Retrieves the ids of all your unread notes. If --old is given, " "list\n" " read notes. If --sent is given, list notes that you have sent. If\n" " --from is specified, only lists notes sent to you from . If\n" " --to is specified, only lists notes sent by you to .\n" " " msgstr "" "[--{old,sent}] [--{from,to} ]\n" "\n" " Recupera gli id di tutte le note non lette. Se --old è specificato,\n" " elenca quelle lette; --sent elenca quelle spedite; --from solo " "quelle\n" " inviate da ; mentre --to elenca le note inviate a .\n" " " #: plugin.py:369 msgid "" "takes no arguments\n" "\n" " Retrieves your next unread note, if any.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Recupera la successiva nota non letta, se presente.\n" " " �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/plugin.py����������������������������������������������������������0000644�0001750�0001750�00000040155�14535072470�016363� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Brett Kelly # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import time import operator import supybot.dbi as dbi import supybot.log as log import supybot.conf as conf import supybot.utils as utils import supybot.ircdb as ircdb 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 import commands from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Note') class NoteRecord(dbi.Record): __fields__ = [ 'frm', 'to', 'at', 'notified', 'read', 'public', 'text', ] class DbiNoteDB(dbi.DB): Mapping = 'flat' Record = NoteRecord def __init__(self, *args, **kwargs): dbi.DB.__init__(self, *args, **kwargs) self.unRead = {} self.unNotified = {} for record in self: self._addCache(record) def _addCache(self, record): if not record.read: self.unRead.setdefault(record.to, []).append(record.id) if not record.notified: self.unNotified.setdefault(record.to, []).append(record.id) def _removeCache(self, record): if record.notified: try: self.unNotified[record.to].remove(record.id) except (KeyError, ValueError): pass if record.read: try: self.unRead[record.to].remove(record.id) except (KeyError, ValueError): pass def setRead(self, id): n = self.get(id) n.read = True n.notified = True self._removeCache(n) self.set(id, n) def setNotified(self, id): n = self.get(id) n.notified = True self._removeCache(n) self.set(id, n) def getUnnotifiedIds(self, to): return self.unNotified.get(to, []) def getUnreadIds(self, to): return self.unRead.get(to, []) def send(self, frm, to, public, text): n = self.Record(frm=frm, to=to, text=text, at=time.time(), public=public) id = self.add(n) self._addCache(n) return id def unsend(self, id): self.remove(id) for cache in self.unRead, self.unNotified: for (to, ids) in cache.items(): while id in ids: ids.remove(id) NoteDB = plugins.DB('Note', {'flat': DbiNoteDB}) class Note(callbacks.Plugin): """Allows you to send notes to other users.""" def __init__(self, irc): self.__parent= super(Note, self) self.__parent.__init__(irc) self.db = NoteDB() def die(self): self.__parent.die() self.db.close() def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return self._notify(irc, msg) def doJoin(self, irc, msg): if self.registryValue('notify.onJoin'): repeatedly = self.registryValue('notify.onJoin.repeatedly') self._notify(irc, msg, repeatedly) def _notify(self, irc, msg, repeatedly=False): irc = callbacks.SimpleProxy(irc, msg) try: to = ircdb.users.getUserId(msg.prefix) except KeyError: return ids = self.db.getUnnotifiedIds(to) if len(ids) <= self.registryValue('notify.autoSend'): for id in ids: irc.reply(self._formatNote(self.db.get(id), to), private=True) self.db.setRead(id) return unnotifiedIds = ['#%s' % nid for nid in ids] unnotified = len(unnotifiedIds) if unnotified or repeatedly: unreadIds = ['#%s' % nid for nid in self.db.getUnreadIds(to)] unread = len(unreadIds) s = format('You have %n; %i that I haven\'t told you about ' 'before now. %L %b still unread.', (unread, 'unread', 'note'), unnotified, unreadIds, unread) # Later we'll have a user value for allowing this to be a NOTICE. irc.reply(s, private=True) for nid in unnotifiedIds: id = int(nid[1:]) self.db.setNotified(id) def _getUserId(self, irc, name): if ircdb.users.hasUser(name): return ircdb.users.getUserId(name) else: try: hostmask = irc.state.nickToHostmask(name) return ircdb.users.getUserId(hostmask) except KeyError: return None def send(self, irc, msg, args, user, targets, text): """,[,[...]] Sends a new note to the user specified. Multiple recipients may be specified by separating their names by commas. """ # Let's get the from user. public = bool(msg.channel) sent = [] for target in targets: id = self.db.send(user.id, target.id, public, text) s = format('note #%i sent to %s', id, target.name) sent.append(s) irc.reply(format('%L.', sent).capitalize()) send = wrap(send, ['user', commalist('otherUser'), 'text']) def reply(self, irc, msg, args, user, id, text): """ Sends a note in reply to . """ try: note = self.db.get(id) except dbi.NoRecordError: irc.error('That\'s not a note in my database.', Raise=True) if note.to != user.id: irc.error('You may only reply to notes ' 'that have been sent to you.', Raise=True) self.db.setRead(id) text += ' (in reply to #%s)' % id public = bool(msg.channel) try: target = ircdb.users.getUser(note.frm) except KeyError: irc.error('The user who sent you that note ' 'is no longer in my user database.', Raise=True) id = self.db.send(user.id, note.frm, public, text) irc.reply(format('Note #%i sent to %s.', id, target.name)) reply = wrap(reply, ['user', ('id', 'note'), 'text']) def unsend(self, irc, msg, args, user, id): """ Unsends the note with the id given. You must be the author of the note, and it must be unread. """ try: note = self.db.get(id) except dbi.NoRecordError: irc.errorInvalid('note id') if note.frm == user.id: if not note.read: self.db.unsend(id) irc.replySuccess() else: irc.error('That note has been read already.') else: irc.error('That note wasn\'t sent by you.') unsend = wrap(unsend, ['user', ('id', 'note')]) def _formatNote(self, note, to): elapsed = utils.timeElapsed(time.time() - note.at) if note.to == to: author = plugins.getUserName(note.frm) return format('#%i: %s (Sent by %s %s ago)', note.id, note.text, author, elapsed) else: assert note.frm == to, 'Odd, userid isn\'t frm either.' recipient = plugins.getUserName(note.to) return format('#%i: %s (Sent to %s %s ago)', note.id, note.text, recipient, elapsed) def note(self, irc, msg, args, user, id): """ Retrieves a single note by its unique note id. Use the 'note list' command to see what unread notes you have. """ try: note = self.db.get(id) except dbi.NoRecordError: irc.errorInvalid('note id') if user.id != note.frm and user.id != note.to: s = 'You may only retrieve notes you\'ve sent or received.' irc.error(s) return newnote = self._formatNote(note, user.id) irc.reply(newnote, private=(not note.public)) self.db.setRead(id) note = wrap(note, ['user', ('id', 'note')]) def _formatNoteId(self, irc, msg, note, sent=False): if note.public or not msg.channel: if sent: sender = plugins.getUserName(note.to) return format('#%i to %s', note.id, sender) else: sender = plugins.getUserName(note.frm) return format('#%i from %s', note.id, sender) else: return format('#%i (private)', note.id) def search(self, irc, msg, args, user, optlist, glob): """[--{regexp} ] [--sent] [] Searches your received notes for ones matching . If --regexp is given, its associated value is taken as a regexp and matched against the notes. If --sent is specified, only search sent notes. """ criteria = [] def to(note): return note.to == user.id def frm(note): return note.frm == user.id own = to 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')) elif option == 'sent': own = frm if glob: glob = utils.python.glob2re(glob) criteria.append(re.compile(glob).search) def match(note): for p in criteria: if not p(note.text): return False return True notes = list(self.db.select(lambda n: match(n) and own(n))) if not notes: irc.reply('No matching notes were found.') else: utils.sortBy(operator.attrgetter('id'), notes) ids = [self._formatNoteId(irc, msg, note) for note in notes] ids = self._condense(ids) irc.reply(format('%L', ids)) search = wrap(search, ['user', getopts({'regexp': ('regexpMatcher', True), 'sent': ''}), additional('glob')]) def list(self, irc, msg, args, user, optlist): """[--{old,sent}] [--{from,to} ] Retrieves the ids of all your unread notes. If --old is given, list read notes. If --sent is given, list notes that you have sent. If --from is specified, only lists notes sent to you from . If --to is specified, only lists notes sent by you to . """ (sender, receiver, old, sent) = (None, None, False, False) for (option, arg) in optlist: if option == 'old': old = True if option == 'sent': sent = True if option == 'from': sender = arg if option == 'to': receiver = arg sent = True if old: return self._oldnotes(irc, msg, sender) if sent: return self._sentnotes(irc, msg, receiver) def p(note): return not note.read and note.to == user.id if sender: originalP = p def p(note): return originalP(note) and note.frm == sender.id notes = list(self.db.select(p)) if not notes: irc.reply('You have no unread notes.') else: utils.sortBy(operator.attrgetter('id'), notes) ids = [self._formatNoteId(irc, msg, note) for note in notes] ids = self._condense(ids) irc.reply(format('%L.', ids)) list = wrap(list, ['user', getopts({'old': '', 'sent': '', 'from': 'otherUser', 'to': 'otherUser'})]) def next(self, irc, msg, args, user): """takes no arguments Retrieves your next unread note, if any. """ notes = self.db.getUnreadIds(user.id) if not notes: irc.reply('You have no unread notes.') else: found = False for id in notes: try: note = self.db.get(id) except KeyError: continue found = True break if not found: irc.reply('You have no unread notes.') else: irc.reply(self._formatNote(note, user.id), private=(not note.public)) self.db.setRead(note.id) next = wrap(next, ['user']) def _condense(self, notes): temp = {} for note in notes: note = note.split(' ', 1) if note[1] in temp: temp[note[1]].append(note[0]) else: temp[note[1]] = [note[0]] notes = [] for (k,v) in temp.items(): if '(private)' in k: k = k.replace('(private)', format('%b private', len(v))) notes.append(format('%L %s', v, k)) return notes def _sentnotes(self, irc, msg, receiver): try: user = ircdb.users.getUser(msg.prefix) except KeyError: irc.errorNotRegistered() return def p(note): return note.frm == user.id if receiver: originalP = p def p(note): return originalP(note) and note.to == receiver.id notes = list(self.db.select(p)) if not notes: irc.error('I couldn\'t find any sent notes for your user.') else: utils.sortBy(operator.attrgetter('id'), notes) notes.reverse() # Most recently sent first. ids = [self._formatNoteId(irc, msg, note, sent=True) for note in notes] ids = self._condense(ids) irc.reply(format('%L.', ids)) def _oldnotes(self, irc, msg, sender): try: user = ircdb.users.getUser(msg.prefix) except KeyError: irc.errorNotRegistered() return def p(note): return note.to == user.id and note.read if sender: originalP = p def p(note): return originalP(note) and note.frm == sender.id notes = list(self.db.select(p)) if not notes: irc.reply('I couldn\'t find any matching read notes ' 'for your user.') else: utils.sortBy(operator.attrgetter('id'), notes) notes.reverse() ids = [self._formatNoteId(irc, msg, note) for note in notes] ids = self._condense(ids) irc.reply(format('%L.', ids)) Class = Note # vim: shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Note/test.py������������������������������������������������������������0000644�0001750�0001750�00000007710�14535072470�016044� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2003, Brett Kelly # Copyright (c) 2010-2021, 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 * class NoteTestCase(PluginTestCase): plugins = ('Note', 'Misc', 'User') config = {'supybot.reply.whenNotCommand': False} def setUp(self): PluginTestCase.setUp(self) # setup a user self.prefix = 'foo!bar@baz' self.assertNotError('register inkedmn bar') self.assertNotError('hostmask add inkedmn test2!bar@baz') def testSendnote(self): self.assertRegexp('note send inkedmn test', '#1') # have to getMsg(' ') after each Note.send to absorb supybot's # automatic "You have an unread note" message _ = self.getMsg(' ') self.assertError('note send alsdkjfasldk foo') self.assertNotError('note send inkedmn test2') _ = self.getMsg(' ') # verify that sending a note to a user via their nick instead of their # ircdb user name works self.prefix = 'test2!bar@baz' self.assertNotError('note send test2 foo') _ = self.getMsg(' ') def testNote(self): self.assertNotError('note send inkedmn test') _ = self.getMsg(' ') self.assertRegexp('note 1', 'test') self.assertError('note blah') def testList(self): self.assertResponse('note list', 'You have no unread notes.') self.assertNotError('note send inkedmn testing') _ = self.getMsg(' ') self.assertNotError('note send inkedmn 1,2,3') _ = self.getMsg(' ') self.assertRegexp('note list --sent', r'#2.*#1') self.assertRegexp('note list --sent --to inkedmn', r'#2.*#1') self.assertRegexp('note list', r'#1.*#2') self.assertRegexp('note 1', 'testing') self.assertRegexp('note list --old', '#1 from inkedmn') self.assertRegexp('note list --old --from inkedmn','#1 from inkedmn') def testSearch(self): self.assertNotError('note send inkedmn testing') _ = self.getMsg(' ') self.assertNotError('note send inkedmn 1,2,3') _ = self.getMsg(' ') self.assertRegexp('note search test', r'#1') self.assertRegexp('note search --regexp m/1,2/', r'#2') self.assertRegexp('note search --sent test', r'#1') def testNext(self): self.assertNotError('note send inkedmn testing') _ = self.getMsg(' ') self.assertRegexp('note next', 'testing') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014675� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/__init__.py�������������������������������������������������������0000644�0001750�0001750�00000004573�14535072470�017015� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Provides commands useful to the owner of the bot; the commands here require their caller to have the 'owner' capability. 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib import reload reload(plugin) # In case we're being reloaded. if world.testing: from . import test Class = plugin.Class configure = config.configure �������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/config.py���������������������������������������������������������0000644�0001750�0001750�00000006264�14535072470�016522� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Owner') 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('Owner', True) Owner = conf.registerPlugin('Owner', True) conf.registerGlobalValue(Owner, 'public', registry.Boolean(True, """Determines whether this plugin is publicly visible.""")) conf.registerGlobalValue(Owner, 'announceFormat', registry.String('Announcement from my owner ($owner): $text', """Determines the format of messages sent by the 'announce' command. $owner may be used for the username of the owner calling this command, and $text for the announcement being made.""")) conf.registerGlobalValue(Owner, 'quitMsg', registry.String('Limnoria $version', """Determines what quit message will be used by default. If the quit command is called without a quit message, this will be used. If this value is empty, the nick of the person giving the quit command will be used. The standard substitutions ($version, $nick, etc.) are all handled appropriately.""")) conf.registerGroup(conf.supybot.commands, 'renames', orderAlphabetically=True) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/locales/����������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016317� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/locales/de.po�����������������������������������������������������0000644�0001750�0001750�00000021024�14535072470�017244� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:38+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" #: plugin.py:85 msgid "" "Owner-only commands for core Supybot. This is a core Supybot module\n" " that should not be removed!" msgstr "" #: plugin.py:319 msgid "" "If the given message is a command, triggers Limnoria's\n" " command-dispatching for that command.\n" "\n" " Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can\n" " potentially be an artificial message synthesized in doBatch\n" " from a multiline batch.\n" "\n" " Usually, a command is a single message, so ``payload=msg." "params[0]``\n" " However, when ``msg`` is part of a multiline message, the payload\n" " is the concatenation of multiple messages.\n" " See .\n" " " msgstr "" #: plugin.py:372 msgid "" "\n" "\n" " Logs to the global Supybot log at critical priority. Useful " "for\n" " marking logfiles for later searching.\n" " " msgstr "" "\n" "\n" "Schreibt in die globale Supybot Logdatei mit kritischer Priorität. " "Nützlich um Logdateien zu markieren um sie später zu durchsuchen." #: plugin.py:382 msgid "" "\n" "\n" " Sends to all channels the bot is currently on and not\n" " lobotomized in.\n" " " msgstr "" "\n" "\n" "Sendet an alle Kanäle in denen der Bot momentan ist und nicht " "hirnamputiert ist." #: plugin.py:403 msgid "" "[--remove] []\n" "\n" " Sets the default plugin for to . If --remove is\n" " given, removes the current default plugin for . If no " "plugin\n" " is given, returns the current default plugin set for . " "See\n" " also, supybot.commands.defaultPlugins.importantPlugins.\n" " " msgstr "" "[--remove] []\n" "\n" "Setzt das Standardplugin für auf . Falls --remove angegeben " "wird, wird das momentane Standardplugin für entfernt. Falls kein " "Plugin angegeben wird, wird das momentane Standardplugin für " "ausgegeben. Schau auch nach supybot.commands.defaultPlugins." "importantPlugins. " #: plugin.py:441 msgid "" "\n" "\n" " Sends the raw string given to the server.\n" " " msgstr "" "\n" "\n" "Sendet die Zeichenketten zum angegeben Server." #: plugin.py:455 #, fuzzy msgid "" "[]\n" "\n" " Exits the bot with the QUIT message . If is not " "given,\n" " the default quit message (supybot.plugins.Owner.quitMsg) will be " "used.\n" " If there is no default quitMsg set, your nick will be used. The " "standard\n" " substitutions ($version, $nick, etc.) are all handled " "appropriately.\n" " " msgstr "" "[]\n" "\n" "Beendet den Bot mit der Nachricht . Falls nicht angegeben wird, " "wird die Standard Endnachricht (supybot.plugins.Owner.quitMsg) benutzt. Wenn " "es keine standard quitMsg gesetzt ist, wird dein Nickname benutzt." #: plugin.py:473 msgid "" "takes no arguments\n" "\n" " Runs all the periodic flushers in world.flushers. This includes\n" " flushing all logs and all configuration changes to disk.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Führt alle periodischen Flusher in world.flushers aus. Das beinhaltet alle " "Log und Konfigurationsänderung auf die Festplatte zu schreiben." #: plugin.py:483 msgid "" "[]\n" "\n" " Runs the standard upkeep stuff (flushes and gc.collects()). If " "given\n" " a level, runs that level of upkeep (currently, the only supported\n" " level is \"high\", which causes the bot to flush a lot of caches as " "well\n" " as do normal upkeep stuff).\n" " " msgstr "" "[]\n" "\n" "Führt standard Instandhaltungen durch (flushes und gc.collects()). Falls " "eine Stufe angegeben wird, wird diese Stufe der Instandhaltungausgeführt " "(momentan ist die einzig unsterstütze Stufe \"high\", welche den Bot dazu " "veranlässt viele Caches aufzuräumen und normale Instandhaltung zu " "bestreiben)." #: plugin.py:529 msgid "" "[--deprecated] \n" "\n" " Loads the plugin from any of the directories in\n" " conf.supybot.directories.plugins; usually this includes the main\n" " installed directory and 'plugins' in the current directory.\n" " --deprecated is necessary if you wish to load deprecated plugins.\n" " " msgstr "" "[--deprecated] \n" "\n" "Läd das Plugin von irgendeinem Verzeichnis das in conf.supybot." "directories.plugins ist; normalerweise enthält diese das " "Hauptinstallationsverzeichnis und 'plugins' im momentanen Verzeichnis. --" "deprecated ist nötig falls du veraltete Plugins laden möchtest." #: plugin.py:567 msgid "" "\n" "\n" " Unloads and subsequently reloads the plugin by name; use the 'list'\n" " command to see a list of the currently loaded plugins.\n" " " msgstr "" "\n" "\n" "Entläd und läd das das Plugin neu; benutze den Befehl 'list' um dir die " "momentan geladenen Plugins anzuzeigen zu lassen." #: plugin.py:602 #, fuzzy msgid "" "\n" "\n" " Unloads the callback by name; use the 'list' command to see a list\n" " of the currently loaded plugins. Obviously, the Owner plugin can't\n" " be unloaded.\n" " " msgstr "" "\n" "\n" "Entläd den Callback nach Name; benutze den 'list' Befehl um zu sehen welche " "Callbacks momentan geladen sind. Es ist wohl klar, dass das Owner Plugin " "nicht entladen werden kann." #: plugin.py:631 msgid "" "{add|remove} \n" "\n" " Adds or removes (according to the first argument) from " "the\n" " default capabilities given to users (the configuration variable\n" " supybot.capabilities stores these).\n" " " msgstr "" "{add|remove} \n" "\n" "Hinzufügen oder entfernen (abhängig vom ersten Argument) der von " "den Standardfähigkeiten die den Benutzern gegeben werden (die " "Konfigurationsvariable supybot.capabilities speichert diese)." #: plugin.py:656 msgid "" "[] \n" "\n" " Disables the command for all users (including the " "owners).\n" " If is given, only disables the from . " "If\n" " you want to disable a command for most users but not for yourself, " "set\n" " a default capability of -plugin.command or -command (if you want to\n" " disable the command in all plugins).\n" " " msgstr "" "[] \n" "\n" "Deaktiviert den Befehl für alle Nutzer (auch den Besitzer). Falls " " von deaktiviert. Dieser " "Befehl ist das gegenteil von disable.Falls du den Befehl nur für die meisten " "Nutzer deaktiveren willst setze eine standard Fähigkeit -plugin.befehl oder -" "befehl (falls du den Befehl von allen Plugins deaktivieren willst)." #: plugin.py:683 msgid "" "[] \n" "\n" " Enables the command for all users. If \n" " if given, only enables the from . This command " "is\n" " the inverse of disable.\n" " " msgstr "" "[] \n" "\n" "Aktiviert den Befehl für alle Nutzer. Falls von aktiviert. Dieser Befehl ist das gegenteil von " "disable." #: plugin.py:702 msgid "" " \n" "\n" " Renames in to the .\n" " " msgstr "" " \n" "\n" "Benennt von in um." #: plugin.py:719 msgid "" "\n" "\n" " Removes all renames in . The plugin will be reloaded after\n" " this command is run.\n" " " msgstr "" "\n" "\n" "Entfernt alle Namensänderungen in . Das Plugin wird neu geladen nach " "diesem Befehl." #: plugin.py:732 msgid "" "takes no argument\n" "\n" " Reloads the locale of the bot." msgstr "" "hat kein Argument\n" "\n" "Läd die Lokalisations des Bots neu." ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/locales/fi.po�����������������������������������������������������0000644�0001750�0001750�00000012740�14535072470�017257� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Owner plugin in Limnoria. # Copyright (C) 2011 Limnoria # FIRST AUTHOR , YEAR. # WARNING: Translations of Owner plugin will not work, because it's loaded directly by supybot # and translations are more like a hack to it. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-15 08:28+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: plugin.py:85 msgid "" "Owner-only commands for core Supybot. This is a core Supybot module\n" " that should not be removed!" msgstr "" #: plugin.py:319 msgid "" "If the given message is a command, triggers Limnoria's\n" " command-dispatching for that command.\n" "\n" " Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can\n" " potentially be an artificial message synthesized in doBatch\n" " from a multiline batch.\n" "\n" " Usually, a command is a single message, so ``payload=msg." "params[0]``\n" " However, when ``msg`` is part of a multiline message, the payload\n" " is the concatenation of multiple messages.\n" " See .\n" " " msgstr "" #: plugin.py:372 msgid "" "\n" "\n" " Logs to the global Supybot log at critical priority. Useful " "for\n" " marking logfiles for later searching.\n" " " msgstr "" #: plugin.py:382 msgid "" "\n" "\n" " Sends to all channels the bot is currently on and not\n" " lobotomized in.\n" " " msgstr "" #: plugin.py:403 msgid "" "[--remove] []\n" "\n" " Sets the default plugin for to . If --remove is\n" " given, removes the current default plugin for . If no " "plugin\n" " is given, returns the current default plugin set for . " "See\n" " also, supybot.commands.defaultPlugins.importantPlugins.\n" " " msgstr "" #: plugin.py:441 msgid "" "\n" "\n" " Sends the raw string given to the server.\n" " " msgstr "" #: plugin.py:455 msgid "" "[]\n" "\n" " Exits the bot with the QUIT message . If is not " "given,\n" " the default quit message (supybot.plugins.Owner.quitMsg) will be " "used.\n" " If there is no default quitMsg set, your nick will be used. The " "standard\n" " substitutions ($version, $nick, etc.) are all handled " "appropriately.\n" " " msgstr "" #: plugin.py:473 msgid "" "takes no arguments\n" "\n" " Runs all the periodic flushers in world.flushers. This includes\n" " flushing all logs and all configuration changes to disk.\n" " " msgstr "" #: plugin.py:483 msgid "" "[]\n" "\n" " Runs the standard upkeep stuff (flushes and gc.collects()). If " "given\n" " a level, runs that level of upkeep (currently, the only supported\n" " level is \"high\", which causes the bot to flush a lot of caches as " "well\n" " as do normal upkeep stuff).\n" " " msgstr "" #: plugin.py:529 msgid "" "[--deprecated] \n" "\n" " Loads the plugin from any of the directories in\n" " conf.supybot.directories.plugins; usually this includes the main\n" " installed directory and 'plugins' in the current directory.\n" " --deprecated is necessary if you wish to load deprecated plugins.\n" " " msgstr "" #: plugin.py:567 msgid "" "\n" "\n" " Unloads and subsequently reloads the plugin by name; use the 'list'\n" " command to see a list of the currently loaded plugins.\n" " " msgstr "" #: plugin.py:602 msgid "" "\n" "\n" " Unloads the callback by name; use the 'list' command to see a list\n" " of the currently loaded plugins. Obviously, the Owner plugin can't\n" " be unloaded.\n" " " msgstr "" #: plugin.py:631 msgid "" "{add|remove} \n" "\n" " Adds or removes (according to the first argument) from " "the\n" " default capabilities given to users (the configuration variable\n" " supybot.capabilities stores these).\n" " " msgstr "" #: plugin.py:656 msgid "" "[] \n" "\n" " Disables the command for all users (including the " "owners).\n" " If is given, only disables the from . " "If\n" " you want to disable a command for most users but not for yourself, " "set\n" " a default capability of -plugin.command or -command (if you want to\n" " disable the command in all plugins).\n" " " msgstr "" #: plugin.py:683 msgid "" "[] \n" "\n" " Enables the command for all users. If \n" " if given, only enables the from . This command " "is\n" " the inverse of disable.\n" " " msgstr "" #: plugin.py:702 msgid "" " \n" "\n" " Renames in to the .\n" " " msgstr "" #: plugin.py:719 msgid "" "\n" "\n" " Removes all renames in . The plugin will be reloaded after\n" " this command is run.\n" " " msgstr "" #: plugin.py:732 msgid "" "takes no argument\n" "\n" " Reloads the locale of the bot." msgstr "" ��������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/locales/fr.po�����������������������������������������������������0000644�0001750�0001750�00000020542�14535072470�017267� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:85 msgid "" "Owner-only commands for core Supybot. This is a core Supybot module\n" " that should not be removed!" msgstr "" #: plugin.py:319 msgid "" "If the given message is a command, triggers Limnoria's\n" " command-dispatching for that command.\n" "\n" " Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can\n" " potentially be an artificial message synthesized in doBatch\n" " from a multiline batch.\n" "\n" " Usually, a command is a single message, so ``payload=msg." "params[0]``\n" " However, when ``msg`` is part of a multiline message, the payload\n" " is the concatenation of multiple messages.\n" " See .\n" " " msgstr "" #: plugin.py:372 msgid "" "\n" "\n" " Logs to the global Supybot log at critical priority. Useful " "for\n" " marking logfiles for later searching.\n" " " msgstr "" "\n" "\n" "Log le aux logs globaux de Supybot avec une priorité critique. Utile " "pour marquer les fichiers de logs pour des recherches ultérieures." #: plugin.py:382 msgid "" "\n" "\n" " Sends to all channels the bot is currently on and not\n" " lobotomized in.\n" " " msgstr "" "\n" "\n" "Envoie le à tous les canaux sur lesquels le bot est sans être " "lobotomisé." #: plugin.py:403 msgid "" "[--remove] []\n" "\n" " Sets the default plugin for to . If --remove is\n" " given, removes the current default plugin for . If no " "plugin\n" " is given, returns the current default plugin set for . " "See\n" " also, supybot.commands.defaultPlugins.importantPlugins.\n" " " msgstr "" "[--remove] []\n" "\n" "Défini le par défaut de la . Si --remove est donné, le " "commande par défaut actuelle est supprimée. Si aucun plugin n'est donné, " "retourne le plugin par défaut actuel. Voyez supybot.commands.defaultPlugins." "importantPlugins pour plus d'informations." #: plugin.py:441 msgid "" "\n" "\n" " Sends the raw string given to the server.\n" " " msgstr "" "\n" "\n" "Envoie la chaîne directement au serveur." #: plugin.py:455 #, fuzzy msgid "" "[]\n" "\n" " Exits the bot with the QUIT message . If is not " "given,\n" " the default quit message (supybot.plugins.Owner.quitMsg) will be " "used.\n" " If there is no default quitMsg set, your nick will be used. The " "standard\n" " substitutions ($version, $nick, etc.) are all handled " "appropriately.\n" " " msgstr "" "[]\n" "\n" "Fait quitter le bot avec le message de quit . Si le n'est pas " "donné, le message de quit par défaut (supybot.plugins.Owner.quitMsg) est " "utilisé. Si quitMsg est vide, votre nick sera utilisé." #: plugin.py:473 msgid "" "takes no arguments\n" "\n" " Runs all the periodic flushers in world.flushers. This includes\n" " flushing all logs and all configuration changes to disk.\n" " " msgstr "" "ne prend pas d'argument.\n" "\n" "Lance tous les 'flushers' périodiques dans world.flushers. Ceci inclue " "l'écriture des logs et de la configuration sur le disque." #: plugin.py:483 msgid "" "[]\n" "\n" " Runs the standard upkeep stuff (flushes and gc.collects()). If " "given\n" " a level, runs that level of upkeep (currently, the only supported\n" " level is \"high\", which causes the bot to flush a lot of caches as " "well\n" " as do normal upkeep stuff).\n" " " msgstr "" "[]\n" "\n" "Lance le 'upkeep' standard (flushes et gc.collect()). Si un niveau est " "donné, lance le niveau d'upkeep (actuellement, le seul niveau supporté est " "\"high\", ce qui fait que le bot vide beaucoup plus de cache que ce qu'il " "fait normalement)." #: plugin.py:529 msgid "" "[--deprecated] \n" "\n" " Loads the plugin from any of the directories in\n" " conf.supybot.directories.plugins; usually this includes the main\n" " installed directory and 'plugins' in the current directory.\n" " --deprecated is necessary if you wish to load deprecated plugins.\n" " " msgstr "" "[--deprecated] \n" "\n" "Charge le de n'importe lequel des répertoires dans conf.supybot." "directories.plugins, ce qui inclue généralement le répertoire principal de " "l'installation, et 'plugins' dans le répertoire courrant. Utilisez --" "deprected si nécessaire pour charger des plugins dépréciés." #: plugin.py:567 msgid "" "\n" "\n" " Unloads and subsequently reloads the plugin by name; use the 'list'\n" " command to see a list of the currently loaded plugins.\n" " " msgstr "" "\n" "\n" "Décharger et recharge immédiatement le ; utilisez la commande " "'list' pour lister les plugins actuellement chargés." #: plugin.py:602 msgid "" "\n" "\n" " Unloads the callback by name; use the 'list' command to see a list\n" " of the currently loaded plugins. Obviously, the Owner plugin can't\n" " be unloaded.\n" " " msgstr "" "\n" "\n" "Décharge le ; utilisez la commande 'list' pour lister les plugins " "actuellement chargés. Évidemment, le plugin Owner ne peut être déchargé." #: plugin.py:631 msgid "" "{add|remove} \n" "\n" " Adds or removes (according to the first argument) from " "the\n" " default capabilities given to users (the configuration variable\n" " supybot.capabilities stores these).\n" " " msgstr "" "{add|remove} \n" "\n" "Ajoute ou supprime (en fonction du premier argument) la à la " "liste des capacités par défaut données aux utilisateurs (stockée dans la " "variable de configuration supybot.capabilities)." #: plugin.py:656 msgid "" "[] \n" "\n" " Disables the command for all users (including the " "owners).\n" " If is given, only disables the from . " "If\n" " you want to disable a command for most users but not for yourself, " "set\n" " a default capability of -plugin.command or -command (if you want to\n" " disable the command in all plugins).\n" " " msgstr "" "[] \n" "\n" "Désactive la pour tous les utilisateurs (y compris le " "propriétaire. Si le est donné, ne désactive la que pour " "le . Si vous voulez désactiver la commande pour tous les " "utilisateurs sauf vous-même, définissez la capacité par défaut -plugin." "command ou -command." #: plugin.py:683 msgid "" "[] \n" "\n" " Enables the command for all users. If \n" " if given, only enables the from . This command " "is\n" " the inverse of disable.\n" " " msgstr "" "[] \n" "\n" "Active la pour tous les utilisateurs. Si le est donné, " "ne réactive la que pour le . Cette commande est l'inverse " "de disable." #: plugin.py:702 msgid "" " \n" "\n" " Renames in to the .\n" " " msgstr "" " \n" "\n" "Renomme la du par un ." #: plugin.py:719 msgid "" "\n" "\n" " Removes all renames in . The plugin will be reloaded after\n" " this command is run.\n" " " msgstr "" "\n" "\n" "Supprime tous les renommages du . Ce plugin sera rechargé après que " "cette commande ait été lancée." #: plugin.py:732 msgid "" "takes no argument\n" "\n" " Reloads the locale of the bot." msgstr "" "ne prend pas d'argument\n" "\n" "Recharge la locale du bot." ��������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/locales/hu.po�����������������������������������������������������0000644�0001750�0001750�00000021516�14535072470�017276� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Owner\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:13+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: hu_HU\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:85 msgid "" "Owner-only commands for core Supybot. This is a core Supybot module\n" " that should not be removed!" msgstr "" #: plugin.py:319 msgid "" "If the given message is a command, triggers Limnoria's\n" " command-dispatching for that command.\n" "\n" " Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can\n" " potentially be an artificial message synthesized in doBatch\n" " from a multiline batch.\n" "\n" " Usually, a command is a single message, so ``payload=msg." "params[0]``\n" " However, when ``msg`` is part of a multiline message, the payload\n" " is the concatenation of multiple messages.\n" " See .\n" " " msgstr "" #: plugin.py:372 msgid "" "\n" "\n" " Logs to the global Supybot log at critical priority. Useful " "for\n" " marking logfiles for later searching.\n" " " msgstr "" "\n" "\n" "Naplózza -et a globális Supybot naplóba kritikus prioritással. " "Hasznos a naplófájlok megjelölésére későbbi kereséshez." #: plugin.py:382 msgid "" "\n" "\n" " Sends to all channels the bot is currently on and not\n" " lobotomized in.\n" " " msgstr "" "\n" "\n" "Elküldi -et minden csatornára, ahol a bot van és nincs némítva." #: plugin.py:403 msgid "" "[--remove] []\n" "\n" " Sets the default plugin for to . If --remove is\n" " given, removes the current default plugin for . If no " "plugin\n" " is given, returns the current default plugin set for . " "See\n" " also, supybot.commands.defaultPlugins.importantPlugins.\n" " " msgstr "" "[--remove] []\n" " alapértelmezett bővítményét -re állítja. Ha --remove " "meg van adva, eltávolítja jelenlegi alapértelmezett bővítményét. " "Ha nincs bővítmény megadva, kiírja jelenlegi alapértelmezett " "bővítményét. Lásd még, supybot.commands.defaultPlugins.importantPlugins." #: plugin.py:441 msgid "" "\n" "\n" " Sends the raw string given to the server.\n" " " msgstr "" "\n" "\n" "Elküldi a megadott nyers karakterláncot a szervernek." #: plugin.py:455 #, fuzzy msgid "" "[]\n" "\n" " Exits the bot with the QUIT message . If is not " "given,\n" " the default quit message (supybot.plugins.Owner.quitMsg) will be " "used.\n" " If there is no default quitMsg set, your nick will be used. The " "standard\n" " substitutions ($version, $nick, etc.) are all handled " "appropriately.\n" " " msgstr "" "[]\n" "\n" "Kilép a botból kilépési üzenettel. Ha nincs meg adva, az " "alapértelmezett kilépési üzenet (supybot.plugins.Owner.quitMsg) lesz " "használva. Ha nincs alapértelmezett kilépési üzenet beállítva, a neved lesz " "használva." #: plugin.py:473 msgid "" "takes no arguments\n" "\n" " Runs all the periodic flushers in world.flushers. This includes\n" " flushing all logs and all configuration changes to disk.\n" " " msgstr "" "paraméter nélküli\n" "\n" "Futtatja az összes időszakos öblítőket world.flushers-ban. Ez magában " "foglalja az összes naplófájl és konfigurációs fájl mentését a lemezre." #: plugin.py:483 msgid "" "[]\n" "\n" " Runs the standard upkeep stuff (flushes and gc.collects()). If " "given\n" " a level, runs that level of upkeep (currently, the only supported\n" " level is \"high\", which causes the bot to flush a lot of caches as " "well\n" " as do normal upkeep stuff).\n" " " msgstr "" "[]\n" "\n" "Futtatja a szabványos karbantartási dolgokat (öblítés és gc.collects()). Ha " "meg van adva egy szint, a megadott szintű karbantartást futtatja (jelenleg, " "az egyetlen témogatott szint a \"high\", amellyel a bot öblít nagyon sok " "gyorsítótárat és megcsinálja a normális karbantartási dolgokat." #: plugin.py:529 msgid "" "[--deprecated] \n" "\n" " Loads the plugin from any of the directories in\n" " conf.supybot.directories.plugins; usually this includes the main\n" " installed directory and 'plugins' in the current directory.\n" " --deprecated is necessary if you wish to load deprecated plugins.\n" " " msgstr "" "[--deprecated] \n" "\n" "Betölti a bővítményt a supybot.directories.plugins könyvtárai " "közül bármelyikből; általában ebbe beletarzozik a fő telepítési könyvtár és " "egy 'plugins' a jelenlegi könyvtárban. --deprecated akkor szükséges, ha " "elavult bővítményeket szeretnél betölteni." #: plugin.py:567 msgid "" "\n" "\n" " Unloads and subsequently reloads the plugin by name; use the 'list'\n" " command to see a list of the currently loaded plugins.\n" " " msgstr "" "\n" "\n" "Kirakja és aztán újratölti a megadott bővítményt név szerint; használd a " "'list' parancsot, hogy lásd a jelenleg betöltött bővítményeket." #: plugin.py:602 #, fuzzy msgid "" "\n" "\n" " Unloads the callback by name; use the 'list' command to see a list\n" " of the currently loaded plugins. Obviously, the Owner plugin can't\n" " be unloaded.\n" " " msgstr "" "\n" "\n" "Kirakja a megadott bővítményt név szerint; használd a 'list' parancsot, hogy " "lásd a jelenleg betöltött bővítményeket. Nyilvánvalóan az Owner bővítményt " "nem lehet kirakni." #: plugin.py:631 msgid "" "{add|remove} \n" "\n" " Adds or removes (according to the first argument) from " "the\n" " default capabilities given to users (the configuration variable\n" " supybot.capabilities stores these).\n" " " msgstr "" "{add|remove} \n" "\n" "Hozzáadja vagy eltávolítja (az első paraméter szerint) -et a " "felhasználóknak adott alapértelmezett képességekhez/-ből (a supybot." "capabilities konfigurációs változó tárolja ezeket)." #: plugin.py:656 msgid "" "[] \n" "\n" " Disables the command for all users (including the " "owners).\n" " If is given, only disables the from . " "If\n" " you want to disable a command for most users but not for yourself, " "set\n" " a default capability of -plugin.command or -command (if you want to\n" " disable the command in all plugins).\n" " " msgstr "" "[] \n" "\n" "Letiltja parancsot minden felhasználónak (a tulajdonosokat is " "beleértve). Ha meg van adva, csak -ben tiltja le " "-ot. Ha le szeretnél tiltani egy parancsot a legtöbb felhasználónak " "de nem magadnak, állíts be egy -bővítmény.parancs vagy -parancs " "alapértelmezett képességet (ha minden bővítményben le szeretnéd tiltani a " "parancsot)." #: plugin.py:683 msgid "" "[] \n" "\n" " Enables the command for all users. If \n" " if given, only enables the from . This command " "is\n" " the inverse of disable.\n" " " msgstr "" "[] \n" "\n" "Engedélyezi parancsot minden felhasználónak. Ha meg " "van adva, csak -ben engedélyezi -ot. Ez a parancs a " "fordítottja a disable-nek." #: plugin.py:702 msgid "" " \n" "\n" " Renames in to the .\n" " " msgstr "" " <új név>\n" "\n" "Átnevezi -ot -ben az <új név>-re." #: plugin.py:719 msgid "" "\n" "\n" " Removes all renames in . The plugin will be reloaded after\n" " this command is run.\n" " " msgstr "" "\n" "\n" "Eltávolítja az összes átnevezést -ben. A bővítmény újra lesz " "töltve miután ez a parancs lefutott." #: plugin.py:732 msgid "" "takes no argument\n" "\n" " Reloads the locale of the bot." msgstr "" "paraméter nélküli\n" "\n" "Újratölti a bot nyelvét." ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/locales/it.po�����������������������������������������������������0000644�0001750�0001750�00000021153�14535072470�017273� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-15 23:59+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" #: plugin.py:85 msgid "" "Owner-only commands for core Supybot. This is a core Supybot module\n" " that should not be removed!" msgstr "" #: plugin.py:319 msgid "" "If the given message is a command, triggers Limnoria's\n" " command-dispatching for that command.\n" "\n" " Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can\n" " potentially be an artificial message synthesized in doBatch\n" " from a multiline batch.\n" "\n" " Usually, a command is a single message, so ``payload=msg." "params[0]``\n" " However, when ``msg`` is part of a multiline message, the payload\n" " is the concatenation of multiple messages.\n" " See .\n" " " msgstr "" #: plugin.py:372 msgid "" "\n" "\n" " Logs to the global Supybot log at critical priority. Useful " "for\n" " marking logfiles for later searching.\n" " " msgstr "" "\n" "\n" " Registra nel log globale di Supybot con priorità critica.\n" " Utile per contrassegnare i file di log per ricerche successive.\n" " " #: plugin.py:382 msgid "" "\n" "\n" " Sends to all channels the bot is currently on and not\n" " lobotomized in.\n" " " msgstr "" "\n" "\n" " Invia a tutti i canali in cui il bot è attualmente presente " "e non è lobotomizzato.\n" " " #: plugin.py:403 msgid "" "[--remove] []\n" "\n" " Sets the default plugin for to . If --remove is\n" " given, removes the current default plugin for . If no " "plugin\n" " is given, returns the current default plugin set for . " "See\n" " also, supybot.commands.defaultPlugins.importantPlugins.\n" " " msgstr "" "[--remove] []\n" "\n" " Imposta il plugin predefinito per . Se --remove è " "specificato, rimuove\n" " l'attuale plugin per . Se non viene fornito alcun plugin, " "riporta quello\n" " impostato per . Vedi anche supybot.commands.defaultPlugins." "importantPlugins.\n" " " #: plugin.py:441 msgid "" "\n" "\n" " Sends the raw string given to the server.\n" " " msgstr "" "\n" "\n" " Invia la data stringa direttamente al server.\n" " " #: plugin.py:455 #, fuzzy msgid "" "[]\n" "\n" " Exits the bot with the QUIT message . If is not " "given,\n" " the default quit message (supybot.plugins.Owner.quitMsg) will be " "used.\n" " If there is no default quitMsg set, your nick will be used. The " "standard\n" " substitutions ($version, $nick, etc.) are all handled " "appropriately.\n" " " msgstr "" "[]\n" "\n" " Fa uscire il bot con un certo messaggio di QUIT. Se non è " "specificato verrà\n" " utilizzato quello configurato in supybot.plugins.Owner.quitMsg, " "altrimenti il tuo nick.\n" " " #: plugin.py:473 msgid "" "takes no arguments\n" "\n" " Runs all the periodic flushers in world.flushers. This includes\n" " flushing all logs and all configuration changes to disk.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Esegue tutti i periodici \"flusher\" in world.flushers. Ciò include\n" " il salvataggio su disco di tutti i log e tutte le modifiche di " "configurazione.\n" " " #: plugin.py:483 msgid "" "[]\n" "\n" " Runs the standard upkeep stuff (flushes and gc.collects()). If " "given\n" " a level, runs that level of upkeep (currently, the only supported\n" " level is \"high\", which causes the bot to flush a lot of caches as " "well\n" " as do normal upkeep stuff).\n" " " msgstr "" "[]\n" "\n" " Esegue tutte le operazioni standard di mantenimento (flush e gc." "collects()).\n" " Se si specifica un livello, esegue quel livello di mantenimento (al " "momento\n" " l'unico gestito è \"high\", che svuota la cache molto più del " "normale).\n" " " #: plugin.py:529 msgid "" "[--deprecated] \n" "\n" " Loads the plugin from any of the directories in\n" " conf.supybot.directories.plugins; usually this includes the main\n" " installed directory and 'plugins' in the current directory.\n" " --deprecated is necessary if you wish to load deprecated plugins.\n" " " msgstr "" "[--deprecated] \n" "\n" " Carica da qualsiasi directory indicata dalla variabile\n" " conf.supybot.directories.plugins; solitamente questa include quella\n" " principale dell'installazione e \"plugins\" nella directory " "attuale.\n" " --deprecated è necessario per caricare plugin deprecati.\n" " " #: plugin.py:567 msgid "" "\n" "\n" " Unloads and subsequently reloads the plugin by name; use the 'list'\n" " command to see a list of the currently loaded plugins.\n" " " msgstr "" "\n" "\n" " Ricarica ; utilizzare il comando \"list\" per ottenere " "l'elenco di quelli attualmente caricati.\n" " " #: plugin.py:602 msgid "" "\n" "\n" " Unloads the callback by name; use the 'list' command to see a list\n" " of the currently loaded plugins. Obviously, the Owner plugin can't\n" " be unloaded.\n" " " msgstr "" "\n" "\n" " De-carica ; utilizzare il comando \"list\" per ottenere " "l'elenco\n" " di quelli attualmente caricati.\n" ". Il plugin Owner non può essere de-caricato.\n" " " #: plugin.py:631 msgid "" "{add|remove} \n" "\n" " Adds or removes (according to the first argument) from " "the\n" " default capabilities given to users (the configuration variable\n" " supybot.capabilities stores these).\n" " " msgstr "" "{add|remove} \n" "\n" " Aggiunge o rimuove (in base al primo argomento) da quelle " "predefinite\n" " date agli utenti (salvate nella variabile di configurazione supybot." "capabilities).\n" " " #: plugin.py:656 msgid "" "[] \n" "\n" " Disables the command for all users (including the " "owners).\n" " If is given, only disables the from . " "If\n" " you want to disable a command for most users but not for yourself, " "set\n" " a default capability of -plugin.command or -command (if you want to\n" " disable the command in all plugins).\n" " " msgstr "" "[] \n" "\n" " Disabilita per tutti gli utenti (compreso il " "proprietario).\n" " Se è specificato, viene disabilitato solo da " ".\n" " Se si vuole disabilitare un comando per tutti gli utenti tranne se " "stessi,\n" " impostare una capacità predefinita -plugin.command o -command " "(questa\n" " disabilita il comando in tutti i plugin).\n" " " #: plugin.py:683 msgid "" "[] \n" "\n" " Enables the command for all users. If \n" " if given, only enables the from . This command " "is\n" " the inverse of disable.\n" " " msgstr "" "[] \n" "\n" " Abilita per tutti gli utenti. Se è specificato,\n" " viene disabilitato solo da . È il contrario di " "\"disable\".\n" " " #: plugin.py:702 msgid "" " \n" "\n" " Renames in to the .\n" " " msgstr "" " \n" "\n" " Rinomina in con .\n" " " #: plugin.py:719 msgid "" "\n" "\n" " Removes all renames in . The plugin will be reloaded after\n" " this command is run.\n" " " msgstr "" "\n" "\n" " Rimuove tutte le rinominazioni in . Dopo questo comando il " "plugin verrà ricaricato.\n" " " #: plugin.py:732 msgid "" "takes no argument\n" "\n" " Reloads the locale of the bot." msgstr "" "non necessita argomenti\n" "\n" " Ricarica la localizzazione del bot." ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/plugin.py���������������������������������������������������������0000644�0001750�0001750�00000101645�14535072470�016552� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008-2009, James McCoy # Copyright (c) 2010-2021, 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 gc import os import sys import time import socket import string import linecache import re import supybot.log as log import supybot.conf as conf import supybot.i18n as i18n import supybot.utils as utils import supybot.world as world import supybot.ircdb as ircdb import supybot.irclib as irclib import supybot.plugin as plugin import supybot.plugins as plugins import supybot.drivers as drivers import supybot.utils.minisix as minisix import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.commands import additional, getopts, optional, wrap from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Owner') ### # supybot.commands. ### def registerDefaultPlugin(command, plugin): command = callbacks.canonicalName(command) conf.registerGlobalValue(conf.supybot.commands.defaultPlugins, command, registry.String(plugin, '')) # This must be set, or the quotes won't be removed. conf.supybot.commands.defaultPlugins.get(command).set(plugin) registerDefaultPlugin('list', 'Misc') registerDefaultPlugin('help', 'Misc') registerDefaultPlugin('ignore', 'Admin') registerDefaultPlugin('reload', 'Owner') registerDefaultPlugin('enable', 'Owner') registerDefaultPlugin('disable', 'Owner') registerDefaultPlugin('unignore', 'Admin') registerDefaultPlugin('capabilities', 'User') registerDefaultPlugin('addcapability', 'Admin') registerDefaultPlugin('removecapability', 'Admin') class Owner(callbacks.Plugin): """Owner-only commands for core Supybot. This is a core Supybot module that should not be removed!""" # This plugin must be first; its priority must be lowest; otherwise odd # things will happen when adding callbacks. def __init__(self, irc=None): if irc is not None: assert not irc.getCallback(self.name()) self.__parent = super(Owner, self) self.__parent.__init__(irc) # Setup command flood detection. self.commands = ircutils.FloodQueue(conf.supybot.abuse.flood.interval()) self.ircquote_responses = utils.structures.ExpiringDict(600) conf.supybot.abuse.flood.interval.addCallback(self.setFloodQueueTimeout) # Setup plugins and default plugins for commands. # # This needs to be done before we connect to any networks so that the # children of supybot.plugins (the actual plugins) exist and can be # loaded. for (name, s) in registry._cache.items(): if 'alwaysLoadDefault' in name or 'alwaysLoadImportant' in name: continue if name.startswith('supybot.plugins'): try: (_, _, name) = registry.split(name) except ValueError: # unpack list of wrong size. continue # This is just for the prettiness of the configuration file. # There are no plugins that are all-lowercase, so we'll at # least attempt to capitalize them. if name == name.lower(): name = name.capitalize() conf.registerPlugin(name) if name.startswith('supybot.commands.defaultPlugins'): try: (_, _, _, name) = registry.split(name) except ValueError: # unpack list of wrong size. continue registerDefaultPlugin(name, s) # Setup Irc objects, connected to networks. If world.ircs is already # populated, chances are that we're being reloaded, so don't do this. if not world.ircs: for network in conf.supybot.networks(): try: self._connect(network) except socket.error as e: self.log.error('Could not connect to %s: %s.', network, e) except Exception as e: self.log.exception('Exception connecting to %s:', network) self.log.error('Could not connect to %s: %s.', network, e) def callPrecedence(self, irc): return ([], [cb for cb in irc.callbacks if cb is not self]) def __call__(self, irc, msg): ret = super().__call__(irc, msg) if not self.ircquote_responses: # short-path: if we have no ircquote in flight (which we don't, # most of the time), then we can skip all the batch and # label resolution. return ret try: label = msg.server_tags.get("label") if label and msg.command != "BATCH": # if labeled-response BATCH, then it's handled in # _doBatchIrcquote ircquote_response = self.ircquote_responses.pop( (irc.network, label), None) if ircquote_response: ircquote_response["irc"].reply(str(msg).strip()) except Exception as e: self.log.exception("Errored while sending ircquote response") finally: return ret def outFilter(self, irc, msg): if msg.command == 'PRIVMSG' and not world.testing: if ircutils.strEqual(msg.args[0], irc.nick): self.log.warning('Tried to send a message to myself: %r.', msg) return None return msg def reset(self): # This has to be done somewhere, I figure here is as good place as any. callbacks.IrcObjectProxy._mores.clear() self.__parent.reset() def _connect(self, network, serverPort=None, password='', ssl=False): try: group = conf.supybot.networks.get(network) group.servers()[0] except (registry.NonExistentRegistryEntry, IndexError): if serverPort is None: raise ValueError('connect requires a (server, port) ' \ 'if the network is not registered.') conf.registerNetwork(network, password, ssl) server = '%s:%s' % serverPort conf.supybot.networks.get(network).servers.append(server) assert conf.supybot.networks.get(network).servers(), \ 'No servers are set for the %s network.' % network self.log.debug('Creating new Irc for %s.', network) newIrc = irclib.Irc(network) driver = drivers.newDriver(newIrc) self._loadPlugins(newIrc) return newIrc def _loadPlugins(self, irc): self.log.debug('Loading plugins (connecting to %s).', irc.network) alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant() important = conf.supybot.commands.defaultPlugins.importantPlugins() for (name, value) in conf.supybot.plugins.getValues(fullNames=False): if irc.getCallback(name) is None: load = value() if not load and name in important: if alwaysLoadImportant: s = '%s is configured not to be loaded, but is being '\ 'loaded anyway because ' \ 'supybot.plugins.alwaysLoadImportant is True.' self.log.warning(s, name) load = True if load: # We don't load plugins that don't start with a capital # letter. if name[0].isupper() and not irc.getCallback(name): # This is debug because each log logs its beginning. self.log.debug('Loading %s.', name) try: m = plugin.loadPluginModule(name, ignoreDeprecation=True) plugin.loadPluginClass(irc, m) except callbacks.Error as e: # This is just an error message. log.warning(str(e)) except plugins.NoSuitableDatabase as e: s = 'Failed to load %s: no suitable database(%s).' % (name, e) log.warning(s) except ImportError as e: e = str(e) if e.endswith(name): s = 'Failed to load {0}: No plugin named {0} exists.'.format( utils.str.dqrepr(name)) elif "No module named 'config'" in e: s = ("Failed to load %s: This plugin may be incompatible " "with your current Python version." % name) else: s = 'Failed to load %s: import error (%s).' % (name, e) log.warning(s) except Exception as e: log.exception('Failed to load %s:', name) else: # Let's import the module so configuration is preserved. try: _ = plugin.loadPluginModule(name) except Exception as e: log.debug('Attempted to load %s to preserve its ' 'configuration, but load failed: %s', name, e) world.starting = False def do376(self, irc, msg): msgs = conf.supybot.networks.get(irc.network).channels.joins() if msgs: for msg in msgs: irc.queueMsg(msg) do422 = do377 = do376 def setFloodQueueTimeout(self, *args, **kwargs): self.commands.timeout = conf.supybot.abuse.flood.interval() def doBatch(self, irc, msg): self._doBatchMultiline(irc, msg) self._doBatchIrcquote(irc, msg) def _doBatchMultiline(self, irc, msg): if not conf.supybot.protocols.irc.experimentalExtensions(): return batch = msg.tagged('batch') # Always not-None on a BATCH message if msg.args[0].startswith('+'): # Start of a batch, we're not interested yet. return if batch.type != 'draft/multiline': # This is not a multiline batch, also not interested. return assert msg.args[0].startswith("-"), ( "BATCH's first argument should start with either - or +, but " "it is %s." ) % msg.args[0] # End of multiline batch. It may be a long command. payloads = [] first_privmsg = None for message in batch.messages: if message.command != "PRIVMSG": # We're only interested in PRIVMSGs for the payloads. # (eg. exclude NOTICE) continue elif not payloads: # This is the first PRIVMSG of the batch first_privmsg = message payloads.append(message.args[1]) elif 'draft/multiline-concat' in message.server_tags: # This message is not a new line, but the continuation # of the previous one. payloads.append(message.args[1]) else: # New line; stop here. We're not processing extra lines # either as the rest of the command or as new commands. # This may change in the future. break payload = ''.join(payloads) if not payload: self.log.error( 'Got empty multiline payload. This is a bug, please ' 'report it along with logs.' ) return assert first_privmsg, "This shouldn't be None unless payload is empty" # Let's build a synthetic message from the various parts of the # batch, to look like the multiline batch was a single (large) # PRIVMSG: # * copy the tags and server tags of the 'BATCH +' command, # * copy the prefix and channel of any of the PRIVMSGs # inside the batch # * create a new args[1] target = first_privmsg.args[0] synthetic_msg = ircmsgs.IrcMsg( msg=batch.messages[0], # tags, server_tags, time prefix=first_privmsg.prefix, command='PRIVMSG', args=(target, payload) ) self._doPrivmsgs(irc, synthetic_msg) def _doBatchIrcquote(self, irc, msg): if not self.ircquote_responses: # short-path: if we have no ircquote in flight (which we don't, # most of the time), then we can skip all the batch and # label resolution. return if not msg.args[0].startswith("-"): return batch = msg.tagged("batch") label = batch.messages[0].server_tags.get("label") if not label: return ircquote_response = self.ircquote_responses.pop( (irc.network, label), None) if ircquote_response: ircquote_response["irc"].replies( [str(msg).strip() for msg in batch.messages], oneToOne=False ) def doPrivmsg(self, irc, msg): if 'batch' in msg.server_tags: parent_batches = irc.state.getParentBatches(msg) parent_batch_types = [batch.type for batch in parent_batches] if 'draft/multiline' in parent_batch_types \ and conf.supybot.protocols.irc.experimentalExtensions(): # We will handle the message in doBatch when the entire # batch ends. return if 'chathistory' in parent_batch_types: # Either sent automatically by the server upon join, # or triggered by a plugin (why?!) # Either way, replying to commands from the history would # look weird, because they may have been sent a while ago, # and we may have already answered to them. # (this is the same behavior as in PluginRegexp.doPrivmsg) return self._doPrivmsgs(irc, msg) def _doPrivmsgs(self, irc, msg): """If the given message is a command, triggers Limnoria's command-dispatching for that command. Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can potentially be an artificial message synthesized in doBatch from a multiline batch. Usually, a command is a single message, so ``payload=msg.params[0]`` However, when ``msg`` is part of a multiline message, the payload is the concatenation of multiple messages. See . """ assert self is irc.callbacks[0], \ 'Owner isn\'t first callback: %r' % irc.callbacks if ircmsgs.isCtcp(msg): return s = callbacks.addressed(irc, msg) if s: ignored = ircdb.checkIgnored(msg.prefix) if ignored: self.log.info('Ignoring command from %s.', msg.prefix) return maximum = conf.supybot.abuse.flood.command.maximum() self.commands.enqueue(msg) if conf.supybot.abuse.flood.command() \ and self.commands.len(msg) > maximum \ and not ircdb.checkCapability(msg.prefix, 'trusted'): punishment = conf.supybot.abuse.flood.command.punishment() banmask = conf.supybot.protocols.irc.banmask \ .makeBanmask(msg.prefix) self.log.info('Ignoring %s for %s seconds due to an apparent ' 'command flood.', banmask, punishment) ircdb.ignores.add(banmask, time.time() + punishment) if conf.supybot.abuse.flood.command.notify(): irc.reply('You\'ve given me %s commands within the last ' '%i seconds; I\'m now ignoring you for %s.' % (maximum, conf.supybot.abuse.flood.interval(), utils.timeElapsed(punishment, seconds=False))) return try: tokens = callbacks.tokenize(s, channel=msg.channel, network=irc.network) self.Proxy(irc, msg, tokens) except SyntaxError as e: if conf.supybot.reply.error.detailed(): irc.error(str(e)) else: irc.replyError(msg=msg) self.log.info('Syntax error: %s', e) def logmark(self, irc, msg, args, text): """ Logs to the global Supybot log at critical priority. Useful for marking logfiles for later searching. """ self.log.critical(text) irc.replySuccess() logmark = wrap(logmark, ['text']) def announce(self, irc, msg, args, text): """ Sends to all channels the bot is currently on and not lobotomized in. """ u = ircdb.users.getUser(msg.prefix) template = self.registryValue('announceFormat') text = ircutils.standardSubstitute( irc, msg, template, env={'owner': u.name, 'text': text}) for channel in irc.state.channels: c = ircdb.channels.getChannel(channel) if not c.lobotomized: irc.queueMsg(ircmsgs.privmsg(channel, text)) irc.noReply() announce = wrap(announce, ['text']) def defaultplugin(self, irc, msg, args, optlist, command, plugin): """[--remove] [] Sets the default plugin for to . If --remove is given, removes the current default plugin for . If no plugin is given, returns the current default plugin set for . See also, supybot.commands.defaultPlugins.importantPlugins. """ remove = False for (option, arg) in optlist: if option == 'remove': remove = True (_, cbs) = irc.findCallbacksForArgs([command]) if remove: try: conf.supybot.commands.defaultPlugins.unregister(command) irc.replySuccess() except registry.NonExistentRegistryEntry: s = 'I don\'t have a default plugin set for that command.' irc.error(s) elif not cbs: irc.errorInvalid('command', command) elif plugin: if not plugin.isCommand(command): irc.errorInvalid('command in the %s plugin' % plugin.name(), command) registerDefaultPlugin(command, plugin.name()) irc.replySuccess() else: try: irc.reply(conf.supybot.commands.defaultPlugins.get(command)()) except registry.NonExistentRegistryEntry: s = 'I don\'t have a default plugin set for that command.' irc.error(s) defaultplugin = wrap(defaultplugin, [getopts({'remove': ''}), 'commandName', additional('plugin')]) def ircquote(self, irc, msg, args, s): """ Sends the raw string given to the server. """ try: m = ircmsgs.IrcMsg(s) except Exception as e: irc.error(utils.exnToString(e)) else: irc.queueMsg(m) label = m.server_tags.get("label") if label and "labeled-response" in irc.state.capabilities_ack: self.ircquote_responses[(irc.network, label)] = {"irc": irc} else: irc.noReply() ircquote = wrap(ircquote, ['text']) def quit(self, irc, msg, args, text): """[] Exits the bot with the QUIT message . If is not given, the default quit message (supybot.plugins.Owner.quitMsg) will be used. If there is no default quitMsg set, your nick will be used. The standard substitutions ($version, $nick, etc.) are all handled appropriately. """ text = text or self.registryValue('quitMsg') or msg.nick text = ircutils.standardSubstitute(irc, msg, text) irc.noReply() m = ircmsgs.quit(text) world.upkeep() for irc in world.ircs[:]: irc.queueMsg(m) irc.die() quit = wrap(quit, [additional('text')]) def flush(self, irc, msg, args): """takes no arguments Runs all the periodic flushers in world.flushers. This includes flushing all logs and all configuration changes to disk. """ world.flush() irc.replySuccess() flush = wrap(flush) def upkeep(self, irc, msg, args, level): """[] Runs the standard upkeep stuff (flushes and gc.collects()). If given a level, runs that level of upkeep (currently, the only supported level is "high", which causes the bot to flush a lot of caches as well as do normal upkeep stuff). """ L = [] if level == 'high': L.append(format('Regexp cache flushed: %n cleared.', (len(re._cache), 'regexp'))) re.purge() L.append(format('Pattern cache flushed: %n cleared.', (len(ircutils._patternCache), 'compiled pattern'))) ircutils._patternCache.clear() L.append(format('hostmaskPatternEqual cache flushed: %n cleared.', (len(ircutils._hostmaskPatternEqualCache), 'result'))) ircutils._hostmaskPatternEqualCache.clear() L.append(format('ircdb username cache flushed: %n cleared.', (len(ircdb.users._nameCache), 'username to id mapping'))) ircdb.users._nameCache.clear() L.append(format('ircdb hostmask cache flushed: %n cleared.', (len(ircdb.users._hostmaskCache), 'hostmask to id mapping'))) ircdb.users._hostmaskCache.clear() L.append(format('linecache line cache flushed: %n cleared.', (len(linecache.cache), 'line'))) linecache.clearcache() if minisix.PY2: sys.exc_clear() collected = world.upkeep() if gc.garbage: L.append('Garbage! %r.' % gc.garbage) if collected is not None: # Some time between 5.2 and 7.1, Pypy (3?) started returning None # when gc.collect() is called. L.append(format('%n collected.', (collected, 'object'))) if L: irc.reply(' '.join(L)) else: irc.replySuccess() upkeep = wrap(upkeep, [additional(('literal', ['high']))]) def load(self, irc, msg, args, optlist, name): """[--deprecated] Loads the plugin from any of the directories in conf.supybot.directories.plugins; usually this includes the main installed directory and 'plugins' in the current directory. --deprecated is necessary if you wish to load deprecated plugins. """ ignoreDeprecation = False for (option, argument) in optlist: if option == 'deprecated': ignoreDeprecation = True if name.endswith('.py'): name = name[:-3] if irc.getCallback(name): irc.error('%s is already loaded.' % name.capitalize()) return try: module = plugin.loadPluginModule(name, ignoreDeprecation) except plugin.Deprecated: irc.error('%s is deprecated. Use --deprecated ' 'to force it to load.' % name.capitalize()) return except ImportError as e: if str(e).endswith(name): irc.error('No plugin named %s exists.' % utils.str.dqrepr(name)) elif "No module named 'config'" in str(e): irc.error('This plugin may be incompatible with your current Python ' 'version. Try running 2to3 on it.') else: irc.error(str(e)) return cb = plugin.loadPluginClass(irc, module) name = cb.name() # Let's normalize this. conf.registerPlugin(name, True) irc.replySuccess() load = wrap(load, [getopts({'deprecated': ''}), 'something']) def reload(self, irc, msg, args, name): """ Unloads and subsequently reloads the plugin by name; use the 'list' command to see a list of the currently loaded plugins. """ if ircutils.strEqual(name, self.name()): irc.error('You can\'t reload the %s plugin.' % name) return callbacks = irc.removeCallback(name) if callbacks: module = sys.modules[callbacks[0].__module__] if hasattr(module, 'reload'): x = module.reload() try: module = plugin.loadPluginModule(name) if hasattr(module, 'reload') and 'x' in locals(): module.reload(x) if hasattr(module, 'config'): from importlib import reload reload(module.config) for callback in callbacks: callback.die() del callback gc.collect() # This makes sure the callback is collected. callback = plugin.loadPluginClass(irc, module) irc.replySuccess() except ImportError: for callback in callbacks: irc.addCallback(callback) irc.error('No plugin named %s exists.' % name) else: irc.error('There was no plugin %s.' % name) reload = wrap(reload, ['something']) def unload(self, irc, msg, args, name): """ Unloads the callback by name; use the 'list' command to see a list of the currently loaded plugins. Obviously, the Owner plugin can't be unloaded. """ if ircutils.strEqual(name, self.name()): irc.error('You can\'t unload the %s plugin.' % name) return # Let's do this so even if the plugin isn't currently loaded, it doesn't # stay attempting to load. old_callback = irc.getCallback(name) if old_callback: # Normalize the plugin case to prevent duplicate registration # entries, https://github.com/progval/Limnoria/issues/1295 name = old_callback.name() conf.registerPlugin(name, False) callbacks = irc.removeCallback(name) if callbacks: for callback in callbacks: callback.die() del callback gc.collect() irc.replySuccess() return irc.error('There was no plugin %s.' % name) unload = wrap(unload, ['something']) def defaultcapability(self, irc, msg, args, action, capability): """{add|remove} Adds or removes (according to the first argument) from the default capabilities given to users (the configuration variable supybot.capabilities stores these). """ if action == 'add': conf.supybot.capabilities().add(capability) irc.replySuccess() elif action == 'remove': try: conf.supybot.capabilities().remove(capability) irc.replySuccess() except KeyError: if ircdb.isAntiCapability(capability): irc.error('That capability wasn\'t in ' 'supybot.capabilities.') else: anticap = ircdb.makeAntiCapability(capability) conf.supybot.capabilities().add(anticap) irc.replySuccess() defaultcapability = wrap(defaultcapability, [('literal', ['add','remove']), 'capability']) def disable(self, irc, msg, args, plugin, command): """[] Disables the command for all users (including the owners). If is given, only disables the from . If you want to disable a command for most users but not for yourself, set a default capability of -plugin.command or -command (if you want to disable the command in all plugins). """ if command in ('enable', 'identify'): irc.error('You can\'t disable %s.' % command) return if plugin: if plugin.isCommand(command): pluginCommand = '%s.%s' % (plugin.name(), command) conf.supybot.commands.disabled().add(pluginCommand) plugin._disabled.add(command, plugin.name()) else: irc.error('%s is not a command in the %s plugin.' % (command, plugin.name())) return else: conf.supybot.commands.disabled().add(command) self._disabled.add(command) irc.replySuccess() disable = wrap(disable, [optional('plugin'), 'commandName']) def enable(self, irc, msg, args, plugin, command): """[] Enables the command for all users. If if given, only enables the from . This command is the inverse of disable. """ try: if plugin: plugin._disabled.remove(command, plugin.name()) command = '%s.%s' % (plugin.name(), command) else: self._disabled.remove(command) conf.supybot.commands.disabled().remove(command) irc.replySuccess() except KeyError: irc.error('That command wasn\'t disabled.') enable = wrap(enable, [optional('plugin'), 'commandName']) def rename(self, irc, msg, args, command_plugin, command, newName): """ Renames in to the . """ if not command_plugin.isCommand(command): what = 'command in the %s plugin' % command_plugin.name() irc.errorInvalid(what, command) if hasattr(command_plugin, newName): irc.error('The %s plugin already has an attribute named %s.' % (command_plugin, newName)) return plugin.registerRename(command_plugin.name(), command, newName) plugin.renameCommand(command_plugin, command, newName) irc.replySuccess() rename = wrap(rename, ['plugin', 'commandName', 'commandName']) def unrename(self, irc, msg, args, plugin): """ Removes all renames in . The plugin will be reloaded after this command is run. """ try: conf.supybot.commands.renames.unregister(plugin.name()) except registry.NonExistentRegistryEntry: irc.errorInvalid('plugin', plugin.name()) self.reload(irc, msg, [plugin.name()]) # This makes the replySuccess. unrename = wrap(unrename, ['plugin']) def reloadlocale(self, irc, msg, args): """takes no argument Reloads the locale of the bot.""" i18n.reloadLocales() irc.replySuccess() Class = Owner # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Owner/test.py�����������������������������������������������������������0000644�0001750�0001750�00000022732�14535072470�016232� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys from unittest import skip from supybot.test import * import supybot.conf as conf import supybot.plugin as plugin class OwnerTestCase(PluginTestCase): plugins = ('Owner', 'Config', 'Misc', 'Admin') def testHelpLog(self): self.assertHelp('help logmark') def testSrcAmbiguity(self): self.assertError('capability add foo bar') def testIrcquote(self): self.assertResponse('ircquote PRIVMSG %s :foo' % self.irc.nick, 'foo') self.feedMsg('ircquote PING foo') self.assertEqual(self.irc.takeMsg(), ircmsgs.IrcMsg( command='PING', args=('foo',))) def testIrcquoteLabeledResponse(self): self.irc.state.capabilities_ack.update({'labeled-response', 'batch'}) self.feedMsg('ircquote @label=abc PING foo') self.assertEqual(self.irc.takeMsg(), ircmsgs.IrcMsg( server_tags={'label': 'abc'}, command='PING', args=('foo',))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'label': 'abc'}, prefix='server.', command='PONG', args=('foo',))) self.assertResponse(' ', '@label=abc :server. PONG :foo') def testIrcquoteLabeledResponseBatch(self): self.irc.state.capabilities_ack.update({'labeled-response', 'batch'}) self.feedMsg('ircquote @label=abc WHO val') self.assertEqual(self.irc.takeMsg(), ircmsgs.IrcMsg( server_tags={'label': 'abc'}, command='WHO', args=('val',))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'label': 'abc'}, prefix='server.', command='BATCH', args=('+4', 'labeled-response'))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '4'}, prefix='server.', command='311', args=('test', 'val', '~u', 'host', '*', 'Val L'))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '4'}, prefix='server.', command='311', args=('test', 'val', '#limnoria-bots'))) # Batch not complete yet -> no response self.assertIsNone(self.irc.takeMsg()) # end of batch self.irc.feedMsg(ircmsgs.IrcMsg( prefix='server.', command='BATCH', args=('-4',))) self.assertResponse( ' ', '@label=abc :server. BATCH +4 :labeled-response') self.assertResponse( ' ', '@batch=4 :server. 311 test val ~u host * :Val L') self.assertResponse( ' ', '@batch=4 :server. 311 test val :#limnoria-bots') self.assertResponse( ' ', ':server. BATCH :-4') def testFlush(self): self.assertNotError('flush') def testUpkeep(self): self.assertNotError('upkeep') def testLoad(self): self.assertError('load Owner') self.assertError('load owner') self.assertNotError('load Channel') self.assertNotError('list Owner') def testReload(self): self.assertError('reload Channel') self.assertNotError('load Channel') self.assertNotError('reload Channel') self.assertNotError('reload Channel') def testUnload(self): self.assertError('unload Foobar') self.assertNotError('load Channel') self.assertNotError('unload Channel') self.assertError('unload Channel') self.assertNotError('load Channel') self.assertNotError('unload CHANNEL') def testDisable(self): self.assertError('disable enable') self.assertError('disable identify') def testEnable(self): self.assertError('enable enable') def testEnableIsCaseInsensitive(self): self.assertNotError('disable Foo') self.assertNotError('enable foo') def testRename(self): self.assertError('rename Admin join JOIN') self.assertError('rename Admin join jo-in') self.assertNotError('rename Admin join testcommand') self.assertRegexp('list Admin', 'testcommand') self.assertNotRegexp('list Admin', 'join') self.assertError('help join') self.assertRegexp('help testcommand', 'Tell the bot to join') self.assertRegexp('join', 'not a valid command') self.assertHelp('testcommand') self.assertNotError('unrename Admin') self.assertNotRegexp('list Admin', 'testcommand') @skip('Nested commands cannot be renamed yet.') def testRenameNested(self): self.assertNotError('rename Admin "capability remove" rmcap') self.assertNotRegexp('list Admin', 'capability remove') self.assertRegexp('list Admin', 'rmcap') self.assertNotError('reload Admin') self.assertNotRegexp('list Admin', 'capability remove') self.assertRegexp('list Admin', 'rmcap') self.assertNotError('unrename Admin') self.assertRegexp('list Admin', 'capability remove') self.assertNotRegexp('list Admin', 'rmcap') def testDefaultPluginErrorsWhenCommandNotInPlugin(self): self.assertError('defaultplugin foobar owner') class CommandsTestCase(PluginTestCase): plugins = ('Owner', 'Utilities') def testSimpleCommand(self): self.irc.feedMsg( ircmsgs.privmsg(self.irc.nick, 'echo foo $nick!$user@$host', self.prefix)) response = self.irc.takeMsg() self.assertEqual(response.args, (self.nick, 'foo ' + self.prefix)) def testMultilineCommandDisabled(self): self._sendBatch() # response to 'echo ' self.assertRegexp('', '(echo )') # response to 'foo ' self.assertResponse('', 'Error: "foo" is not a valid command.') # response to '$prefix' self.assertResponse( '', 'Error: "$nick!$user@$host" is not a valid command.') # response to 'echo nope' self.assertResponse('', 'nope') def testMultilineCommand(self): with conf.supybot.protocols.irc.experimentalExtensions.context(True): self._sendBatch() response = self.irc.takeMsg() self.assertEqual(response.args, (self.nick, 'foo ' + self.prefix)) response = self.irc.takeMsg() self.assertIsNone(response, 'Should not respond to second line') def _sendBatch(self): self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('+123', 'draft/multiline', self.irc.nick))) # one line self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123'}, prefix=self.prefix, command='PRIVMSG', args=(self.irc.nick, 'echo '))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123', 'draft/multiline-concat': None}, prefix=self.prefix, command='PRIVMSG', args=(self.irc.nick, 'foo '))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123', 'draft/multiline-concat': None}, prefix=self.prefix, command='PRIVMSG', args=(self.irc.nick, '$nick!$user@$host'))) # an other line self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123'}, prefix=self.prefix, command='PRIVMSG', args=(self.irc.nick, 'echo nope'))) self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('-123',))) def testIgnoreChathistory(self): self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('+123', 'chathistory', self.irc.nick))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123'}, prefix=self.prefix, command='PRIVMSG', args=(self.irc.nick, 'echo foo'))) self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('-123',))) self.irc.feedMsg(ircmsgs.IrcMsg( prefix=self.prefix, command='PRIVMSG', args=(self.irc.nick, 'echo bar'))) self.assertResponse('', 'bar') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/�����������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015041� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/__init__.py������������������������������������������������������0000644�0001750�0001750�00000005152�14535072470�017153� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ This plugin handles various plugin-related things, such as getting help for a plugin or retrieving author info. """ import supybot import supybot.world as world __version__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. # Note: the tests for this plugin look at some of this data - update that too # as necessary __contributors__ = { supybot.authors.skorobeus: ['original contributors command'], supybot.authors.jlu: ['refactored contributors command'], } from . import config from . import plugin from importlib 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: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/config.py��������������������������������������������������������0000644�0001750�0001750�00000004733�14535072470�016665� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Plugin') 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('Plugin', True) Plugin = conf.registerPlugin('Plugin') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Plugin, 'someConfigVariableName', # registry.Boolean(False, """Help for someConfigVariableName.""")) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/locales/���������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016463� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/locales/de.po����������������������������������������������������0000644�0001750�0001750�00000016102�14535072470�017411� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-11-01 20:55+0100\n" "Last-Translator: Florian Besser \n" "Language-Team: \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:44 #, fuzzy msgid "" "\n" " This plugin exists to help users manage their plugins. Use 'plugin\n" " list' to list the loaded plugins; use 'plugin help' to get the " "description\n" " of a plugin; use the 'plugin' command itself to determine what plugin a\n" " command exists in.\n" " " msgstr "" "Dieses Plugin gibt es um Nutzern zu helfen ihre Plugins zu verwalten. " "Benutze 'plugin list' um alle geladenen Plugins aufzulisten; benutze 'plugin " "help' um eine Beschreibung des Plugins zu bekommen; benutze 'plugin' selbst " "um herauszufinden in welchem Plugin ein Befehl existiert." #: plugin.py:53 msgid "" "\n" "\n" " Returns a useful description of how to use , if the plugin " "has\n" " one.\n" " " msgstr "" "\n" "\n" "Gibt eine nützliche Beschreibung aus wie man das benutzt, falls das " "Plugin eine hat" #: plugin.py:62 msgid "That plugin is loaded, but has no plugin help." msgstr "Das Plugin ist geladen, hat aber keine Hilfe." #: plugin.py:67 #, fuzzy msgid "" "\n" "\n" " Returns the name of the plugin that would be used to call " ".\n" "\n" " If it is not uniquely determined, returns list of all plugins that\n" " contain .\n" " " msgstr "" "\n" "\n" "Gibt den Namen des Plugins aus das benutzt wird um aufzurufen. Wenn " "dieser nicht einzigartig ist, wird eine Liste aller Plugins ausgegeben die " "über den verfügen." #: plugin.py:85 msgid "plugins" msgstr "Plugins" #: plugin.py:87 msgid "plugin" msgstr "Plugin" #: plugin.py:88 plugin.py:122 msgid "The %q command is available in the %L %s." msgstr "Der %q Befehl ist im %L %s verfügbar." #: plugin.py:91 msgid "There is no command %q." msgstr "Es gibt keinen Befehl %q." #: plugin.py:108 msgid "" "\n" "\n" " Returns the names of all plugins that contain .\n" " " msgstr "" "\n" "\n" "Gibt die Namen aller Plugins aus die beinhalten." #: plugin.py:129 msgid "" "\n" "\n" " Returns the author of . This is the person you should talk " "to\n" " if you have ideas, suggestions, or other comments about a given " "plugin.\n" " " msgstr "" "\n" "\n" "Gibt den Autor von aus. Das ist die Person mit der du kontakt " "aufnehmen sollte falls du Ideen, Vorschläge oder andere Kommentare für das " "Plugin hast." #: plugin.py:135 msgid "That plugin does not seem to be loaded." msgstr "Das Plugin ist wohl nicht geladen." #: plugin.py:146 #, fuzzy msgid "%s was written by %s" msgstr "wurde geschrieben von %s" #: plugin.py:148 msgid "%s was written by %s and is maintained by %s." msgstr "" #: plugin.py:151 #, fuzzy msgid "%s does not have any author listed." msgstr "Kein Autor hat das Plugin beansprucht." #: plugin.py:156 #, fuzzy msgid "" " []\n" "\n" " Replies with a list of people who made contributions to a given " "plugin.\n" " If is specified, that person's specific contributions will\n" " be listed. You can specify a person's name by their full name or " "their nick,\n" " which is shown inside brackets if available.\n" " " msgstr "" " []\n" "\n" "Antwortet mit einer Liste von Personen die zum angegeben Plugin beigetragen " "haben.Falls angegeben wird, werden die personenspezifischen Beitröge " "gelistet. Notiz: Der ist der eingeklammerte Bereich in der " "Personenliste." #: plugin.py:164 msgid "" "\n" " Take a list of long names and turn it into :\n" " shortname[, shortname and shortname].\n" " " msgstr "" "\n" "Nimmt eine Liste von langen Namen und macht daraus: Kurzname[, Kurzname und " "Kurzname]." #: plugin.py:172 msgid "" "\n" " Build the list of author + contributors (if any) for the " "requested\n" " plugin.\n" " " msgstr "" "\n" "Erstellt eine Liste der Autoren + Mitwirkenden (falls es welche gibt) des " "abgefragten Plugins." #: plugin.py:180 #, fuzzy msgid "The %s plugin has not been claimed by an author. " msgstr "wurde von keinem Autor beansprucht." #: plugin.py:184 msgid "%s %h contributed to it." msgstr "%s %h hat dazu beigesteuert." #: plugin.py:188 #, fuzzy msgid "No additional contributors are listed." msgstr "hat keine zusätzliche Mitwirkende." #: plugin.py:192 #, fuzzy msgid "" "\n" " Build the list of contributions (if any) for the requested " "person\n" " for the requested plugin.\n" " " msgstr "" "\n" "Erstelle eine Liste von Beiträgen (falls es welche gibt) die von der " "erfragten Person für das erfragte Plugin gemacht wurden." #: plugin.py:211 #, fuzzy msgid "%s wrote the %s plugin." msgstr "%s hat das %s Plugin geschrieben" #: plugin.py:213 #, fuzzy msgid "%s is not listed as a contributor to %s." msgstr "%s hat keine gelisteten Beiträge für das %s Plugin." #: plugin.py:220 #, fuzzy msgid "%s contributed the following to %s: %s" msgstr "%s hat %L zum %s Plugin beigesteuert." #: plugin.py:223 #, fuzzy msgid "%s did not list any specific contributions to the %s plugin." msgstr "%s hat keine gelisteten Beiträge für das %s Plugin." #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns a list of the currently loaded plugins.\n" #~ " " #~ msgstr "" #~ "hat keine Argumente\n" #~ "\n" #~ "Gibt eine liste aller geladenen Plugins aus." #~ msgid "" #~ "\n" #~ " Take an Authors object, and return only the name and nick " #~ "values\n" #~ " in the format 'First Last (nick)'.\n" #~ " " #~ msgstr "" #~ "\n" #~ " Nimmt ein Autoren Objekt und gibt nur die Namens und Nick Werte im " #~ "format 'Vorname Nachname (Nick)' aus." #~ msgid "" #~ "\n" #~ " Sort the list of 'long names' based on the number of " #~ "contributions\n" #~ " associated with each.\n" #~ " " #~ msgstr "" #~ "\n" #~ "Sortiert die Liste von 'langen Namen' nach der Nummer der Beiträge, mit " #~ "denen sie in Verbindung gebracht werden." #~ msgid "The %s plugin" #~ msgstr "Das %s Plugin" #~ msgid "and" #~ msgstr "und" #~ msgid "has no contributors listed." #~ msgstr "hat keine Mitwirkenden gelistet." #~ msgid "but" #~ msgstr "aber" #~ msgid "The nick specified (%s) is not a registered contributor." #~ msgstr "Der gegebene Nick (%s) hat keinen registrierten Mitwirkenden." #~ msgid "The %s plugin does not have '%s' listed as a contributor." #~ msgstr "Das Plugin %s hat keinen '%s' als Mitwirkenden aufgeführt." #~ msgid "command" #~ msgstr "Befehl" #~ msgid "the %L %s" #~ msgstr "das %L %s" #~ msgid "the %L" #~ msgstr "das %L" #~ msgid "%s wrote the %s plugin and also contributed %L." #~ msgstr "%s hat das Plugin %s geschrieben und auch %L beigesteuert." ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/locales/fi.po����������������������������������������������������0000644�0001750�0001750�00000017401�14535072470�017422� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-24 17:55+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: plugin.py:44 #, fuzzy msgid "" "\n" " This plugin exists to help users manage their plugins. Use 'plugin\n" " list' to list the loaded plugins; use 'plugin help' to get the " "description\n" " of a plugin; use the 'plugin' command itself to determine what plugin a\n" " command exists in.\n" " " msgstr "" "Tämä lisäosa on olemassa auttaakseen ihmisiä hallitsemaan lisäosiaan. Käytä " "komentoa 'plugin\n" " list' nähdäksesi listan ladatuista lisäosista; käytä 'plugin help' " "komentoa saadaksesi kuvauksen\n" " lisäosasta; käytä 'plugin' komentoa itseään määrittääksesi mitkä " "komennot lisäosassa\n" " on olemassa." #: plugin.py:53 msgid "" "\n" "\n" " Returns a useful description of how to use , if the plugin " "has\n" " one.\n" " " msgstr "" "\n" "\n" " Palauttaa hyödyllistä tietoa, kuinka käytetään, jos " "lisäosalla on\n" " sellainen.\n" " " #: plugin.py:62 msgid "That plugin is loaded, but has no plugin help." msgstr "Tuo lisäosa on ladattu, mutta sillä ei ole \"plugin help\" ohjetta." #: plugin.py:67 #, fuzzy msgid "" "\n" "\n" " Returns the name of the plugin that would be used to call " ".\n" "\n" " If it is not uniquely determined, returns list of all plugins that\n" " contain .\n" " " msgstr "" "\n" "\n" " Palauttaa lisäosan nimen, jota voisi käyttää kutsumaan .\n" " \n" " Jos se ei ole ainutlaatuisesti määritetty, palauttaa listan kaikista " "lisäosista, jotka\n" " sisältävät .\n" " " #: plugin.py:85 msgid "plugins" msgstr "lisäosissa" #: plugin.py:87 msgid "plugin" msgstr "lisäosassa" #: plugin.py:88 plugin.py:122 msgid "The %q command is available in the %L %s." msgstr "Komento %q on saatavilla %L %s." #: plugin.py:91 msgid "There is no command %q." msgstr "%q komentoa %q ei ole." #: plugin.py:108 msgid "" "\n" "\n" " Returns the names of all plugins that contain .\n" " " msgstr "" "\n" "\n" " Palauttaa kaikkien lisäosien nimet, jotka sisältävät .\n" " " #: plugin.py:129 msgid "" "\n" "\n" " Returns the author of . This is the person you should talk " "to\n" " if you have ideas, suggestions, or other comments about a given " "plugin.\n" " " msgstr "" "\n" "\n" " Palauttaa kirjoittajan. Tämä on henkilö, jolle sinun " "pitäisi puhua\n" " jos sinulla on ideaoita, ehdotuksia, tai muita kommentteja annetusta " "lisäosasta.\n" " " #: plugin.py:135 msgid "That plugin does not seem to be loaded." msgstr "Tuo lisäosa ei vaikuta olevan ladattu." #: plugin.py:146 #, fuzzy msgid "%s was written by %s" msgstr "kirjoittanut %s" #: plugin.py:148 msgid "%s was written by %s and is maintained by %s." msgstr "" #: plugin.py:151 #, fuzzy msgid "%s does not have any author listed." msgstr "Tuolla lisäosalla ei ole tekijää, joka ilmoittaa sen omakseen." #: plugin.py:156 #, fuzzy msgid "" " []\n" "\n" " Replies with a list of people who made contributions to a given " "plugin.\n" " If is specified, that person's specific contributions will\n" " be listed. You can specify a person's name by their full name or " "their nick,\n" " which is shown inside brackets if available.\n" " " msgstr "" " []\n" "\n" " Vastaa listalla ihmisistä, jotka ovat osallistuneet annetun lisäosan " "kehittämiseen.\n" " Jos on annettu, juuri tuon henkilön osallistuminen\n" " luetellaan. Huomaa: on se sulkeissa oleva osa\n" " henkilöluetteloinnissa..\n" " " #: plugin.py:164 #, fuzzy msgid "" "\n" " Take a list of long names and turn it into :\n" " shortname[, shortname and shortname].\n" " " msgstr "" "\n" " Ottaa listan pitkistä nimistä ja muuttaa sen :\n" " lyhyeksi nimeksi[, lyhyt nimi ja lyhyt nimi].\n" " " #: plugin.py:172 msgid "" "\n" " Build the list of author + contributors (if any) for the " "requested\n" " plugin.\n" " " msgstr "" "\n" " Rakentaa listan tekijöistä + osallistujista (jos yhtään) " "pyydetylle\n" " lisäosalle.\n" " " #: plugin.py:180 #, fuzzy msgid "The %s plugin has not been claimed by an author. " msgstr "ei ole kirjoittajan omakseen väittämä" #: plugin.py:184 msgid "%s %h contributed to it." msgstr "%s %h osallistuivat siihen." #: plugin.py:188 #, fuzzy msgid "No additional contributors are listed." msgstr "ei ole vaihtoehtoisia osallistujia lueteltuna." #: plugin.py:192 #, fuzzy msgid "" "\n" " Build the list of contributions (if any) for the requested " "person\n" " for the requested plugin.\n" " " msgstr "" "\n" " Rakentaa listan osallistumisista (jos yhtään) pyydetylle " "henkilölle\n" " pyydetystä lisäosasta.\n" " " #: plugin.py:211 #, fuzzy msgid "%s wrote the %s plugin." msgstr "%s kirjoitti lisäosan %s" #: plugin.py:213 #, fuzzy msgid "%s is not listed as a contributor to %s." msgstr "%s:llä ei ole lueteltuja osallistujia lisäosaan %s." #: plugin.py:220 #, fuzzy msgid "%s contributed the following to %s: %s" msgstr "%s osallistui %L:stä %s lisäosaan." #: plugin.py:223 #, fuzzy msgid "%s did not list any specific contributions to the %s plugin." msgstr "%s:llä ei ole lueteltuja osallistujia lisäosaan %s." #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns a list of the currently loaded plugins.\n" #~ " " #~ msgstr "" #~ "ei ota parametrejä\n" #~ "\n" #~ " Palauttaa listan tällä hetkellä ladatuista lisäosista.\n" #~ " " #~ msgid "" #~ "\n" #~ " Take an Authors object, and return only the name and nick " #~ "values\n" #~ " in the format 'First Last (nick)'.\n" #~ " " #~ msgstr "" #~ "\n" #~ " Ota kirjoittajan objekti, ja palauta vain nimi ja nimimerkki " #~ "arvot\n" #~ " muodossa 'Etu- Sukunimi (nimimerkki)'.\n" #~ " " #~ msgid "" #~ "\n" #~ " Sort the list of 'long names' based on the number of " #~ "contributions\n" #~ " associated with each.\n" #~ " " #~ msgstr "" #~ "\n" #~ " Lajittelee listan 'pitkistä nimistä' perustuen osallistumisen " #~ "määrän\n" #~ " liitettynä toisiinsa.\n" #~ " " #~ msgid "The %s plugin" #~ msgstr "%s lisäosa" #~ msgid "and" #~ msgstr "ja" #~ msgid "has no contributors listed." #~ msgstr "ei ole lueteltuja osallistumisia." #~ msgid "but" #~ msgstr "mutta" #~ msgid "The nick specified (%s) is not a registered contributor." #~ msgstr "Määritetty nimimerkki (%s) ei ole rekisteröitynyt osallistuja." #~ msgid "The %s plugin does not have '%s' listed as a contributor." #~ msgstr "Lisäosalla %s ei ole '%s':ää lueteltuna osallistujaksi." #~ msgid "command" #~ msgstr "komento" #~ msgid "the %L %s" #~ msgstr "%L %s" #~ msgid "the %L" #~ msgstr "%L" #~ msgid "%s wrote the %s plugin and also contributed %L." #~ msgstr "%s kirjoitti lisäosan %s ja osallistui myös %L:ään." ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/locales/fr.po����������������������������������������������������0000644�0001750�0001750�00000014351�14535072470�017434� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:44 #, fuzzy msgid "" "\n" " This plugin exists to help users manage their plugins. Use 'plugin\n" " list' to list the loaded plugins; use 'plugin help' to get the " "description\n" " of a plugin; use the 'plugin' command itself to determine what plugin a\n" " command exists in.\n" " " msgstr "" "Ce plugin existe pour aider les utilisateurs à gérer leurs plugins. " "Utilisez 'list' pour liser les plugins chargés ; utilisez 'help' pour " "avoir de l'aide quant à d'autres plugins ; utilisez la commande 'plugin' " "elle-même pour déterminer dans quel plugin une commande existe." #: plugin.py:53 msgid "" "\n" "\n" " Returns a useful description of how to use , if the plugin " "has\n" " one.\n" " " msgstr "" "\n" "\n" "Retourne une description utile de comment utiliser le , si le plugin " "en a une." #: plugin.py:62 msgid "That plugin is loaded, but has no plugin help." msgstr "Ce plugin est chargé mais n'a pas d'aide." #: plugin.py:67 #, fuzzy msgid "" "\n" "\n" " Returns the name of the plugin that would be used to call " ".\n" "\n" " If it is not uniquely determined, returns list of all plugins that\n" " contain .\n" " " msgstr "" "\n" "\n" "Retourne le nom du plugin qui serait utilisé pour appeller la . Si " "il ne peut être déterminé (c'est à dire s'il y a un conflit, sans plugin par " "défaut), la liste de tous les plugins contenant cette commande sera renvoyée." #: plugin.py:85 msgid "plugins" msgstr "plugins" #: plugin.py:87 msgid "plugin" msgstr "plugin" #: plugin.py:88 plugin.py:122 msgid "The %q command is available in the %L %s." msgstr "La commande %q est disponibles dans le(s) plugin(s) %L%v." #: plugin.py:91 msgid "There is no command %q." msgstr "Il n'y a pas de commande %q." #: plugin.py:108 msgid "" "\n" "\n" " Returns the names of all plugins that contain .\n" " " msgstr "" "\n" "\n" "Retourne les noms de tous les plugins contenant la ." #: plugin.py:129 msgid "" "\n" "\n" " Returns the author of . This is the person you should talk " "to\n" " if you have ideas, suggestions, or other comments about a given " "plugin.\n" " " msgstr "" "\n" "\n" "Retourne l'auteur du . C'est la personne à qui vous devriez parler " "si vous avez des idées, suggestions, ou d'autres commentaires à propos d'un " "plugin donné." #: plugin.py:135 msgid "That plugin does not seem to be loaded." msgstr "Ce plugin ne semble pas être chargé." #: plugin.py:146 #, fuzzy msgid "%s was written by %s" msgstr "a été écrit par %s" #: plugin.py:148 msgid "%s was written by %s and is maintained by %s." msgstr "" #: plugin.py:151 #, fuzzy msgid "%s does not have any author listed." msgstr "Ce plugin n'a pas d'auteur." #: plugin.py:156 #, fuzzy msgid "" " []\n" "\n" " Replies with a list of people who made contributions to a given " "plugin.\n" " If is specified, that person's specific contributions will\n" " be listed. You can specify a person's name by their full name or " "their nick,\n" " which is shown inside brackets if available.\n" " " msgstr "" " []\n" "\n" "Renvoie une liste des personnes ayant contribué à un plugin donné. Si le " " est spécifié, les contributions de cette personne seront lisées. " "Note : est la partie entre parenthèses lors du listing des personnes." #: plugin.py:164 msgid "" "\n" " Take a list of long names and turn it into :\n" " shortname[, shortname and shortname].\n" " " msgstr "" #: plugin.py:172 msgid "" "\n" " Build the list of author + contributors (if any) for the " "requested\n" " plugin.\n" " " msgstr "" #: plugin.py:180 #, fuzzy msgid "The %s plugin has not been claimed by an author. " msgstr "n'a aucun auteur" #: plugin.py:184 msgid "%s %h contributed to it." msgstr "%s y %h contribué." #: plugin.py:188 #, fuzzy msgid "No additional contributors are listed." msgstr "n'a pas d'autre contributeur listé." #: plugin.py:192 msgid "" "\n" " Build the list of contributions (if any) for the requested " "person\n" " for the requested plugin.\n" " " msgstr "" #: plugin.py:211 #, fuzzy msgid "%s wrote the %s plugin." msgstr "%s a écrit le plugin %s" #: plugin.py:213 #, fuzzy msgid "%s is not listed as a contributor to %s." msgstr "%s n'a pas de contribution listée pour le plugin %s." #: plugin.py:220 #, fuzzy msgid "%s contributed the following to %s: %s" msgstr "%s a contribué à %L et au plugin %s" #: plugin.py:223 #, fuzzy msgid "%s did not list any specific contributions to the %s plugin." msgstr "%s n'a pas de contribution listée pour le plugin %s." #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns a list of the currently loaded plugins.\n" #~ " " #~ msgstr "" #~ "ne prend pas d'argument\n" #~ "\n" #~ "Retourne une liste des plugins actuellement chargés." #~ msgid "The %s plugin" #~ msgstr "Le plugin s" #~ msgid "and" #~ msgstr "et" #~ msgid "has no contributors listed." #~ msgstr "n'a pas de contributeur listé." #~ msgid "but" #~ msgstr "mais" #~ msgid "The nick specified (%s) is not a registered contributor." #~ msgstr "Le nick spécifié(%s) n'est pas un contributeur enregistré." #~ msgid "The %s plugin does not have '%s' listed as a contributor." #~ msgstr "Le plugin %s n'a pas '%s' listé comme contributeur." #~ msgid "command" #~ msgstr "commande" #~ msgid "the %L %s" #~ msgstr "La/les commande(s) %L%v" #~ msgid "the %L" #~ msgstr "La/les %L" #~ msgid "%s wrote the %s plugin and also contributed %L." #~ msgstr "%s a écrit le plugin %s et a aussi contribué à %L" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/locales/it.po����������������������������������������������������0000644�0001750�0001750�00000016172�14535072470�017444� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-10 11: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" #: plugin.py:44 #, fuzzy msgid "" "\n" " This plugin exists to help users manage their plugins. Use 'plugin\n" " list' to list the loaded plugins; use 'plugin help' to get the " "description\n" " of a plugin; use the 'plugin' command itself to determine what plugin a\n" " command exists in.\n" " " msgstr "" "Questo plugin è per aiutare gli utenti a gestire i loro plugin. Usa " "\"plugin\n" " list\" per elencare tutti quelli caricati; \"plugin help\" per ottenere " "la\n" " descrizione di un plugin; lo stesso comando \"plugin\" per determinare " "in quale\n" " plugin è presente un certo comando." #: plugin.py:53 msgid "" "\n" "\n" " Returns a useful description of how to use , if the plugin " "has\n" " one.\n" " " msgstr "" "\n" "\n" " Riporta un'utile descrizione di come utilizzare , se " "disponibile.\n" " " #: plugin.py:62 msgid "That plugin is loaded, but has no plugin help." msgstr "Il plugin è caricato ma non ha un help." #: plugin.py:67 #, fuzzy msgid "" "\n" "\n" " Returns the name of the plugin that would be used to call " ".\n" "\n" " If it is not uniquely determined, returns list of all plugins that\n" " contain .\n" " " msgstr "" "\n" "\n" " Restituisce il nome del plugin che sarà utilizzato per richiamare " ".\n" " \n" " Se non è unico riporta l'elenco di tutti i plugin che contengono " ".\n" " " #: plugin.py:85 msgid "plugins" msgstr "plugin" #: plugin.py:87 msgid "plugin" msgstr "plugin" #: plugin.py:88 plugin.py:122 msgid "The %q command is available in the %L %s." msgstr "Il comando %q è disponibile nel %L %s." #: plugin.py:91 msgid "There is no command %q." msgstr "Non c'è un comando %q." #: plugin.py:108 msgid "" "\n" "\n" " Returns the names of all plugins that contain .\n" " " msgstr "" "\n" "\n" " Restituisce i nomi di tutti i plugin che contengono .\n" " " #: plugin.py:129 msgid "" "\n" "\n" " Returns the author of . This is the person you should talk " "to\n" " if you have ideas, suggestions, or other comments about a given " "plugin.\n" " " msgstr "" "\n" "\n" " Riporta l'autore di . È la persona con cui dovresti parlare " "se hai\n" " idee, suggerimenti o altri commenti a proposito del plugin.\n" " " #: plugin.py:135 msgid "That plugin does not seem to be loaded." msgstr "Questo plugin non sembra caricato." #: plugin.py:146 #, fuzzy msgid "%s was written by %s" msgstr "è stato scritto da %s" #: plugin.py:148 msgid "%s was written by %s and is maintained by %s." msgstr "" #: plugin.py:151 #, fuzzy msgid "%s does not have any author listed." msgstr "Questo plugin non ha un autore." #: plugin.py:156 #, fuzzy msgid "" " []\n" "\n" " Replies with a list of people who made contributions to a given " "plugin.\n" " If is specified, that person's specific contributions will\n" " be listed. You can specify a person's name by their full name or " "their nick,\n" " which is shown inside brackets if available.\n" " " msgstr "" " []\n" "\n" " Risponde con un elenco di persone che hanno contribuito al dato " "plugin.\n" " Se è specificato, verranno elencati i contributi di quella " "determinata\n" " persona. Nota: è la parte tra parentesi.\n" " " #: plugin.py:164 msgid "" "\n" " Take a list of long names and turn it into :\n" " shortname[, shortname and shortname].\n" " " msgstr "" "\n" " Prende una lista di nomi lunghi e li trasforma in: nomecorto[, " "nomecorto e nomecorto].\n" " " #: plugin.py:172 msgid "" "\n" " Build the list of author + contributors (if any) for the " "requested\n" " plugin.\n" " " msgstr "" "\n" " Crea l'elenco di autori + contributori (eventuali) per il plugin " "richiesto.\n" " " #: plugin.py:180 #, fuzzy msgid "The %s plugin has not been claimed by an author. " msgstr "non ha alcun autore" #: plugin.py:184 msgid "%s %h contributed to it." msgstr "%s %h contribuito." #: plugin.py:188 #, fuzzy msgid "No additional contributors are listed." msgstr "non ha contributori aggiuntivi elencati." #: plugin.py:192 #, fuzzy msgid "" "\n" " Build the list of contributions (if any) for the requested " "person\n" " for the requested plugin.\n" " " msgstr "" "\n" " Crea l'elenco di contributi (eventuali) per la persona richiesta " "nel dato plugin.\n" " " #: plugin.py:211 #, fuzzy msgid "%s wrote the %s plugin." msgstr "%s ha scritto il plugin %s" #: plugin.py:213 #, fuzzy msgid "%s is not listed as a contributor to %s." msgstr "%s non ha contributi elencati per il plugin %s." #: plugin.py:220 #, fuzzy msgid "%s contributed the following to %s: %s" msgstr "%s ha contribuito a %L per il plugin %s." #: plugin.py:223 #, fuzzy msgid "%s did not list any specific contributions to the %s plugin." msgstr "%s non ha contributi elencati per il plugin %s." #~ msgid "" #~ "takes no arguments\n" #~ "\n" #~ " Returns a list of the currently loaded plugins.\n" #~ " " #~ msgstr "" #~ "non necessita argomenti\n" #~ "\n" #~ " Riporta un elenco dei plugin attualmente caricati.\n" #~ " " #~ msgid "" #~ "\n" #~ " Take an Authors object, and return only the name and nick " #~ "values\n" #~ " in the format 'First Last (nick)'.\n" #~ " " #~ msgstr "" #~ "\n" #~ " Prende l'oggetto Authors e riporta nome e nick nella forma " #~ "\"Nome Cognome (nick)\".\n" #~ " " #~ msgid "" #~ "\n" #~ " Sort the list of 'long names' based on the number of " #~ "contributions\n" #~ " associated with each.\n" #~ " " #~ msgstr "" #~ "\n" #~ " Ordina l'elenco di \"nomi lunghi\" in base al numero di " #~ "contributi associati a ognuno.\n" #~ " " #~ msgid "The %s plugin" #~ msgstr "Il plugin %s" #~ msgid "and" #~ msgstr "e" #~ msgid "has no contributors listed." #~ msgstr "non ha contributori elencati." #~ msgid "but" #~ msgstr "ma" #~ msgid "The nick specified (%s) is not a registered contributor." #~ msgstr "Il nick specificato (%s) non è un contributore registrato." #~ msgid "The %s plugin does not have '%s' listed as a contributor." #~ msgstr "Il plugin %s non ha \"%s\" elencato come contributore." #~ msgid "command" #~ msgstr "comando" #~ msgid "the %L %s" #~ msgstr "il %L %s" #~ msgid "the %L" #~ msgstr "il %L" #~ msgid "%s wrote the %s plugin and also contributed %L." #~ msgstr "%s ha scritto il plugin %s e ha contribuito anche a %L." ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/plugin.py��������������������������������������������������������0000644�0001750�0001750�00000023401�14535072470�016707� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2019, James Lu # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot 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.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Plugin') class Plugin(callbacks.Plugin): """ This plugin exists to help users manage their plugins. Use 'plugin list' to list the loaded plugins; use 'plugin help' to get the description of a plugin; use the 'plugin' command itself to determine what plugin a command exists in. """ @internationalizeDocstring def help(self, irc, msg, args, cb): """ Returns a useful description of how to use , if the plugin has one. """ doc = cb.getPluginHelp() if doc: irc.reply(utils.str.normalizeWhitespace(doc.split("\n\n")[0])) else: irc.reply(_('That plugin is loaded, but has no plugin help.')) help = wrap(help, ['plugin']) @internationalizeDocstring def plugin(self, irc, msg, args, command): """ Returns the name of the plugin that would be used to call . If it is not uniquely determined, returns list of all plugins that contain . """ (maxL, cbs) = irc.findCallbacksForArgs(command) L = [] if maxL == command: for cb in cbs: L.append(cb.name()) command = callbacks.formatCommand(command) if L: if irc.nested: irc.reply(format('%L', L)) else: if len(L) > 1: plugin = _('plugins') else: plugin = _('plugin') irc.reply(format(_('The %q command is available in the %L ' '%s.'), command, L, plugin)) else: irc.error(format(_('There is no command %q.'), command)) plugin = wrap(plugin, [many('something')]) def _findCallbacks(self, irc, command): command = list(map(callbacks.canonicalName, command)) plugin_list = [] for cb in irc.callbacks: if not hasattr(cb, 'getCommand'): continue longest_matching_command = cb.getCommand(command) if len(longest_matching_command) >= len(command): # Actually, this is equivalent to use == plugin_list.append(cb.name()) return plugin_list @internationalizeDocstring def plugins(self, irc, msg, args, command): """ Returns the names of all plugins that contain . """ L = self._findCallbacks(irc, command) command = callbacks.formatCommand(command) if L: if irc.nested: irc.reply(format('%L', L)) else: if len(L) > 1: plugin = 'plugins' else: plugin = 'plugin' irc.reply(format(_('The %q command is available in the %L %s.'), command, L, plugin)) else: irc.error(format('There is no command %q.', command)) plugins = wrap(plugins, [many('something')]) def author(self, irc, msg, args, cb): """ Returns the author of . This is the person you should talk to if you have ideas, suggestions, or other comments about a given plugin. """ if cb is None: irc.error(_('That plugin does not seem to be loaded.')) return module = cb.classModule author = getattr(module, '__author__', None) # Allow for a maintainer field, which better represents plugins that have changed hands # over time. Of course, assume that the author is the maintainer if no other info is given. maintainer = getattr(module, '__maintainer__', None) or author if author: if maintainer == author: irc.reply(_("%s was written by %s") % (cb.name(), author)) else: irc.reply(_("%s was written by %s and is maintained by %s.") % \ (cb.name(), author, maintainer)) else: irc.reply(_('%s does not have any author listed.') % cb.name()) author = wrap(author, [('plugin')]) @internationalizeDocstring def contributors(self, irc, msg, args, cb, nick): """ [] Replies with a list of people who made contributions to a given plugin. If is specified, that person's specific contributions will be listed. You can specify a person's name by their full name or their nick, which is shown inside brackets if available. """ def buildContributorsString(longList): """ Take a list of long names and turn it into : shortname[, shortname and shortname]. """ L = [authorInfo.format(short=True) for authorInfo in longList] return format('%L', L) def buildPeopleString(module): """ Build the list of author + contributors (if any) for the requested plugin. """ author = getattr(module, '__author__', supybot.authors.unknown) if author != supybot.authors.unknown: s = _('The %s plugin was written by %s. ' % (cb.name(), author)) else: s = _('The %s plugin has not been claimed by an author. ') % cb.name() contribs = getattr(module, '__contributors__', {}) if contribs: s += format(_('%s %h contributed to it.'), buildContributorsString(contribs.keys()), len(contribs)) else: s += _('No additional contributors are listed.') return s def buildPersonString(module): """ Build the list of contributions (if any) for the requested person for the requested plugin. """ contributors = getattr(module, '__contributors__', {}) # Make a mapping of nicks and names to author instances contributorNicks = {} for contributor in contributors.keys(): if contributor.nick: contributorNicks[contributor.nick.lower()] = contributor if contributor.name: contributorNicks[contributor.name.lower()] = contributor lnick = nick.lower() author = getattr(module, '__author__', supybot.authors.unknown) if author != supybot.authors.unknown and \ (lnick == (author.name or '').lower() or lnick == (author.nick or '').lower()): # Special case for the plugin author. We remove legacy handling of the case where # someone is listed both as author and contributor, which should never really happen? return _('%s wrote the %s plugin.') % (author, cb.name()) elif lnick not in contributorNicks: return _('%s is not listed as a contributor to %s.') % (nick, cb.name()) authorInfo = contributorNicks[lnick] contributions = contributors[authorInfo] fullName = authorInfo.format(short=True) if contributions: return format(_('%s contributed the following to %s: %s'), fullName, cb.name(), ', '.join(contributions)) else: return _('%s did not list any specific contributions to the %s ' 'plugin.') % (fullName, cb.name()) # First we need to check and see if the requested plugin is loaded module = cb.classModule if not nick: irc.reply(buildPeopleString(module)) else: nick = ircutils.toLower(nick) irc.reply(buildPersonString(module)) contributors = wrap(contributors, ['plugin', additional('text')]) Plugin = internationalizeDocstring(Plugin) Class = Plugin # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Plugin/test.py����������������������������������������������������������0000644�0001750�0001750�00000010260�14535072470�016367� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot from supybot.test import * class PluginTestCase(PluginTestCase): plugins = ('Plugin', 'Utilities', 'Admin', 'Format') def testPlugin(self): self.assertRegexp('plugin ignore', 'available.*Utilities plugin') self.assertResponse('echo [plugin ignore]', 'Utilities') def testPlugins(self): self.assertRegexp('plugins join', '(Format.*Admin|Admin.*Format)') self.assertRegexp('plugins plugin', 'Plugin') self.assertNotRegexp('plugins ignore add', 'Utilities') self.assertNotRegexp('plugins ignore', 'Admin') def testHelp(self): self.assertRegexp('plugin help plugin', 'manage their plugins') def testAuthor(self): self.assertRegexp('plugin author plugin', 'jemfinch') self.assertRegexp('plugin author plugin', 'maintained by %s' % supybot.authors.limnoria_core.name) def testContributors(self): # Test ability to list contributors self.assertNotError('contributors Plugin') # Test ability to list contributions # As of 2019-10-19 there is no more distinction between commands and non-commands self.assertRegexp('contributors Plugin skorobeus', 'original contributors command') self.assertRegexp('contributors Plugin Kevin Murphy', 'original contributors command') self.assertRegexp('contributors Plugin James Lu', 'refactored contributors command') # Test handling of the plugin author, who is usually not listed in __contributors__ self.assertRegexp('contributors Plugin jemfinch', 'wrote the Plugin plugin') self.assertRegexp('contributors Plugin Jeremy Fincher', 'wrote the Plugin plugin') # TODO: test handling of a person with multiple contributions to a command # Test handling of invalid plugin self.assertRegexp('contributors InvalidPlugin', 'not a valid plugin') # Test handling of unknown person. As of 2019-10-19 it doesn't matter whether # they're listed in supybot.authors or not. self.assertRegexp('contributors Plugin noname', 'not listed as a contributor') self.assertRegexp('contributors Plugin bwp', 'not listed as a contributor') def testContributorsIsCaseInsensitive(self): self.assertNotError('contributors Plugin Skorobeus') self.assertNotError('contributors Plugin sKoRoBeUs') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3497546 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/�������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�017060� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/__init__.py��������������������������������������������0000644�0001750�0001750�00000005156�14535072470�021176� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2011-2021, 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. ### """ This plugin allows you to quickly download and install a plugin from other repositories. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.progval __maintainer__ = supybot.authors.limnoria_core # 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/PluginDownloader/download' from . import config from . import plugin from importlib 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 tabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/config.py����������������������������������������������0000644�0001750�0001750�00000004747�14535072470�020711� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2011-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('PluginDownloader') 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('PluginDownloader', True) PluginDownloader = conf.registerPlugin('PluginDownloader') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(PluginDownloader, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: �������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/locales/�����������������������������������������������0000755�0001750�0001750�00000000000�14535072535�020502� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/locales/de.po������������������������������������������0000644�0001750�0001750�00000010732�14535072470�021433� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:39+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:167 msgid "" "Plugin is probably not compatible with your Python version (3.x) and could " "not be converted because 2to3 is not installed." msgstr "" #: plugin.py:174 msgid "" "Plugin was designed for Python 2, but an attempt to convert it to Python 3 " "has been made. There is no guarantee it will work, though." msgstr "" #: plugin.py:178 msgid "Plugin successfully installed." msgstr "" #: plugin.py:323 msgid "" "\n" " This plugin allows you to install unofficial plugins from\n" " multiple repositories easily. Use the \"repolist\" command to see list " "of\n" " available repositories and \"repolist \" to list plugins,\n" " which are available in that repository. When you want to install a " "plugin,\n" " just run command \"install \".\n" "\n" " First start by using the `plugindownloader repolist` command to see the\n" " available repositories.\n" "\n" " To see the plugins inside repository, use command\n" " `plugindownloader repolist `\n" "\n" " When you see anything interesting, you can use\n" " `plugindownloader info ` to see what the plugin is\n" " for.\n" "\n" " And finally to install the plugin,\n" " `plugindownloader install `.\n" "\n" " Examples\n" " ^^^^^^^^\n" "\n" " ::\n" "\n" " < Mikaela> @load PluginDownloader\n" " < Limnoria> Ok.\n" " < Mikaela> @plugindownloader repolist\n" " < Limnoria> Antibody, jlu5, Hoaas, Iota, progval, SpiderDave, " "boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, " "jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, " "nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, " "stepnem\n" " < Mikaela> @plugindownloader repolist progval\n" " < Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, " "ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, " "Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, " "IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, " "Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, " "Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)\n" " < Mikaela> more\n" " < Limnoria> SilencePlugin, StdoutCapture, Sudo, SupyML, SupySandbox, " "TWSS, Trigger, Trivia, Twitter, TwitterStream, Untiny, Variables, WebDoc, " "WebLogs, WebStats, Website, WikiTrans, Wikipedia, WunderWeather\n" " < Mikaela> @plugindownloader info progval Wikipedia\n" " < Limnoria> Grabs data from Wikipedia.\n" " < Mikaela> @plugindownloader install progval Wikipedia\n" " < Limnoria> Ok.\n" " < Mikaela> @load Wikipedia\n" " < Limnoria> Ok.\n" " " msgstr "" #: plugin.py:368 msgid "" "[]\n" "\n" " Displays the list of plugins in the .\n" " If is not given, returns a list of available\n" " repositories." msgstr "" #: plugin.py:376 plugin.py:387 msgid ", " msgstr ", " #: plugin.py:378 plugin.py:400 plugin.py:425 msgid "This repository does not exist or is not known by this bot." msgstr "Diese Quelle existiert nicht, oder sie ist dem Bot nicht bekannt." #: plugin.py:385 msgid "No plugin found in this repository." msgstr "Kein Plugin in dieser Quelle gefunden." #: plugin.py:392 msgid "" " \n" "\n" " Downloads and installs the from the ." msgstr "" #: plugin.py:396 msgid "" "This command is not available, because supybot.commands.allowShell is False." msgstr "" #: plugin.py:405 plugin.py:430 msgid "This plugin does not exist in this repository." msgstr "Das Plugin ist in dieser Quelle nicht vorhanden." #: plugin.py:420 msgid "" " \n" "\n" " Displays informations on the in the ." msgstr "" #: plugin.py:434 msgid "No README found for this plugin." msgstr "" #: plugin.py:437 #, fuzzy msgid "This plugin has no description." msgstr "Das Plugin ist in dieser Quelle nicht vorhanden." ��������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/locales/fi.po������������������������������������������0000644�0001750�0001750�00000015106�14535072470�021441� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# PluginDownloader plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-03-22 16:38+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.5.4\n" #: plugin.py:167 msgid "" "Plugin is probably not compatible with your Python version (3.x) and could " "not be converted because 2to3 is not installed." msgstr "" "Lisä-osa ei luultavasti ole yhteensopiva Python-versiosi (3.x) kanssa, eikä " "sitä voitu muuntaa, koska 2to3 ei ole asennettu." #: plugin.py:174 msgid "" "Plugin was designed for Python 2, but an attempt to convert it to Python 3 " "has been made. There is no guarantee it will work, though." msgstr "" "Lisä-osa suunniteltiin Python 2:lle, mutta yritys muuntaa se Python 3:lle on " "tehty. Ei kuitenkaan ole takuuta, että se toimisi." #: plugin.py:178 msgid "Plugin successfully installed." msgstr "Lisä-osa asennettiin onnistuneesti." #: plugin.py:323 msgid "" "\n" " This plugin allows you to install unofficial plugins from\n" " multiple repositories easily. Use the \"repolist\" command to see list " "of\n" " available repositories and \"repolist \" to list plugins,\n" " which are available in that repository. When you want to install a " "plugin,\n" " just run command \"install \".\n" "\n" " First start by using the `plugindownloader repolist` command to see the\n" " available repositories.\n" "\n" " To see the plugins inside repository, use command\n" " `plugindownloader repolist `\n" "\n" " When you see anything interesting, you can use\n" " `plugindownloader info ` to see what the plugin is\n" " for.\n" "\n" " And finally to install the plugin,\n" " `plugindownloader install `.\n" "\n" " Examples\n" " ^^^^^^^^\n" "\n" " ::\n" "\n" " < Mikaela> @load PluginDownloader\n" " < Limnoria> Ok.\n" " < Mikaela> @plugindownloader repolist\n" " < Limnoria> Antibody, jlu5, Hoaas, Iota, progval, SpiderDave, " "boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, " "jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, " "nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, " "stepnem\n" " < Mikaela> @plugindownloader repolist progval\n" " < Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, " "ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, " "Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, " "IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, " "Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, " "Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)\n" " < Mikaela> more\n" " < Limnoria> SilencePlugin, StdoutCapture, Sudo, SupyML, SupySandbox, " "TWSS, Trigger, Trivia, Twitter, TwitterStream, Untiny, Variables, WebDoc, " "WebLogs, WebStats, Website, WikiTrans, Wikipedia, WunderWeather\n" " < Mikaela> @plugindownloader info progval Wikipedia\n" " < Limnoria> Grabs data from Wikipedia.\n" " < Mikaela> @plugindownloader install progval Wikipedia\n" " < Limnoria> Ok.\n" " < Mikaela> @load Wikipedia\n" " < Limnoria> Ok.\n" " " msgstr "" #: plugin.py:368 msgid "" "[]\n" "\n" " Displays the list of plugins in the .\n" " If is not given, returns a list of available\n" " repositories." msgstr "" "[]\n" "\n" " Näyttää listan lisäosista, jotka löytyvät . Jos\n" " ei ole annettu, palauttaa listan kaikista saataville " "olevista\n" " ohjelmistolähteistä." #: plugin.py:376 plugin.py:387 msgid ", " msgstr ", " #: plugin.py:378 plugin.py:400 plugin.py:425 msgid "This repository does not exist or is not known by this bot." msgstr "" "Tämä ohjelmistolähde ei ole olemassakaan tai tämä botti ei tiedä siitä." #: plugin.py:385 msgid "No plugin found in this repository." msgstr "Lisäosaa ei löytynyt tästä ohjelmistolähteestä." #: plugin.py:392 msgid "" " \n" "\n" " Downloads and installs the from the ." msgstr "" " \n" "\n" " Lataa ja asentaa ." #: plugin.py:396 msgid "" "This command is not available, because supybot.commands.allowShell is False." msgstr "" #: plugin.py:405 plugin.py:430 msgid "This plugin does not exist in this repository." msgstr "Tämä lisäosa ei ole tässä ohjelmistolähteessä." #: plugin.py:420 msgid "" " \n" "\n" " Displays informations on the in the ." msgstr "" " \n" "\n" " Näyttää tietoja ." #: plugin.py:434 msgid "No README found for this plugin." msgstr "Tämän lisäosan README-tiedostoa ei löydy." #: plugin.py:437 msgid "This plugin has no description." msgstr "Tällä lisäosalla ei ole kuvausta." #~ msgid "" #~ "This plugin allows you to install unofficial plugins from\n" #~ " multiple repositories easily. Use the \"repolist\" command to see " #~ "list of\n" #~ " available repositories and \"repolist \" to list " #~ "plugins, \n" #~ " which are available in that repository. When you want to install " #~ "plugin,\n" #~ " just run command \"install \"." #~ msgstr "" #~ "Tämä lisäosa sallii epävirallisten lisäosien asentamisen monista " #~ "ohjelmistolähteistä\n" #~ " helposti. Käytä \"repolist\" komentoa nähdäksesi listan saatavilla " #~ "olevista\n" #~ " ohjelmistolähteistä ja \"repolist \" saadaksesi listan " #~ "lisäosista,\n" #~ " jotka ovat saatavilla kyseisessä ohjelmistolähteessä. Kun tahdot asentaa " #~ "lisäosan,\n" #~ " suorita vain komento \"install \"." #~ msgid "" #~ "Add the help for 'plugin help PluginDownloader' here\n" #~ " This should describe *how* to use this plugin." #~ msgstr "" #~ "Lisää ohjeteksti komennolle 'plugin help PluginDownloader' tähän.\n" #~ " Tämän pitäisi kuvata *kuinka* tätä lisäosaa käytetään." ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/locales/fr.po������������������������������������������0000644�0001750�0001750�00000014267�14535072470�021461� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# French translation for limnoria # Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 # This file is distributed under the same license as the limnoria package. # FIRST AUTHOR , 2012. # msgid "" msgstr "" "Project-Id-Version: limnoria\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-01-22 07:52+0100\n" "Last-Translator: \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Launchpad-Export-Date: 2012-04-19 19:44+0000\n" "X-Generator: Poedit 1.5.4\n" #: plugin.py:167 #, fuzzy msgid "" "Plugin is probably not compatible with your Python version (3.x) and could " "not be converted because 2to3 is not installed." msgstr "" "Ce plugin n’est probablement pas compatible avec votre version de Python (3." "x), mais il n’a pas pu être converti car 2to3 n’est pas installé." #: plugin.py:174 msgid "" "Plugin was designed for Python 2, but an attempt to convert it to Python 3 " "has been made. There is no guarantee it will work, though." msgstr "" "Ce plugin a été conçu pour Python 2, mais une tentative de conversion à " "Python 3 a été faite. Cependant, il n’y a pas de garantie que le plugin " "fonctionnera." #: plugin.py:178 msgid "Plugin successfully installed." msgstr "Plugin installé avec succès." #: plugin.py:323 msgid "" "\n" " This plugin allows you to install unofficial plugins from\n" " multiple repositories easily. Use the \"repolist\" command to see list " "of\n" " available repositories and \"repolist \" to list plugins,\n" " which are available in that repository. When you want to install a " "plugin,\n" " just run command \"install \".\n" "\n" " First start by using the `plugindownloader repolist` command to see the\n" " available repositories.\n" "\n" " To see the plugins inside repository, use command\n" " `plugindownloader repolist `\n" "\n" " When you see anything interesting, you can use\n" " `plugindownloader info ` to see what the plugin is\n" " for.\n" "\n" " And finally to install the plugin,\n" " `plugindownloader install `.\n" "\n" " Examples\n" " ^^^^^^^^\n" "\n" " ::\n" "\n" " < Mikaela> @load PluginDownloader\n" " < Limnoria> Ok.\n" " < Mikaela> @plugindownloader repolist\n" " < Limnoria> Antibody, jlu5, Hoaas, Iota, progval, SpiderDave, " "boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, " "jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, " "nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, " "stepnem\n" " < Mikaela> @plugindownloader repolist progval\n" " < Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, " "ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, " "Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, " "IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, " "Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, " "Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)\n" " < Mikaela> more\n" " < Limnoria> SilencePlugin, StdoutCapture, Sudo, SupyML, SupySandbox, " "TWSS, Trigger, Trivia, Twitter, TwitterStream, Untiny, Variables, WebDoc, " "WebLogs, WebStats, Website, WikiTrans, Wikipedia, WunderWeather\n" " < Mikaela> @plugindownloader info progval Wikipedia\n" " < Limnoria> Grabs data from Wikipedia.\n" " < Mikaela> @plugindownloader install progval Wikipedia\n" " < Limnoria> Ok.\n" " < Mikaela> @load Wikipedia\n" " < Limnoria> Ok.\n" " " msgstr "" #: plugin.py:368 msgid "" "[]\n" "\n" " Displays the list of plugins in the .\n" " If is not given, returns a list of available\n" " repositories." msgstr "" "[]\n" "\n" "Affiche une liste des plugins du . Si n'est pas donné, " "retourne une liste des dépôts disponibles." #: plugin.py:376 plugin.py:387 msgid ", " msgstr ", " #: plugin.py:378 plugin.py:400 plugin.py:425 msgid "This repository does not exist or is not known by this bot." msgstr "Ce dépôt n'existe pas ou n'est pas connu de ce bot." #: plugin.py:385 msgid "No plugin found in this repository." msgstr "Aucun plugin trouvé dans ce dépôt." #: plugin.py:392 msgid "" " \n" "\n" " Downloads and installs the from the ." msgstr "" " \n" "\n" "Télécharge et installe le depuis le ." #: plugin.py:396 msgid "" "This command is not available, because supybot.commands.allowShell is False." msgstr "" #: plugin.py:405 plugin.py:430 msgid "This plugin does not exist in this repository." msgstr "Ce plugin n'existe pas dans ce dépôt." #: plugin.py:420 msgid "" " \n" "\n" " Displays informations on the in the ." msgstr "" " \n" "\n" "Affiche des informations sur depuis le ." #: plugin.py:434 msgid "No README found for this plugin." msgstr "Aucun README trouvé pour ce plugin." #: plugin.py:437 msgid "This plugin has no description." msgstr "Ce plugin n'a pas de description." #~ msgid "" #~ "This plugin allows you to install unofficial plugins from\n" #~ " multiple repositories easily. Use the \"repolist\" command to see " #~ "list of\n" #~ " available repositories and \"repolist \" to list " #~ "plugins, \n" #~ " which are available in that repository. When you want to install " #~ "plugin,\n" #~ " just run command \"install \"." #~ msgstr "" #~ "Ce plugin vous permet d'installer des plugins non officiels depuis de " #~ "multiples dépôts facilement. Utilisez la commande \"repolist\" pour avoir " #~ "une liste des dépôts disponibles et \"repolist \" pour lister les " #~ "plugins qui sont disponibles dans le dépôt. Lorsque vous voulez installer " #~ "un plugin, utilisez simplement la commande \"install \"." �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/locales/it.po������������������������������������������0000644�0001750�0001750�00000013137�14535072470�021461� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-05-04 01:17+020\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:167 msgid "" "Plugin is probably not compatible with your Python version (3.x) and could " "not be converted because 2to3 is not installed." msgstr "" #: plugin.py:174 msgid "" "Plugin was designed for Python 2, but an attempt to convert it to Python 3 " "has been made. There is no guarantee it will work, though." msgstr "" #: plugin.py:178 msgid "Plugin successfully installed." msgstr "" #: plugin.py:323 msgid "" "\n" " This plugin allows you to install unofficial plugins from\n" " multiple repositories easily. Use the \"repolist\" command to see list " "of\n" " available repositories and \"repolist \" to list plugins,\n" " which are available in that repository. When you want to install a " "plugin,\n" " just run command \"install \".\n" "\n" " First start by using the `plugindownloader repolist` command to see the\n" " available repositories.\n" "\n" " To see the plugins inside repository, use command\n" " `plugindownloader repolist `\n" "\n" " When you see anything interesting, you can use\n" " `plugindownloader info ` to see what the plugin is\n" " for.\n" "\n" " And finally to install the plugin,\n" " `plugindownloader install `.\n" "\n" " Examples\n" " ^^^^^^^^\n" "\n" " ::\n" "\n" " < Mikaela> @load PluginDownloader\n" " < Limnoria> Ok.\n" " < Mikaela> @plugindownloader repolist\n" " < Limnoria> Antibody, jlu5, Hoaas, Iota, progval, SpiderDave, " "boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, " "jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, " "nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, " "stepnem\n" " < Mikaela> @plugindownloader repolist progval\n" " < Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, " "ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, " "Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, " "IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, " "Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, " "Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)\n" " < Mikaela> more\n" " < Limnoria> SilencePlugin, StdoutCapture, Sudo, SupyML, SupySandbox, " "TWSS, Trigger, Trivia, Twitter, TwitterStream, Untiny, Variables, WebDoc, " "WebLogs, WebStats, Website, WikiTrans, Wikipedia, WunderWeather\n" " < Mikaela> @plugindownloader info progval Wikipedia\n" " < Limnoria> Grabs data from Wikipedia.\n" " < Mikaela> @plugindownloader install progval Wikipedia\n" " < Limnoria> Ok.\n" " < Mikaela> @load Wikipedia\n" " < Limnoria> Ok.\n" " " msgstr "" #: plugin.py:368 msgid "" "[]\n" "\n" " Displays the list of plugins in the .\n" " If is not given, returns a list of available\n" " repositories." msgstr "" "[]\n" "\n" " Mostra l'elenco dei plugin presenti in .\n" " Se non è specificato riporta una lista di quelli " "disponibili." #: plugin.py:376 plugin.py:387 msgid ", " msgstr ", " #: plugin.py:378 plugin.py:400 plugin.py:425 msgid "This repository does not exist or is not known by this bot." msgstr "Questo repository non esiste o non è riconosciuto." #: plugin.py:385 msgid "No plugin found in this repository." msgstr "Nessun plugin trovato in questo repository." #: plugin.py:392 msgid "" " \n" "\n" " Downloads and installs the from the ." msgstr "" " \n" "\n" " Scarica e installa da ." #: plugin.py:396 msgid "" "This command is not available, because supybot.commands.allowShell is False." msgstr "" #: plugin.py:405 plugin.py:430 msgid "This plugin does not exist in this repository." msgstr "Il plugin non esiste in questo repository." #: plugin.py:420 msgid "" " \n" "\n" " Displays informations on the in the ." msgstr "" " \n" "\n" " Visualizza informazioni riguardo in ." #: plugin.py:434 #, fuzzy msgid "No README found for this plugin." msgstr "Non è stato trovato nessun file README per questo plugin." #: plugin.py:437 msgid "This plugin has no description." msgstr "Questo plugin non ha una descrizione." #~ msgid "" #~ "This plugin allows you to install unofficial plugins from\n" #~ " multiple repositories easily. Use the \"repolist\" command to see " #~ "list of\n" #~ " available repositories and \"repolist \" to list " #~ "plugins, \n" #~ " which are available in that repository. When you want to install " #~ "plugin,\n" #~ " just run command \"install \"." #~ msgstr "" #~ "Questo plugin permette di installare facilmente plugin non ufficiali da " #~ "vari\n" #~ " repository. Usare il comando \"repolist\" per ottenere l'elenco dei " #~ "repository\n" #~ " disponibili e \"repolist \" per elencare i plugin " #~ "disponibili nel\n" #~ " dato repository. Per installare il plugin basta eseguire il comando\n" #~ " \"install \"." ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/plugin.py����������������������������������������������0000644�0001750�0001750�00000037663�14535072470�020745� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2011-2021, 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 io import sys import json import shutil import tarfile import supybot.log as log 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.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('PluginDownloader') class Repository: pass class VersionnedRepository(Repository): pass class GitRepository(VersionnedRepository): pass class GithubRepository(GitRepository): def __init__(self, username, reponame, path='/', branch='HEAD'): self._username = username self._reponame = reponame self._branch = branch if not path.startswith('/'): path = '/' + path if not path.endswith('/'): path += '/' self._path = path self._downloadUrl = 'https://github.com/%s/%s/tarball/%s' % \ ( self._username, self._reponame, self._branch ) _apiUrl = 'https://api.github.com' def _query(self, type_, uri_end, args={}): args = dict([(x,y) for x,y in args.items() if y is not None]) url = '%s/%s/%s?%s' % (self._apiUrl, type_, uri_end, utils.web.urlencode(args)) return json.loads(utils.web.getUrl(url).decode('utf8')) def getPluginList(self): plugins = self._query('repos', '%s/%s/contents%s' % (self._username, self._reponame, self._path), args={'ref': self._branch} ) if plugins is None: log.error(( 'Cannot get plugins list from repository %s/%s ' 'at Github' ) % (self._username, self._reponame)) return [] plugins = [x['name'] for x in plugins if x['type'] == 'dir'] return plugins def _download(self, plugin): try: response = utils.web.getUrlFd(self._downloadUrl) if minisix.PY2: assert response.getcode() == 200, response.getcode() else: assert response.status == 200, response.status fileObject = minisix.io.BytesIO() fileObject.write(response.read()) finally: # urllib does not handle 'with' statements :( response.close() fileObject.seek(0) return tarfile.open(fileobj=fileObject, mode='r:gz') def install(self, plugin): archive = self._download(plugin) prefix = archive.getnames()[0] dirname = ''.join((self._path, plugin)) directories = conf.supybot.directories.plugins() directory = self._getWritableDirectoryFromList(directories) assert directory is not None, \ 'No valid directory in supybot.directories.plugins.' possibly_incompatible = False try: assert archive.getmember(prefix + dirname).isdir(), \ 'This is not a valid plugin (it is a file, not a directory).' for file in archive.getmembers(): if file.name.startswith(prefix + dirname): extractedFile = archive.extractfile(file) newFileName = os.path.join(*file.name.split('/')[1:]) newFileName = newFileName[len(self._path)-1:] newFileName = os.path.join(directory, newFileName) if os.path.exists(newFileName): assert os.path.isdir(newFileName), newFileName + \ 'should not be a file.' shutil.rmtree(newFileName) if extractedFile is None: os.mkdir(newFileName) else: with open(newFileName, 'ab') as fd: for line in extractedFile.readlines(): if file.name.endswith('__init__.py') and \ line.startswith((b'import config', b'import plugin')): possibly_incompatible = True fd.write(line) finally: archive.close() del archive if possibly_incompatible: return _('Plugin installed. However, it may be incompatible with ' 'Python 3 and require manual code changes to work correctly.') return _('Plugin successfully installed.') def getInfo(self, plugin): archive = self._download(plugin) prefix = archive.getnames()[0] dirname = ''.join((self._path, plugin)) for file in archive.getmembers(): if file.name.startswith(prefix + dirname + '/README'): extractedFile = archive.extractfile(file) content = extractedFile.read() if minisix.PY3: content = content.decode() return content def _getWritableDirectoryFromList(self, directories): for directory in directories: if os.access(directory, os.W_OK): return directory return None repositories = utils.InsensitivePreservingDict({ 'progval': GithubRepository( 'progval', 'Supybot-plugins' ), 'SpiderDave': GithubRepository( 'SpiderDave', 'spidey-supybot-plugins', 'Plugins', ), 'Hoaas': GithubRepository( 'Hoaas', 'Supybot-plugins' ), 'nyuszika7h': GithubRepository( 'nyuszika7h', 'limnoria-plugins' ), 'frumious': GithubRepository( 'frumiousbandersnatch', 'sobrieti-plugins', 'plugins', ), 'skgsergio': GithubRepository( 'skgsergio', 'Limnoria-plugins', ), 'jlu5': GithubRepository( 'jlu5', 'SupyPlugins', ), 'Iota': GithubRepository( 'IotaSpencer', 'supyplugins', ), 'waratte': GithubRepository( 'waratte', 'supybot', ), 't3chguy': GithubRepository( 't3chguy', 'Limnoria-Plugins', ), 'prgmrbill': GithubRepository( 'prgmrbill', 'limnoria-plugins', ), 'fudster': GithubRepository( 'fudster', 'supybot-plugins', ), 'oddluck': GithubRepository( 'oddluck', 'limnoria-plugins', ), 'appas': GithubRepository( 'matiasw', 'my-limnoria-plugins', ), }) class PluginDownloader(callbacks.Plugin): """ This plugin allows you to install unofficial plugins from multiple repositories easily. Use the "repolist" command to see list of available repositories and "repolist " to list plugins, which are available in that repository. When you want to install a plugin, just run command "install ". First start by using the `plugindownloader repolist` command to see the available repositories. To see the plugins inside repository, use command `plugindownloader repolist ` When you see anything interesting, you can use `plugindownloader info ` to see what the plugin is for. And finally to install the plugin, `plugindownloader install `. Examples ^^^^^^^^ :: < Mikaela> @load PluginDownloader < Limnoria> Ok. < Mikaela> @plugindownloader repolist < Limnoria> Antibody, jlu5, Hoaas, Iota, progval, SpiderDave, boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, stepnem < Mikaela> @plugindownloader repolist progval < Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message) < Mikaela> more < Limnoria> SilencePlugin, StdoutCapture, Sudo, SupyML, SupySandbox, TWSS, Trigger, Trivia, Twitter, TwitterStream, Untiny, Variables, WebDoc, WebLogs, WebStats, Website, WikiTrans, Wikipedia, WunderWeather < Mikaela> @plugindownloader info progval Wikipedia < Limnoria> Grabs data from Wikipedia. < Mikaela> @plugindownloader install progval Wikipedia < Limnoria> Ok. < Mikaela> @load Wikipedia < Limnoria> Ok. """ threaded = True @internationalizeDocstring def repolist(self, irc, msg, args, repository): """[] Displays the list of plugins in the . If is not given, returns a list of available repositories.""" global repositories if repository is None: irc.reply(_(', ').join(sorted(x for x in repositories))) elif repository not in repositories: irc.error(_( 'This repository does not exist or is not known by ' 'this bot.' )) else: plugins = repositories[repository].getPluginList() if plugins == []: irc.error(_('No plugin found in this repository.')) else: irc.reply(_(', ').join([x for x in plugins])) repolist = wrap(repolist, [optional('something')]) @internationalizeDocstring def install(self, irc, msg, args, repository, plugin): """ Downloads and installs the from the .""" if not conf.supybot.commands.allowShell(): irc.error(_('This command is not available, because ' 'supybot.commands.allowShell is False.'), Raise=True) global repositories if repository not in repositories: irc.error(_( 'This repository does not exist or is not known by ' 'this bot.' )) elif plugin not in repositories[repository].getPluginList(): irc.error(_('This plugin does not exist in this repository.')) else: try: irc.reply(repositories[repository].install(plugin)) except Exception as e: import traceback traceback.print_exc() log.error(str(e)) irc.error('The plugin could not be installed. Check the logs ' 'for a more detailed error.') install = wrap(install, ['owner', 'something', 'something']) @internationalizeDocstring def info(self, irc, msg, args, repository, plugin): """ Displays informations on the in the .""" global repositories if repository not in repositories: irc.error(_( 'This repository does not exist or is not known by ' 'this bot.' )) elif plugin not in repositories[repository].getPluginList(): irc.error(_('This plugin does not exist in this repository.')) else: info = repositories[repository].getInfo(plugin) if info is None: irc.error(_('No README found for this plugin.')) else: if info.startswith('Insert a description of your plugin here'): irc.error(_('This plugin has no description.')) else: info = info.split('\n\n')[0] irc.reply(info.replace('\n', ' ')) info = wrap(info, ['something', optional('something')]) PluginDownloader = internationalizeDocstring(PluginDownloader) Class = PluginDownloader # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/PluginDownloader/test.py������������������������������������������������0000644�0001750�0001750�00000007407�14535072470�020417� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2011-2021, 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 shutil from supybot.test import * pluginsPath = '%s/test-plugins' % os.getcwd() class PluginDownloaderTestCase(PluginTestCase): plugins = ('PluginDownloader',) config = {'supybot.directories.plugins': [pluginsPath]} def setUp(self): PluginTestCase.setUp(self) try: shutil.rmtree(pluginsPath) except: pass os.mkdir(pluginsPath) def tearDown(self): try: shutil.rmtree(pluginsPath) finally: PluginTestCase.tearDown(self) def _testPluginInstalled(self, name): assert os.path.isdir(pluginsPath + '/%s/' % name) assert os.path.isfile(pluginsPath + '/%s/plugin.py' % name) assert os.path.isfile(pluginsPath + '/%s/config.py' % name) def testRepolist(self): self.assertRegexp('repolist', '(.*, )?progval(, .*)?') self.assertRegexp('repolist', '(.*, )?jlu5(, .*)?') self.assertRegexp('repolist progval', '(.*, )?AttackProtector(, .*)?') def testInstallprogval(self): self.assertError('plugindownloader install progval Darcs') self.assertNotError('plugindownloader install progval AttackProtector') self.assertError('plugindownloader install progval Darcs') self._testPluginInstalled('AttackProtector') def testShellForbidden(self): with conf.supybot.commands.allowShell.context(False): self.assertRegexp('plugindownloader install progval Darcs', 'Error:.*not available.*supybot.commands.allowShell') def testInstallNonAsciiInit(self): self.assertNotError('plugindownloader install Hoaas DuckDuckGo') self._testPluginInstalled('DuckDuckGo') def testInstallLegacyWarning(self): self.assertRegexp('plugindownloader install frumious Codepoints', 'may be incompatible') def testInfo(self): self.assertResponse('plugindownloader info progval Twitter', 'Advanced Twitter plugin for Supybot, with capabilities ' 'handling, and per-channel user account.') if not network: class PluginDownloaderTestCase(PluginTestCase): pass # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Poll/�������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014511� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Poll/__init__.py��������������������������������������������������������0000644�0001750�0001750�00000004724�14535072470�016627� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2021, 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. ### """ Poll: Provides a simple way to vote on answers to a question """ import sys import supybot from supybot import world # Use this for the version of this plugin. __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 importlib 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: ��������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Poll/config.py����������������������������������������������������������0000644�0001750�0001750�00000005552�14535072470�016335� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2021, 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 import conf, registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization("Poll") 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("Poll", True) Poll = conf.registerPlugin("Poll") # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Poll, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue( Poll, "requireManageCapability", registry.String( "channel,op; channel,halfop", _( """Determines the capabilities required (if any) to open and close polls. Use 'channel,capab' for channel-level capabilities. Note that absence of an explicit anticapability means user has capability. """ ), ), ) ������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Poll/plugin.py����������������������������������������������������������0000644�0001750�0001750�00000020536�14535072470�016365� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2021, 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 collections import re from supybot import utils, plugins, ircdb, ircutils, callbacks from supybot.commands import * try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization("Poll") except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x Poll = collections.namedtuple("Poll", "question answers votes open") class Poll_(callbacks.Plugin): """Provides a simple way to vote on answers to a question For example, this creates a poll:: @poll add "Is this a test?" "Yes" "No" "Maybe" The operation succeeded. Poll # 42 created. Creates a poll that can be voted on in this way:: @vote 42 Yes @vote 42 No @vote 42 No And results:: @poll results 2 votes for No, 1 vote for Yes, and 0 votes for Maybe Longer answers are possible, and voters only need to use the first word of each answer to vote. For example, this creates a poll that can be voted on in the same way:: @poll add "Is this a test?" "Yes totally" "No no no" "Maybe" The operation succeeded. Poll # 43 created. You can also add a number or letter at the beginning of each question to make it easier:: @poll add "Who is the best captain?" "1 James T Kirk" "2 Jean-Luc Picard" "3 Benjamin Sisko" "4 Kathryn Janeway" The operation succeeded. Poll # 44 created. @vote 42 1 @vote 42 4 @vote 42 4 """ def __init__(self, irc): super().__init__(irc) # {(network, channel): {id: Poll}} self._polls = collections.defaultdict(dict) def name(self): return "Poll" def _checkManageCapability(self, irc, msg, channel): # Copy-pasted from Topic capabilities = self.registryValue( "requireManageCapability", channel, irc.network ) 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 irc.errorNoCapability(capabilities, Raise=True) def _getPoll(self, irc, channel, poll_id): poll = self._polls[(irc.network, channel)].get(poll_id) if poll is None: irc.error( _("A poll with this ID does not exist in this channel."), Raise=True, ) return poll @wrap(["channel", "something", many("something")]) def add(self, irc, msg, args, channel, question, answers): """[] [ [ [...]]] Creates a new poll with the specified and answers on the . The first word of each answer is used as its id to vote, so each answer should start with a different word. is only necessary if this command is run in private, and defaults to the current channel otherwise.""" self._checkManageCapability(irc, msg, channel) poll_id = max(self._polls[(irc.network, channel)], default=0) + 1 answers = [(answer.split()[0].casefold(), answer) for answer in answers] answer_id_counts = collections.Counter( id_ for (id_, _) in answers ).items() duplicate_answer_ids = [ answer_id for (answer_id, count) in answer_id_counts if count > 1 ] if duplicate_answer_ids: irc.error( format( _("Duplicate answer identifier(s): %L"), duplicate_answer_ids, ), Raise=True, ) self._polls[(irc.network, channel)][poll_id] = Poll( question=question, answers=dict(answers), votes=ircutils.IrcDict(), open=True, ) irc.replySuccess(_("Poll # %d created.") % poll_id) @wrap(["channel", "positiveInt"]) def close(self, irc, msg, args, channel, poll_id): """[] Closes the specified poll.""" self._checkManageCapability(irc, msg, channel) poll = self._getPoll(irc, channel, poll_id) if not poll.open: irc.error(_("This poll was already closed."), Raise=True) poll = Poll( question=poll.question, answers=poll.answers, votes=poll.votes, open=False, ) self._polls[(irc.network, channel)][poll_id] = poll irc.replySuccess() @wrap(["channel", "positiveInt", "somethingWithoutSpaces"]) def vote(self, irc, msg, args, channel, poll_id, answer_id): """[] Registers your vote on the poll as being the answer identified by (which is the first word of each possible answer).""" poll = self._getPoll(irc, channel, poll_id) if not poll.open: irc.error(_("This poll is closed."), Raise=True) if msg.nick in poll.votes: irc.error(_("You already voted on this poll."), Raise=True) answer_id = answer_id.casefold() if answer_id not in poll.answers: irc.error( format( _("Invalid answer ID. Valid answers are: %L"), poll.answers, ), Raise=True, ) poll.votes[msg.nick] = answer_id irc.replySuccess() @wrap(["channel", "positiveInt"]) def results(self, irc, msg, args, channel, poll_id): """[] Returns the results of the specified poll.""" poll = self._getPoll(irc, channel, poll_id) counts = collections.Counter(poll.votes.values()) # Add answers with 0 votes counts.update({answer_id: 0 for answer_id in poll.answers}) results = [ format(_("%n for %s"), (v, _("vote")), poll.answers[k].split()[0]) for (k, v) in counts.most_common() ] irc.replies(results) @wrap(["channel"]) def list(self, irc, msg, args, channel): """[] Lists open polls in the .""" results = [ format( _("%i: %s (%n)"), poll_id, poll.question, (len(poll.votes), _("vote")), ) for (poll_id, poll) in self._polls[(irc.network, channel)].items() if poll.open ] if results: irc.replies(results) else: irc.reply(_("There are no open polls.")) Class = Poll_ ������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Poll/test.py������������������������������������������������������������0000644�0001750�0001750�00000015312�14535072470�016042� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2021, 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 * class PollTestCase(ChannelPluginTestCase): plugins = ("Poll",) def testBasics(self): self.assertResponse( 'poll add "Is this a test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertNotError("vote 1 Yes", frm="voter1!foo@bar") self.assertNotError("vote 1 No", frm="voter2!foo@bar") self.assertNotError("vote 1 No", frm="voter3!foo@bar") self.assertResponse( "results 1", "2 votes for No, 1 vote for Yes, and 0 votes for Maybe", ) def testNoResults(self): self.assertResponse( 'poll add "Is this a test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertResponse( "results 1", "0 votes for Yes, 0 votes for No, and 0 votes for Maybe", ) def testDoubleVoting(self): self.assertResponse( 'poll add "Is this a test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertNotError("vote 1 Yes", frm="voter1!foo@bar") self.assertNotError("vote 1 No", frm="voter2!foo@bar") self.assertResponse( "vote 1 Yes", "voter1: Error: You already voted on this poll.", frm="voter1!foo@bar", ) self.assertResponse( "vote 1 Yes", "VOTER1: Error: You already voted on this poll.", frm="VOTER1!foo@bar", ) self.assertRegexp( "results 1", "1 vote for (Yes|No), 1 vote for (Yes|No), and 0 votes for Maybe", ) def testClosed(self): self.assertResponse( 'poll add "Is this a test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertNotError("vote 1 Yes", frm="voter1!foo@bar") self.assertNotError("vote 1 No", frm="voter2!foo@bar") self.assertNotError("close 1") self.assertResponse( "vote 1 Yes", "voter3: Error: This poll is closed.", frm="voter3!foo@bar", ) self.assertRegexp("close 1", "already closed") self.assertRegexp( "results 1", "1 vote for (Yes|No), 1 vote for (Yes|No), and 0 votes for Maybe", ) def testNonExisting(self): self.assertResponse( 'poll add "Is this a test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertRegexp("vote 2 Yes", "does not exist") def testLongAnswers(self): self.assertResponse( 'poll add "Is this a test?" "Yes totally" "No no no" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertNotError("vote 1 Yes", frm="voter1!foo@bar") self.assertNotError("vote 1 No", frm="voter2!foo@bar") self.assertNotError("vote 1 No", frm="voter3!foo@bar") self.assertResponse( "results 1", "2 votes for No, 1 vote for Yes, and 0 votes for Maybe", ) def testDuplicateId(self): self.assertResponse( 'poll add "Is this a test?" "Yes" "Yes" "Maybe"', "Error: Duplicate answer identifier(s): yes", ) self.assertResponse( 'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"', "Error: Duplicate answer identifier(s): yes", ) def testCaseInsensitive(self): self.assertResponse( 'poll add "Is this a test?" "Yeß" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertNotError("vote 1 Yeß", frm="voter1!foo@bar") self.assertNotError("vote 1 yESS", frm="voter2!foo@bar") self.assertNotError("vote 1 no", frm="voter3!foo@bar") self.assertResponse( "results 1", "2 votes for Yeß, 1 vote for No, and 0 votes for Maybe", ) def testList(self): self.assertResponse("poll list", "There are no open polls.") self.assertResponse( 'poll add "Is this a test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 1 created.", ) self.assertResponse("poll list", "1: Is this a test? (0 votes)") self.assertNotError("vote 1 Yes", frm="voter1!foo@bar") self.assertResponse("poll list", "1: Is this a test? (1 vote)") self.assertNotError("vote 1 No", frm="voter2!foo@bar") self.assertResponse("poll list", "1: Is this a test? (2 votes)") self.assertResponse( 'poll add "Is this another test?" "Yes" "No" "Maybe"', "The operation succeeded. Poll # 2 created.", ) self.assertResponse( "poll list", "1: Is this a test? (2 votes) and 2: Is this another test? (0 votes)", ) self.assertNotError("poll close 1") self.assertResponse( "poll list", "2: Is this another test? (0 votes)", ) self.assertNotError("poll close 2") self.assertResponse("poll list", "There are no open polls.") ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/�����������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015026� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/__init__.py������������������������������������������������������0000644�0001750�0001750�00000004740�14535072470�017142� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ Hand out praise to IRC denizens with this plugin. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ��������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/config.py��������������������������������������������������������0000644�0001750�0001750�00000005207�14535072470�016647� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Praise') 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('Praise', True) Praise = conf.registerPlugin('Praise') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Praise, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue(Praise, 'showIds', registry.Boolean(False, _("""Determines whether the bot will show the ids of a praise when the praise is given."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/locales/���������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016450� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/locales/fi.po����������������������������������������������������0000644�0001750�0001750�00000004764�14535072470�017417� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-24 15:36+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: config.py:50 msgid "" "Determines whether the bot will show the ids of\n" " a praise when the praise is given." msgstr "" "Määrittää näyttääkö botti ylistysten id:t, kun\n" " ylistys annetaan." #: plugin.py:40 #, fuzzy msgid "" "\n" " Praise is a plugin for ... well, praising things. Feel free to add\n" " your own flavor to it by customizing what praises it gives. Use " "\"praise\n" " add \" to add new ones, making sure to include \"$who\" in " "where\n" " you want to insert the thing being praised.\n" "\n" " Example:\n" "\n" " * If you add ``hugs $who``\n" " * Someone says ``@praise ChanServ``.\n" " * ``* bot hugs ChanServ``\n" " " msgstr "" "Praise on lisäosa ... no, ylistämään asioita. Lisää vapaasti\n" " omaa suosiotasi muokkaamalla mitä ylistyksiä se antaa. Käytä komentoa " "\"praise\n" " add \" lisätäksesi uusia ylistyksiä, varmistaen, että " "sisällytät \"$who\"n mihin\n" " haluat laittaa ylistettävän asian.\n" " " #: plugin.py:61 msgid "Praises must contain $who." msgstr "Ylistyksien täytyy sisältää $who." #: plugin.py:65 msgid "" "[] [] [for ]\n" "\n" " Praises (for , if given). If is given, " "uses\n" " that specific praise. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [] [for ]\n" "\n" " Ylistää (, jos annettu). Jos on annettu, " "käyttää\n" " juuri sitä tiettyä ylistystä. on vaadittu vain jos viestiä " "ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:81 msgid "There is no praise with id #%i." msgstr "Ylistystä id:llä #%i ei löydy." #: plugin.py:86 msgid "There are no praises in my database for %s." msgstr "Minun tietokannassani ei ole ylistyksiä %s:lle." #: plugin.py:94 msgid " for " msgstr "varten" ������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/locales/fr.po����������������������������������������������������0000644�0001750�0001750�00000004570�14535072470�017423� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:50 msgid "" "Determines whether the bot will show the ids of\n" " a praise when the praise is given." msgstr "" "Détermine si le bot affichera les ids des louanges lorsqu'une louange est " "donnée." #: plugin.py:40 #, fuzzy msgid "" "\n" " Praise is a plugin for ... well, praising things. Feel free to add\n" " your own flavor to it by customizing what praises it gives. Use " "\"praise\n" " add \" to add new ones, making sure to include \"$who\" in " "where\n" " you want to insert the thing being praised.\n" "\n" " Example:\n" "\n" " * If you add ``hugs $who``\n" " * Someone says ``@praise ChanServ``.\n" " * ``* bot hugs ChanServ``\n" " " msgstr "" "Praise est un plugin pour... mmh... louer des gens et des choses. Sentez-" "vous libre de personnaliser à votre goût pourquoi vous louez ceci. Utilisez " "\"praise add\" pour ajouter les vôtres, et assurez-vous d'ajouter \"$who\" " "dans le là où vous voulez insérer le nom de la chose à louer." #: plugin.py:61 msgid "Praises must contain $who." msgstr "Les louanges doivent contenir $who" #: plugin.py:65 msgid "" "[] [] [for ]\n" "\n" " Praises (for , if given). If is given, " "uses\n" " that specific praise. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [] [for ]\n" "\n" "Loue (pour la , si elle est donnée). Si l' est donné, " "utilise une louange spécifique. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:81 msgid "There is no praise with id #%i." msgstr "Il n'y a pas de louange d'id #%i" #: plugin.py:86 msgid "There are no praises in my database for %s." msgstr "Il n'y a pas de louange dans ma base de données pour %s." #: plugin.py:94 msgid " for " msgstr " pour " ����������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/locales/it.po����������������������������������������������������0000644�0001750�0001750�00000004460�14535072470�017426� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-15 09:54+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 the bot will show the ids of\n" " a praise when the praise is given." msgstr "" "Determina se il bot mostrerà gli id\n" " di un elogio quando questo viene dato." #: plugin.py:40 #, fuzzy msgid "" "\n" " Praise is a plugin for ... well, praising things. Feel free to add\n" " your own flavor to it by customizing what praises it gives. Use " "\"praise\n" " add \" to add new ones, making sure to include \"$who\" in " "where\n" " you want to insert the thing being praised.\n" "\n" " Example:\n" "\n" " * If you add ``hugs $who``\n" " * Someone says ``@praise ChanServ``.\n" " * ``* bot hugs ChanServ``\n" " " msgstr "" "Praise è un plugin per ... beh, elogiare cose. Sentiti libero/a di " "personalizzarlo\n" " modificando gli elogi; utilizzando \"praise\" add \" per " "aggiungerne\n" " di nuovi e accertandoti di includere \"$who\" alla posizione del " "in cui\n" " desideri che il soggetto venga elogiato.\n" " " #: plugin.py:61 msgid "Praises must contain $who." msgstr "Gli elogi devono contenere $who." #: plugin.py:65 msgid "" "[] [] [for ]\n" "\n" " Praises (for , if given). If is given, " "uses\n" " that specific praise. is only necessary if the message " "isn't\n" " sent in the channel itself.\n" " " msgstr "" "[] [] [per ]\n" "\n" " Elogia (per il , se fornito). Se viene dato, " "usa\n" " quello specifico elogio. è necessario solo se il messaggio " "non\n" " viene inviato nel canale stesso.\n" " " #: plugin.py:81 msgid "There is no praise with id #%i." msgstr "Non c'è nessun elogio con l'id #%i." #: plugin.py:86 msgid "There are no praises in my database for %s." msgstr "Non ci sono elogi per %s nel mio database." #: plugin.py:94 msgid " for " msgstr " per" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/plugin.py��������������������������������������������������������0000644�0001750�0001750�00000010374�14535072470�016701� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Praise') class Praise(plugins.ChannelIdDatabasePlugin): """ Praise is a plugin for ... well, praising things. Feel free to add your own flavor to it by customizing what praises it gives. Use "praise add " to add new ones, making sure to include "$who" in where you want to insert the thing being praised. Example: * If you add ``hugs $who`` * Someone says ``@praise ChanServ``. * ``* bot hugs ChanServ`` """ _meRe = re.compile(r'\bme\b', re.I) _myRe = re.compile(r'\bmy\b', re.I) def _replaceFirstPerson(self, s, nick): s = self._meRe.sub(nick, s) s = self._myRe.sub('%s\'s' % nick, s) return s def addValidator(self, irc, text): if '$who' not in text: irc.error(_('Praises must contain $who.'), Raise=True) @internationalizeDocstring def praise(self, irc, msg, args, channel, id, text): """[] [] [for ] Praises (for , if given). If is given, uses that specific praise. is only necessary if the message isn't sent in the channel itself. """ if ' for ' in text: (target, reason) = list(map(str.strip, text.split(' for ', 1))) else: (target, reason) = (text, '') if ircutils.strEqual(target, irc.nick): target = 'itself' if id is not None: try: praise = self.db.get(channel, id) except KeyError: irc.error(format(_('There is no praise with id #%i.'), id)) return else: praise = self.db.random(channel) if not praise: irc.error(format(_('There are no praises in my database ' \ 'for %s.'), channel)) return text = self._replaceFirstPerson(praise.text, msg.nick) reason = self._replaceFirstPerson(reason, msg.nick) target = self._replaceFirstPerson(target, msg.nick) text = text.replace('$who', target) if reason: text += _(' for ') + reason if self.registryValue('showIds', channel, irc.network): text += format(' (#%i)', praise.id) irc.reply(text, action=True) praise = wrap(praise, ['channeldb', optional('id'), 'text']) Praise = internationalizeDocstring(Praise) Class = Praise # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Praise/test.py����������������������������������������������������������0000644�0001750�0001750�00000003641�14535072470�016361� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 * class PraiseTestCase(ChannelPluginTestCase): plugins = ('Praise',) def testAdd(self): self.assertError('praise add foo') # needs $who def testPraise(self): self.assertError('praise foo') # no praises! # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/��������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015564� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/__init__.py���������������������������������������������������0000644�0001750�0001750�00000005042�14535072470�017674� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Defends a channel against actions by people who don't have the proper capabilities, even if they have +o or +h. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/config.py�����������������������������������������������������0000644�0001750�0001750�00000005525�14535072470�017410� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.ircutils as ircutils import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Protector') 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('Protector', True) Protector = conf.registerPlugin('Protector') conf.registerChannelValue(Protector, 'enable', registry.Boolean(False, _("""Determines whether this plugin is enabled in a given channel."""))) class ImmuneNicks(conf.ValidNicks): List = ircutils.IrcSet sorted = True conf.registerChannelValue(Protector, 'immune', ImmuneNicks([], _("""Determines what nicks the bot will consider to be immune from enforcement. These nicks will not even have their actions watched by this plugin. In general, only the ChanServ for this network will be in this list."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/locales/������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�017206� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/locales/fi.po�������������������������������������������������0000644�0001750�0001750�00000003132�14535072470�020141� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. msgid "" msgstr "" "Project-Id-Version: Protector plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:09+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:48 msgid "" "Determines whether this plugin is enabled in a\n" " given channel." msgstr "" "Määrittää onko tämä lisäosa käytössä\n" " annetulla kanavalla." #: config.py:55 msgid "" "Determines what nicks the bot will consider to\n" " be immune from enforcement. These nicks will not even have their " "actions\n" " watched by this plugin. In general, only the ChanServ for this network\n" " will be in this list." msgstr "" "Määrittää nimimerkit, jotka botti ajattelee\n" " immuuneiksi tästä pakotuksesta. Näiden nimimerkkien toiminnot jäävät\n" " tämän lisäosan valvonnan ulkopuolisiksi. Yleisesti, vain tämän verkon " "ChanServ tulee olemaan\n" " tässä listassa." #: plugin.py:40 msgid "" "Prevents users from doing things they are not supposed to do on a channel,\n" " even if they have +o or +h." msgstr "" "Estää käyttäjiä tekemästä asioita, joita heidän ei ole tarkoitus tehdä " "kanavalla,\n" " vaikka heillä on +o tai +h." ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/locales/fr.po�������������������������������������������������0000644�0001750�0001750�00000002335�14535072470�020156� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:48 msgid "" "Determines whether this plugin is enabled in a\n" " given channel." msgstr "Détermine si le plugin est activé sur un canal donné." #: config.py:55 msgid "" "Determines what nicks the bot will consider to\n" " be immune from enforcement. These nicks will not even have their " "actions\n" " watched by this plugin. In general, only the ChanServ for this network\n" " will be in this list." msgstr "" "Détermine quels nicks le bot considérera comme imunisés. Les actions de ces " "nicks ne seront pas surveillés par ce plugin. En général, seul le ChanServ " "de ce réseau doit être dans cette liste." #: plugin.py:40 msgid "" "Prevents users from doing things they are not supposed to do on a channel,\n" " even if they have +o or +h." msgstr "" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/locales/it.po�������������������������������������������������0000644�0001750�0001750�00000002201�14535072470�020153� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-07-25 23:33+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:48 msgid "" "Determines whether this plugin is enabled in a\n" " given channel." msgstr "Determina se il plugin è abilitato in un dato canale." #: config.py:55 msgid "" "Determines what nicks the bot will consider to\n" " be immune from enforcement. These nicks will not even have their " "actions\n" " watched by this plugin. In general, only the ChanServ for this network\n" " will be in this list." msgstr "" "Determina quali nick il bot considererà immuni, le azioni di questi nick " "non\n" " verranno monitorate dal plugin. In linea di massima sarà presente nella " "lista\n" " solo il ChanServ della rete." #: plugin.py:40 msgid "" "Prevents users from doing things they are not supposed to do on a channel,\n" " even if they have +o or +h." msgstr "" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/plugin.py�����������������������������������������������������0000644�0001750�0001750�00000016656�14535072470�017450� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.utils as utils import supybot.ircdb as ircdb import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Protector') class Protector(callbacks.Plugin): """Prevents users from doing things they are not supposed to do on a channel, even if they have +o or +h.""" def isImmune(self, irc, msg): if not ircutils.isUserHostmask(msg.prefix): self.log.debug('%q is immune, it\'s a server.', msg) return True # It's a server prefix. if ircutils.strEqual(msg.nick, irc.nick): self.log.debug('%q is immune, it\'s me.', msg) return True # It's the bot itself. if msg.nick in self.registryValue('immune', msg.channel, irc.network): self.log.debug('%q is immune, it\'s configured to be immune.', msg) return True return False def isOp(self, irc, channel, hostmask): cap = ircdb.makeChannelCapability(channel, 'op') if ircdb.checkCapability(hostmask, cap): self.log.debug('%s is an op on %s, it has %s.', hostmask, channel, cap) return True if ircutils.strEqual(hostmask, irc.prefix): return True return False def isProtected(self, irc, channel, hostmask): cap = ircdb.makeChannelCapability(channel, 'protected') if ircdb.checkCapability(hostmask, cap): self.log.debug('%s is protected on %s, it has %s.', hostmask, channel, cap) return True if ircutils.strEqual(hostmask, irc.prefix): return True return False def demote(self, irc, channel, nick): irc.queueMsg(ircmsgs.deop(channel, nick)) def __call__(self, irc, msg): def ignore(reason): self.log.debug('Ignoring %q, %s.', msg, reason) if not msg.args: ignore('no msg.args') elif not msg.channel: ignore('not on a channel') elif not self.registryValue('enable', msg.channel, irc.network): ignore('supybot.plugins.Protector.enable is False.') elif msg.channel not in irc.state.channels: # One has to wonder how this would happen, but just in case... ignore('bot isn\'t in channel') elif irc.nick not in irc.state.channels[msg.channel].ops: ignore('bot is not opped') elif msg.nick not in irc.state.channels[msg.channel].users: ignore('sender is not in channel (ChanServ, maybe?)') elif msg.nick not in irc.state.channels[msg.channel].ops: ignore('sender is not an op in channel (IRCOP, maybe?)') elif self.isImmune(irc, msg): ignore('sender is immune') else: super(Protector, self).__call__(irc, msg) def doMode(self, irc, msg): channel = msg.channel chanOp = ircdb.makeChannelCapability(channel, 'op') chanVoice = ircdb.makeChannelCapability(channel, 'voice') chanHalfOp = ircdb.makeChannelCapability(channel, 'halfop') if not ircdb.checkCapability(msg.prefix, chanOp): irc.sendMsg(ircmsgs.deop(channel, msg.nick)) for (mode, value) in ircutils.separateModes(msg.args[1:]): if not value: continue if ircutils.strEqual(value, msg.nick): # We allow someone to mode themselves to oblivion. continue if irc.isNick(value): hostmask = irc.state.nickToHostmask(value) if mode == '+o': if not self.isOp(irc, channel, hostmask): irc.queueMsg(ircmsgs.deop(channel, value)) elif mode == '+h': if not ircdb.checkCapability(hostmask, chanHalfOp): irc.queueMsg(ircmsgs.dehalfop(channel, value)) elif mode == '+v': if not ircdb.checkCapability(hostmask, chanVoice): irc.queueMsg(ircmsgs.devoice(channel, value)) elif mode == '-o': if ircdb.checkCapability(hostmask, chanOp): irc.queueMsg(ircmsgs.op(channel, value)) elif mode == '-h': if ircdb.checkCapability(hostmask, chanOp): irc.queueMsg(ircmsgs.halfop(channel, value)) elif mode == '-v': if ircdb.checkCapability(hostmask, chanOp): irc.queueMsg(ircmsgs.voice(channel, value)) else: assert ircutils.isUserHostmask(value) # Handle bans. def doKick(self, irc, msg): channel = msg.channel kicked = msg.args[1].split(',') protected = [] for nick in kicked: if ircutils.strEqual(nick, irc.nick): return # Channel will handle the rejoin. for nick in kicked: hostmask = irc.state.nickToHostmask(nick) if self.isProtected(irc, channel, hostmask): self.log.info('%s was kicked from %s and is protected; ' 'inviting back.', hostmask, channel) hostmask = '%s!%s' % (nick, irc.state.nickToHostmask(nick)) protected.append(nick) bans = [] for banmask in irc.state.channels[channel].bans: if ircutils.hostmaskPatternEqual(banmask, hostmask): bans.append(banmask) irc.queueMsg(ircmsgs.unbans(channel, bans)) irc.queueMsg(ircmsgs.invite(nick, channel)) if not self.isOp(irc, channel, msg.prefix): self.demote(irc, channel, msg.nick) Class = Protector # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ����������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Protector/test.py�������������������������������������������������������0000644�0001750�0001750�00000003375�14535072470�017123� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class ProtectorTestCase(PluginTestCase): plugins = ('Protector',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014700� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/__init__.py�������������������������������������������������������0000644�0001750�0001750�00000004734�14535072470�017017� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ Maintains a Quotes database for each channel. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/config.py���������������������������������������������������������0000644�0001750�0001750�00000004727�14535072470�016527� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Quote') 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('Quote', True) Quote = conf.registerPlugin('Quote') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Quote, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �����������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/locales/����������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�016322� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/locales/fi.po�����������������������������������������������������0000644�0001750�0001750�00000003244�14535072470�017261� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Quote plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:07+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" #: plugin.py:37 msgid "This plugin allows you to add quotes to the database for a channel." msgstr "Tämä plugin sallii lainausten lisäämisen kanavan tietokantaan." #: plugin.py:40 msgid "" "[]\n" "\n" " Returns a random quote from . is only necessary " "if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa satunnaisen lainauksen . on vaadittu " "vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:49 msgid "I have no quotes in my database for %s." msgstr "Minun tietokannassani ei ole lainauksia kanavalla %s." #: plugin.py:53 #, fuzzy msgid "" "[] \n" " Replace quote with . is only necessary if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa satunnaisen lainauksen . on vaadittu " "vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/locales/fr.po�����������������������������������������������������0000644�0001750�0001750�00000002612�14535072470�017270� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:37 msgid "This plugin allows you to add quotes to the database for a channel." msgstr "" #: plugin.py:40 msgid "" "[]\n" "\n" " Returns a random quote from . is only necessary " "if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne une citation aléatoire du . n'est nécessaire que si " "le message n'est pas envoyé sur le canal lui-même." #: plugin.py:49 msgid "I have no quotes in my database for %s." msgstr "Je n'ai pas de citation dans ma base de données pour %s." #: plugin.py:53 #, fuzzy msgid "" "[] \n" " Replace quote with . is only necessary if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne une citation aléatoire du . n'est nécessaire que si " "le message n'est pas envoyé sur le canal lui-même." ����������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/locales/it.po�����������������������������������������������������0000644�0001750�0001750�00000002534�14535072470�017300� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 18:39+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:37 msgid "This plugin allows you to add quotes to the database for a channel." msgstr "" #: plugin.py:40 msgid "" "[]\n" "\n" " Returns a random quote from . is only necessary " "if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Restituisce una citazione casuale da . è necessario " "solo\n" " se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:49 msgid "I have no quotes in my database for %s." msgstr "Non ho citazioni per %s nel mio database." #: plugin.py:53 #, fuzzy msgid "" "[] \n" " Replace quote with . is only necessary if\n" " the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Restituisce una citazione casuale da . è necessario " "solo\n" " se il messaggio non viene inviato nel canale stesso.\n" " " ��������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/plugin.py���������������������������������������������������������0000644�0001750�0001750�00000006042�14535072470�016550� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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.commands import * import supybot.plugins as plugins from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Quote') class Quote(plugins.ChannelIdDatabasePlugin): """This plugin allows you to add quotes to the database for a channel.""" @internationalizeDocstring def random(self, irc, msg, args, channel): """[] Returns a random quote from . is only necessary if the message isn't sent in the channel itself. """ quote = self.db.random(channel) if quote: irc.reply(self.showRecord(quote)) else: irc.error(_('I have no quotes in my database for %s.') % channel) random = wrap(random, ['channeldb']) def replace(self, irc, msg, args, user, channel, id, text): """[] Replace quote with . is only necessary if the message isn't sent in the channel itself. """ try: record = self.db.get(channel, id) self.checkChangeAllowed(irc, msg, channel, user, record) record.text = text self.db.set(channel, id, record) irc.replySuccess() except KeyError: self.noSuchRecord(irc, channel, id) replace = wrap(replace, ['user', 'channeldb', 'id', 'text']) Class = Quote # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/Quote/test.py�����������������������������������������������������������0000644�0001750�0001750�00000004755�14535072470�016242� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 * class QuoteTestCase(ChannelPluginTestCase): plugins = ('Quote', 'User') def testReplace(self): self.feedMsg('register testuser moo', to=self.nick, frm=self.prefix) _ = self.getMsg(' ') self.assertNotError("quote add hello") self.assertNotError("quote replace 1 goodbye") self.assertRegexp("quote get 1", "goodbye") self.assertError("quote replace 5 afsdafas") # non-existant def testUnauthenticatedAdd(self): # This should fail because the user isn't registered self.assertError('quote add hello world!') with conf.supybot.databases.plugins.requireRegistration.context(False): self.assertNotError('quote add hello world!') self.assertRegexp('quote get 1', 'hello') self.assertNotError('quote remove 1') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/�������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015657� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/__init__.py��������������������������������������������������0000644�0001750�0001750�00000005163�14535072470�017773� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ Quotegrabs are like IRC sound bites. When someone says something funny, incriminating, stupid, outrageous, ... anything that might be worth remembering, you can grab that quote for that person. With this plugin, you can store many quotes per person and display their most recent quote, as well as see who "grabbed" the quote in the first place. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/config.py����������������������������������������������������0000644�0001750�0001750�00000007126�14535072470�017502� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('QuoteGrabs') 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('QuoteGrabs', True) QuoteGrabs = conf.registerPlugin('QuoteGrabs') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(QuoteGrabs, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue(conf.supybot.plugins.QuoteGrabs, 'randomGrabber', registry.Boolean(False, _("""Determines whether the bot will randomly grab possibly-suitable quotes on occasion. The suitability of a given message is determined by ..."""))) conf.registerChannelValue(conf.supybot.plugins.QuoteGrabs.randomGrabber, 'averageTimeBetweenGrabs', registry.PositiveInteger(864000, _("""Determines about how many seconds, on average, should elapse between random grabs. This is only an average value; grabs can happen from any time after half this time until never, although that's unlikely to occur."""))) conf.registerChannelValue(conf.supybot.plugins.QuoteGrabs.randomGrabber, 'minimumWords', registry.PositiveInteger(3, _("""Determines the minimum number of words in a message for it to be considered for random grabbing."""))) conf.registerChannelValue(conf.supybot.plugins.QuoteGrabs.randomGrabber, 'minimumCharacters', registry.PositiveInteger(8, _("""Determines the minimum number of characters in a message for it to be considered for random grabbing."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/locales/�����������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�017301� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/locales/fi.po������������������������������������������������0000644�0001750�0001750�00000016251�14535072470�020242� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: QuoteGrabs plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:08+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 the bot will randomly grab\n" " possibly-suitable quotes on occasion. The suitability of a given " "message\n" " is determined by ..." msgstr "" "Määrittää kaappaako botti sattumanvaraisesti mahdollisesti sopivia " "lainauksia. Annetun viestin\n" " sopivuuden määrittää ..." #: config.py:55 msgid "" "Determines about how many seconds, on\n" " average, should elapse between random grabs. This is only an average\n" " value; grabs can happen from any time after half this time until never,\n" " although that's unlikely to occur." msgstr "" "Määrittää kuinka monta sekuntia keskiarvona,\n" " pitäisi kulua kaappausten välissä. Tämä on vain keskiarvo;\n" " kaappaukset voivat tapahtua milloin tahansa, kun puolet tästä ajasta on " "kulunut ikuisuuteen asti, mutta\n" " sitä tuskin tapahtuu." #: config.py:60 msgid "" "Determines the minimum\n" " number of words in a message for it to be considered for random\n" " grabbing." msgstr "" "Määrittää minimimäärän sanoja viestissä, joita pidetään kaappaukseen " "soveltuvina." #: config.py:64 msgid "" "Determines the\n" " minimum number of characters in a message for it to be considered for\n" " random grabbing." msgstr "" "Määrittää\n" " minimimäärän merkkejä ennen kuin sitä harkitaan sopivaksi\n" " satunnaiselle kaappaamiselle." #: plugin.py:67 msgid "%s (Said by: %s; grabbed by %s at %t)" msgstr "%s (Sanonut: %s; kaapannut %s kello %t)" #: plugin.py:234 msgid "" "Stores and displays quotes from channels. Quotes are stored randomly\n" " and/or on user request." msgstr "" "Tallentaa ja näyttää lainauksia, jotka on kaapattu kanavilta. Lainauksia " "tallennetaan sattumanvaraisesti\n" " ja/tai käyttäjän pyytäessä sitä." #: plugin.py:277 msgid "" "[] \n" "\n" " Grabs a quote from by for the quotegrabs table.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Kaappaa lainauksen, jonka on sanonut " "lainaus kaappaus tietokantaan.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla " "itsellään.\n" " " #: plugin.py:290 msgid "You can't quote grab yourself." msgstr "Et voi kaapata lainausta itseltäsi." #: plugin.py:305 msgid "I couldn't find a proper message to grab." msgstr "En voinut löytää kelvollista viestiä kaapattavaksi." #: plugin.py:310 msgid "" "[] \n" "\n" " Removes the grab (the last by default) on .\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Poistaa kaappauksen (oletuksena viimeinen) .\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla " "itsellään.\n" " " #: plugin.py:321 msgid "Nothing to ungrab." msgstr "Ei mitään kaappausta poistettavaksi." #: plugin.py:323 msgid "Invalid grab number." msgstr "Epäkelvollinen kaappauksen numero." #: plugin.py:328 msgid "" "[] \n" "\n" " Returns 's latest quote grab in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa viimeisimmän lainauksen kaappauksen " ". on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:336 msgid "I couldn't find a matching quotegrab for %s." msgstr "En voinut löytää täsmäävää lainaus kaappausta %s:lle." #: plugin.py:342 msgid "" "[] \n" "\n" " Returns a list of shortened quotes that have been grabbed for " "\n" " as well as the id of each quote. These ids can be used to get the\n" " full quote. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa listan lyhennetyistä lainauksista, jotka on kaapattu " ",\n" " kuten myös jokaisen lainauksen id:een. Näitä id:eitä voidaan " "käyttää\n" " koko lainauksen saamiseen. on vaadittu vain, jos viestiä " "ei lähetetä\n" " kanavalla itsellään.\n" " " #: plugin.py:359 msgid "I couldn't find any quotegrabs for %s." msgstr "En voinut löytää yhtään kaapattuja lainauksia %s:ltä." #: plugin.py:365 msgid "" "[] []\n" "\n" " Returns a randomly grabbed quote, optionally choosing only from " "those\n" " quotes grabbed for . is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Palauttaa satunnaisen lainatun kaappauksen, vaihtoehtoisesti valiten " "vain\n" " lainauksista, jotka on kaapattu . on " "vaadittu vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:375 msgid "Couldn't get a random quote for that nick." msgstr "Satunnaista lainausta tuolta nimimerkiltä ei voitu noutaa." #: plugin.py:377 msgid "" "Couldn't get a random quote. Are there any grabbed quotes in the database?" msgstr "" "Satunnaista lainausta ei voitu noutaa. Onko tietokannassa yhtään kaapattuja " "lainauksia?" #: plugin.py:383 plugin.py:397 msgid "" "[] \n" "\n" " Return the quotegrab with the given . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa kaapatun lainauksen annetulla . on " "vaadittu vain\n" " jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:391 plugin.py:405 msgid "No quotegrab for id %s" msgstr "Ei kaapattuja lainauksia %s:lle." #: plugin.py:411 msgid "" "[] \n" "\n" " Searches for in a quote. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Etsii lainauksesta. on vaadittu vain, jos " "viestiä\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:426 msgid "No quotegrabs matching %s" msgstr "Ei %s:ään täsmääviä lainauksia" #~ msgid "Add the help for \"help QuoteGrabs\" here." #~ msgstr "Lisää ohje \"help QuoteGrabs\"ille tähän." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/locales/fr.po������������������������������������������������0000644�0001750�0001750�00000014601�14535072470�020250� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:50 msgid "" "Determines whether the bot will randomly grab\n" " possibly-suitable quotes on occasion. The suitability of a given " "message\n" " is determined by ..." msgstr "" "Détermine si le bot récupèrera automatiquement des quotes éligible à une " "quote. L'éligibilté est déterminée par..." #: config.py:55 msgid "" "Determines about how many seconds, on\n" " average, should elapse between random grabs. This is only an average\n" " value; grabs can happen from any time after half this time until never,\n" " although that's unlikely to occur." msgstr "" "Détermine le nombre de secondes, en moyenne, entre deux récupération de " "citations. Il s'agit seulement d'une moyenne, et les prises de citation " "peuvent être faites n'importe quand après la moitié de ce temps, voire " "jamais, même si c'est improbable." #: config.py:60 msgid "" "Determines the minimum\n" " number of words in a message for it to be considered for random\n" " grabbing." msgstr "" "Détermine le nombre minimum de mots dans un message pour être éligible à la " "quotation aléatoire." #: config.py:64 msgid "" "Determines the\n" " minimum number of characters in a message for it to be considered for\n" " random grabbing." msgstr "" "Détermine le nombre minimum de caractères dans un message pour être éligible " "à la quotation aléatoire." #: plugin.py:67 msgid "%s (Said by: %s; grabbed by %s at %t)" msgstr "%s (Dit par %s ; récupéré par %s à %t)" #: plugin.py:234 msgid "" "Stores and displays quotes from channels. Quotes are stored randomly\n" " and/or on user request." msgstr "" #: plugin.py:277 msgid "" "[] \n" "\n" " Grabs a quote from by for the quotegrabs table.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Récupère une quote du par le dans la table des citations. " " n'est nécessaire que si le message n'est pas envoyé sur le canal lui-" "même." #: plugin.py:290 msgid "You can't quote grab yourself." msgstr "Vous ne pouvez récupérer des citations de vous-même." #: plugin.py:305 msgid "I couldn't find a proper message to grab." msgstr "Je ne peux trouver de message à quoter." #: plugin.py:310 msgid "" "[] \n" "\n" " Removes the grab (the last by default) on .\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Supprime la quote désignée par le (la dernière par défaut) sur le " ". n'est nécessaire que si le message n'est pas envoyé sur le " "canal lui-même." #: plugin.py:321 msgid "Nothing to ungrab." msgstr "Rien à dé-quoter" #: plugin.py:323 msgid "Invalid grab number." msgstr "Numéro de quote invalide." #: plugin.py:328 msgid "" "[] \n" "\n" " Returns 's latest quote grab in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Retourne le nick de la dernière personne citée sur le . n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:336 msgid "I couldn't find a matching quotegrab for %s." msgstr "Je ne peux trouver de quote coresspondant à %s." #: plugin.py:342 msgid "" "[] \n" "\n" " Returns a list of shortened quotes that have been grabbed for " "\n" " as well as the id of each quote. These ids can be used to get the\n" " full quote. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[] \n" "\n" "Retourne une liste de quotes raccourcies que ont été récupérées pour le " ", ainsi que l'id de chaque quote. Les ids peuvent être utilisés pour " "récupérer les quotes entières. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:359 msgid "I couldn't find any quotegrabs for %s." msgstr "Je ne peux trouver de citation pour %s" #: plugin.py:365 msgid "" "[] []\n" "\n" " Returns a randomly grabbed quote, optionally choosing only from " "those\n" " quotes grabbed for . is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" "Retourne une citation aléatoire, éventuellement parmis les citations " "récupérées de . n'est nécessaire que si le message n'est pas " "envoyé sur le canal lui-même." #: plugin.py:375 msgid "Couldn't get a random quote for that nick." msgstr "Je ne peux récupérer une quote aléatoire pour ce nick." #: plugin.py:377 msgid "" "Couldn't get a random quote. Are there any grabbed quotes in the database?" msgstr "" "Je ne peux récupérer de quote aléatoire. Y a-t-il des quotes dans la base de " "données ?" #: plugin.py:383 plugin.py:397 msgid "" "[] \n" "\n" " Return the quotegrab with the given . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Retourne la quote d' donné. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:391 plugin.py:405 msgid "No quotegrab for id %s" msgstr "Pas de quote d'id %s" #: plugin.py:411 msgid "" "[] \n" "\n" " Searches for in a quote. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Recherche le dans les citations. n'est nécessaire que si le " "message n'est pas envoyé sur le canal lui-même." #: plugin.py:426 msgid "No quotegrabs matching %s" msgstr "Aucune quote ne correspond à %s" �������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/locales/it.po������������������������������������������������0000644�0001750�0001750�00000014754�14535072470�020266� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-07 11:07+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 the bot will randomly grab\n" " possibly-suitable quotes on occasion. The suitability of a given " "message\n" " is determined by ..." msgstr "" "Determina se il bot pescherà in modo casuale citazioni adatte " "all'occasione.\n" " L'idoneità del messaggio è determinata da ..." #: config.py:55 msgid "" "Determines about how many seconds, on\n" " average, should elapse between random grabs. This is only an average\n" " value; grabs can happen from any time after half this time until never,\n" " although that's unlikely to occur." msgstr "" "Determina quanti secondi, in media, devono passare tra due recuperi " "casuali.\n" " Questa è solo un valore medio; il recupero delle citazioni può accadere " "da\n" " qualsiasi momento dopo la metà di questo tempo fino a mai, seppur " "improbabile." #: config.py:60 msgid "" "Determines the minimum\n" " number of words in a message for it to be considered for random\n" " grabbing." msgstr "" "Determina il numero minimo di parole in un messaggio per essere considerato\n" " un recupero casuale." #: config.py:64 msgid "" "Determines the\n" " minimum number of characters in a message for it to be considered for\n" " random grabbing." msgstr "" "Determina il numero minimo di caratteri in un messaggio per essere " "considerato\n" " un recupero casuale." #: plugin.py:67 msgid "%s (Said by: %s; grabbed by %s at %t)" msgstr "%s (Detto da: %s; pescato da %s il %t)" #: plugin.py:234 msgid "" "Stores and displays quotes from channels. Quotes are stored randomly\n" " and/or on user request." msgstr "" #: plugin.py:277 msgid "" "[] \n" "\n" " Grabs a quote from by for the quotegrabs table.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Recupera una citazione di da per la tabella delle " "citazioni.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:290 msgid "You can't quote grab yourself." msgstr "Non puoi recuperare citazioni da solo." #: plugin.py:305 msgid "I couldn't find a proper message to grab." msgstr "Impossibile trovare un messaggio da recuperare." #: plugin.py:310 msgid "" "[] \n" "\n" " Removes the grab (the last by default) on .\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Rimuove la citazione (di default l'ultima) su .\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:321 msgid "Nothing to ungrab." msgstr "Niente da rimuovere." #: plugin.py:323 msgid "Invalid grab number." msgstr "Numero citazione non valido." #: plugin.py:328 msgid "" "[] \n" "\n" " Returns 's latest quote grab in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Riporta il dell'ultima citazione catturata in . " "\n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:336 msgid "I couldn't find a matching quotegrab for %s." msgstr "Impossibile trovare una citazione corrispondente a %s." #: plugin.py:342 msgid "" "[] \n" "\n" " Returns a list of shortened quotes that have been grabbed for " "\n" " as well as the id of each quote. These ids can be used to get the\n" " full quote. is only necessary if the message isn't sent " "in\n" " the channel itself.\n" " " msgstr "" "[] \n" "\n" " Riporta un elenco di citazioni in versione ridotta che sono state " "pescate per \n" " e l'ID di ognuna. Questi ID possono essere utilizzati per recuperare " "la citazione\n" " completa. è necessario solo se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:359 msgid "I couldn't find any quotegrabs for %s." msgstr "Impossibile trovare una citazione per %s." #: plugin.py:365 msgid "" "[] []\n" "\n" " Returns a randomly grabbed quote, optionally choosing only from " "those\n" " quotes grabbed for . is only necessary if the " "message\n" " isn't sent in the channel itself.\n" " " msgstr "" "[] []\n" "\n" " Restituisce una citazione pescata in modo casuale, eventualmente " "scegliendo solo quelle\n" " di . è necessario solo se il messaggio non viene " "inviato nel canale stesso.\n" " " #: plugin.py:375 msgid "Couldn't get a random quote for that nick." msgstr "Impossibile ottenere una citazione casuale per questo nick." #: plugin.py:377 msgid "" "Couldn't get a random quote. Are there any grabbed quotes in the database?" msgstr "" "Impossibile ottenere una citazione casuale. Ci sono delle citazioni nel " "database?" #: plugin.py:383 plugin.py:397 msgid "" "[] \n" "\n" " Return the quotegrab with the given . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Restituisce la citazione con l' specificato. è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:391 plugin.py:405 msgid "No quotegrab for id %s" msgstr "Nessuna citazione per l'id %s." #: plugin.py:411 msgid "" "[] \n" "\n" " Searches for in a quote. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" " Cerca in una citazione. è necessario solo se il\n" " messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:426 msgid "No quotegrabs matching %s" msgstr "Nessuna citazione corrispondente a %s." ��������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/plugin.py����������������������������������������������������0000644�0001750�0001750�00000041342�14535072470�017531� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Daniel DiPaolo # Copyright (c) 2008-2010, James McCoy # Copyright (c) 2010-2021, 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 sys import time import random 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.ircmsgs as ircmsgs import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('QuoteGrabs') import sqlite3 import traceback #sqlite3.register_converter('bool', bool) class QuoteGrabsRecord(dbi.Record): __fields__ = [ 'by', 'text', 'grabber', 'at', 'hostmask', ] def __str__(self): grabber = plugins.getUserName(self.grabber) if self.at: return format(_('%s (Said by: %s; grabbed by %s at %t)'), self.text, self.hostmask, grabber, self.at) else: return format('%s', self.text) class SqliteQuoteGrabsDB(object): def __init__(self, filename): self.dbs = ircutils.IrcDict() self.filename = filename def close(self): for db in self.dbs.values(): db.close() def _getDb(self, channel): filename = plugins.makeChannelFilename(self.filename, channel) def p(s1, s2): # text_factory seems to only apply as an output adapter, # so doesn't apply to created functions; so we use str() return ircutils.nickEqual(str(s1), str(s2)) if filename in self.dbs: return self.dbs[filename] if os.path.exists(filename): db = sqlite3.connect(filename) if minisix.PY2: db.text_factory = str db.create_function('nickeq', 2, p) self.dbs[filename] = db return db db = sqlite3.connect(filename) if minisix.PY2: db.text_factory = str db.create_function('nickeq', 2, p) self.dbs[filename] = db cursor = db.cursor() cursor.execute("""CREATE TABLE quotegrabs ( id INTEGER PRIMARY KEY, nick BLOB, hostmask TEXT, added_by TEXT, added_at TIMESTAMP, quote TEXT );""") db.commit() return db def get(self, channel, id, quoteonly = 0): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT id, nick, quote, hostmask, added_at, added_by FROM quotegrabs WHERE id = ?""", (id,)) results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError (id, by, quote, hostmask, at, grabber) = results[0] if quoteonly == 0: return QuoteGrabsRecord(id, by=by, text=quote, hostmask=hostmask, at=int(at), grabber=grabber) else: return QuoteGrabsRecord(id, text=quote) def random(self, channel, nick): db = self._getDb(channel) cursor = db.cursor() if nick: cursor.execute("""SELECT quote FROM quotegrabs WHERE nickeq(nick, ?) ORDER BY random() LIMIT 1""", (nick,)) else: cursor.execute("""SELECT quote FROM quotegrabs ORDER BY random() LIMIT 1""") results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError return results[0][0] def list(self, channel, nick): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT id, quote FROM quotegrabs WHERE nickeq(nick, ?) ORDER BY id DESC""", (nick,)) results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError return [QuoteGrabsRecord(id, text=quote) for (id, quote) in results] def getQuote(self, channel, nick): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT quote FROM quotegrabs WHERE nickeq(nick, ?) ORDER BY id DESC LIMIT 1""", (nick,)) results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError return results[0][0] def select(self, channel, nick): db = self._getDb(channel) cursor = db.cursor() cursor.execute("""SELECT added_at FROM quotegrabs WHERE nickeq(nick, ?) ORDER BY id DESC LIMIT 1""", (nick,)) results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError return results[0][0] def add(self, channel, msg, by): db = self._getDb(channel) cursor = db.cursor() text = ircmsgs.prettyPrint(msg) # Check to see if the latest quotegrab is identical cursor.execute("""SELECT quote FROM quotegrabs WHERE nick=? ORDER BY id DESC LIMIT 1""", (msg.nick,)) results = cursor.fetchall() if len(results) != 0: if text == results[0][0]: return cursor.execute("""INSERT INTO quotegrabs VALUES (NULL, ?, ?, ?, ?, ?)""", (msg.nick, msg.prefix, by, int(time.time()), text,)) db.commit() def remove(self, channel, grab=None): db = self._getDb(channel) cursor = db.cursor() if grab is not None: # the testing if there actually *is* the to-be-deleted record is # strictly unnecessary -- the DELETE operation would "succeed" # anyway, but it's silly to just keep saying 'OK' no matter what, # so... cursor.execute("""SELECT * FROM quotegrabs WHERE id = ?""", (grab,)) results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError cursor.execute("""DELETE FROM quotegrabs WHERE id = ?""", (grab,)) else: cursor.execute("""SELECT * FROM quotegrabs WHERE id = (SELECT MAX(id) FROM quotegrabs)""") results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError cursor.execute("""DELETE FROM quotegrabs WHERE id = (SELECT MAX(id) FROM quotegrabs)""") db.commit() def search(self, channel, text): db = self._getDb(channel) cursor = db.cursor() text = '%' + text + '%' cursor.execute("""SELECT id, nick, quote FROM quotegrabs WHERE quote LIKE ? ORDER BY id DESC""", (text,)) results = cursor.fetchall() if len(results) == 0: raise dbi.NoRecordError return [QuoteGrabsRecord(id, text=quote, by=nick) for (id, nick, quote) in results] QuoteGrabsDB = plugins.DB('QuoteGrabs', {'sqlite3': SqliteQuoteGrabsDB}) class QuoteGrabs(callbacks.Plugin): """Stores and displays quotes from channels. Quotes are stored randomly and/or on user request.""" def __init__(self, irc): self.__parent = super(QuoteGrabs, self) self.__parent.__init__(irc) self.db = QuoteGrabsDB() def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return irc = callbacks.SimpleProxy(irc, msg) if msg.channel: payload = msg.args[1] words = self.registryValue('randomGrabber.minimumWords', msg.channel, irc.network) length = self.registryValue('randomGrabber.minimumCharacters', msg.channel, irc.network) grabTime = \ self.registryValue('randomGrabber.averageTimeBetweenGrabs', msg.channel, irc.network) channel = plugins.getChannel(msg.channel) if self.registryValue('randomGrabber', msg.channel, irc.network): if len(payload) > length and len(payload.split()) > words: try: last = int(self.db.select(channel, msg.nick)) except dbi.NoRecordError: self._grab(channel, irc, msg, irc.prefix) self._sendGrabMsg(irc, msg) else: elapsed = int(time.time()) - last if (random.random() * elapsed) > (grabTime / 2): self._grab(channel, irc, msg, irc.prefix) self._sendGrabMsg(irc, msg) def _grab(self, channel, irc, msg, addedBy): self.db.add(channel, msg, addedBy) def _sendGrabMsg(self, irc, msg): s = 'jots down a new quote for %s' % msg.nick irc.reply(s, action=True, prefixNick=False) @internationalizeDocstring def grab(self, irc, msg, args, channel, nick): """[] Grabs a quote from by for the quotegrabs table. is only necessary if the message isn't sent in the channel itself. """ # chan is used to make sure we know where to grab the quote from, as # opposed to channel which is used to determine which db to store the # quote in chan = msg.args[0] if chan is None or not irc.isChannel(chan): raise callbacks.ArgumentError if ircutils.nickEqual(nick, msg.nick): irc.error(_('You can\'t quote grab yourself.'), Raise=True) if conf.supybot.protocols.irc.experimentalExtensions(): msgid = msg.server_tags.get('+draft/reply') else: msgid = None for m in reversed(irc.state.history): if msgid and m.server_tags.get('msgid') != msgid: continue if m.command == 'PRIVMSG' and ircutils.nickEqual(m.nick, nick) \ and ircutils.strEqual(m.args[0], chan): # TODO: strip statusmsg prefix for comparison? Must be careful # abouk leaks, though. self._grab(channel, irc, m, msg.prefix) irc.replySuccess() return irc.error(_('I couldn\'t find a proper message to grab.')) grab = wrap(grab, ['channeldb', 'nick']) @internationalizeDocstring def ungrab(self, irc, msg, args, channel, grab): """[] Removes the grab (the last by default) on . is only necessary if the message isn't sent in the channel itself. """ try: self.db.remove(channel, grab) irc.replySuccess() except dbi.NoRecordError: if grab is None: irc.error(_('Nothing to ungrab.')) else: irc.error(_('Invalid grab number.')) ungrab = wrap(ungrab, ['channeldb', optional('id')]) @internationalizeDocstring def quote(self, irc, msg, args, channel, nick): """[] Returns 's latest quote grab in . is only necessary if the message isn't sent in the channel itself. """ try: irc.reply(self.db.getQuote(channel, nick)) except dbi.NoRecordError: irc.error(_('I couldn\'t find a matching quotegrab for %s.') % nick, Raise=True) quote = wrap(quote, ['channeldb', 'nick']) @internationalizeDocstring def list(self, irc, msg, args, channel, nick): """[] Returns a list of shortened quotes that have been grabbed for as well as the id of each quote. These ids can be used to get the full quote. is only necessary if the message isn't sent in the channel itself. """ try: records = self.db.list(channel, nick) L = [] for record in records: # strip the nick from the quote quote = record.text.replace('<%s> ' % nick, '', 1) item = utils.str.ellipsisify('#%s: %s' % (record.id, quote),50) L.append(item) irc.reply(utils.str.commaAndify(L)) except dbi.NoRecordError: irc.error(_('I couldn\'t find any quotegrabs for %s.') % nick, Raise=True) list = wrap(list, ['channeldb', 'nick']) @internationalizeDocstring def random(self, irc, msg, args, channel, nick): """[] [] Returns a randomly grabbed quote, optionally choosing only from those quotes grabbed for . is only necessary if the message isn't sent in the channel itself. """ try: irc.reply(self.db.random(channel, nick)) except dbi.NoRecordError: if nick: irc.error(_('Couldn\'t get a random quote for that nick.')) else: irc.error(_('Couldn\'t get a random quote. Are there any ' 'grabbed quotes in the database?')) random = wrap(random, ['channeldb', additional('nick')]) @internationalizeDocstring def say(self, irc, msg, args, channel, id): """[] Return the quotegrab with the given . is only necessary if the message isn't sent in the channel itself. """ try: irc.reply(self.db.get(channel, id, 1)) except dbi.NoRecordError: irc.error(_('No quotegrab for id %s') % utils.str.quoted(id), Raise=True) say = wrap(say, ['channeldb', 'id']) @internationalizeDocstring def get(self, irc, msg, args, channel, id): """[] Return the quotegrab with the given . is only necessary if the message isn't sent in the channel itself. """ try: irc.reply(self.db.get(channel, id)) except dbi.NoRecordError: irc.error(_('No quotegrab for id %s') % utils.str.quoted(id), Raise=True) get = wrap(get, ['channeldb', 'id']) @internationalizeDocstring def search(self, irc, msg, args, channel, text): """[] Searches for in a quote. is only necessary if the message isn't sent in the channel itself. """ try: records = self.db.search(channel, text) L = [] for record in records: # strip the nick from the quote quote = record.text.replace('<%s> ' % record.by, '', 1) item = utils.str.ellipsisify('#%s: %s' % (record.id, quote),50) L.append(item) irc.reply(utils.str.commaAndify(L)) except dbi.NoRecordError: irc.error(_('No quotegrabs matching %s') % utils.str.quoted(text), Raise=True) search = wrap(search, ['channeldb', 'text']) Class = QuoteGrabs # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/QuoteGrabs/test.py������������������������������������������������������0000644�0001750�0001750�00000020560�14535072470�017211� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2004, Daniel DiPaolo # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 * class QuoteGrabsTestCase(ChannelPluginTestCase): plugins = ('QuoteGrabs',) def testQuoteGrab(self): testPrefix = 'foo!bar@baz' self.assertError('grab foo') # Test join/part/notice (shouldn't grab) self.irc.feedMsg(ircmsgs.join(self.channel, prefix=testPrefix)) self.assertError('grab foo') self.irc.feedMsg(ircmsgs.part(self.channel, prefix=testPrefix)) self.assertError('grab foo') # Test privmsgs self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) self.assertNotError('grab foo') self.assertResponse('quote foo', ' something') # Test actions self.irc.feedMsg(ircmsgs.action(self.channel, 'moos', prefix=testPrefix)) self.assertNotError('grab foo') self.assertResponse('quote foo', '* foo moos') def testQuoteGrabReplyDisabled(self): testPrefix = 'foo!bar@baz' prefixChar = conf.supybot.reply.whenAddressedBy.chars()[0] self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'msgid': 'aaaa'}, prefix=testPrefix, command='PRIVMSG', args=(self.channel, 'something'))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'msgid': 'bbbb'}, prefix=testPrefix, command='PRIVMSG', args=(self.channel, 'something else'))) # supybot.protocols.irc.experimentalExtensions is not enabled, so # +draft/reply is ignored. self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'+draft/reply': 'aaaa'}, prefix=self.prefix, command='PRIVMSG', args=(self.channel, prefixChar+'grab foo'))) self.assertResponse(' ', 'The operation succeeded.') self.assertResponse('quote foo', ' something else') def testQuoteGrabReply(self): testPrefix = 'foo!bar@baz' prefixChar = conf.supybot.reply.whenAddressedBy.chars()[0] self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'msgid': 'aaaa'}, prefix=testPrefix, command='PRIVMSG', args=(self.channel, 'something'))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'msgid': 'bbbb'}, prefix=testPrefix, command='PRIVMSG', args=(self.channel, 'something else'))) with conf.supybot.protocols.irc.experimentalExtensions.context(True): self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'+draft/reply': 'aaaa'}, prefix=self.prefix, command='PRIVMSG', args=(self.channel, prefixChar+'grab foo'))) self.assertResponse(' ', 'The operation succeeded.') self.assertResponse('quote foo', ' something') def testUngrab(self): testPrefix = 'foo!bar@baz' # nothing yet self.assertError('ungrab') self.assertError('ungrab 2') self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) # still not grabbed self.assertError('ungrab') self.assertError('ungrab 3') # grab and ungrab a quote self.assertNotError('grab foo') self.assertNotError('ungrab') self.assertNotError('grab foo') # this is not there... self.assertError('ungrab 8883') # ...unlike this... self.assertNotError('ungrab 1') # ...but not now anymore :-D self.assertError('ungrab') # grab two quotes and ungrab them by id self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'something', prefix=testPrefix)) self.assertNotError('grab foo') self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'another', prefix=testPrefix)) self.assertNotError('grab foo') self.assertNotError('ungrab 1') self.assertNotError('ungrab 2') self.assertError('ungrab') def testList(self): testPrefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'testList', prefix=testPrefix)) self.assertNotError('grab foo') self.assertResponse('quotegrabs list foo', '#1: testList') self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'a' * 80, prefix=testPrefix)) self.assertNotError('grab foo') self.assertResponse('quotegrabs list foo', '#2: %s... and #1: testList' %\ ('a'*43)) # 50 - length of "#2: ..." def testDuplicateGrabs(self): testPrefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'testDupe', prefix=testPrefix)) self.assertNotError('grab foo') self.assertNotError('grab foo') # note:NOTanerror,stillwon'tdupe self.assertResponse('quotegrabs list foo', '#1: testDupe') def testCaseInsensitivity(self): testPrefix = 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'testCI', prefix=testPrefix)) self.assertNotError('grab FOO') self.assertNotError('quote foo') self.assertNotError('quote FoO') self.assertNotError('quote Foo') self.assertNotError('quotegrabs list FOO') self.assertNotError('quotegrabs list fOo') def testRandom(self): testPrefix = 'foo!bar@baz' self.assertError('random') self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'testRandom', prefix=testPrefix)) self.assertError('random') # still none in the db self.assertNotError('grab foo') self.assertResponse('random', ' testRandom') self.assertResponse('random foo', ' testRandom') self.assertResponse('random FOO', ' testRandom') def testGet(self): testPrefix= 'foo!bar@baz' self.assertError('quotegrabs get asdf') self.assertError('quotegrabs get 1') self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'testGet', prefix=testPrefix)) self.assertNotError('grab foo') self.assertNotError('quotegrabs get 1') def testSearch(self): testPrefix= 'foo!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'testSearch', prefix=testPrefix)) self.assertError('quotegrabs search test') # still none in db self.assertNotError('grab foo') self.assertNotError('quotegrabs search test') class QuoteGrabsNonChannelTestCase(QuoteGrabsTestCase): config = { 'databases.plugins.channelSpecific' : False } # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/��������������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�014252� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/__init__.py���������������������������������������������������������0000644�0001750�0001750�00000005220�14535072470�016360� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Provides basic functionality for handling RSS/RDF feeds, and allows announcing them periodically to channels. In order to use this plugin you must have the following modules installed: * feedparser: http://feedparser.org/ """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/config.py�����������������������������������������������������������0000644�0001750�0001750�00000015323�14535072470�016073� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('RSS') 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('RSS', True) class FeedNames(registry.SpaceSeparatedSetOfStrings): List = callbacks.CanonicalNameSet class FeedItemSortOrder(registry.OnlySomeStrings): """Valid values include 'asInFeed', 'oldestFirst', 'newestFirst'.""" validStrings = ('asInFeed', 'oldestFirst', 'newestFirst', 'outdatedFirst', 'updatedFirst') RSS = conf.registerPlugin('RSS') conf.registerGlobalValue(RSS, 'feeds', FeedNames([], _("""Determines what feeds should be accessible as commands."""))) ######## # Format conf.registerChannelValue(RSS, 'headlineSeparator', registry.StringSurroundedBySpaces('|', _("""Determines what string is used to separate headlines in new feeds."""))) conf.registerChannelValue(RSS, 'format', registry.String(_('$date: $title <$link>'), _("""The format the bot will use for displaying headlines of a RSS feed that is triggered manually. In addition to fields defined by feedparser ($published (the entry date), $title, $link, $description, $id, etc.), the following variables can be used: $feed_name (the configured name) $feed_title/$feed_subtitle/$feed_author/$feed_language/$feed_link, $date (parsed date, as defined in supybot.reply.format.time)"""))) conf.registerChannelValue(RSS, 'announceFormat', registry.String(_('News from $feed_name: $title <$link>'), _("""The format the bot will use for displaying headlines of a RSS feed that is announced. See supybot.plugins.RSS.format for the available variables."""))) ########### # Announces conf.registerChannelValue(RSS, 'announce', registry.SpaceSeparatedSetOfStrings([], _("""Determines which RSS feeds should be announced in the channel; valid input is a list of strings (either registered RSS feeds or RSS feed URLs) separated by spaces."""))) conf.registerGlobalValue(RSS, 'waitPeriod', registry.PositiveInteger(1800, _("""Indicates how many seconds the bot will wait between retrieving RSS feeds; requests made within this period will return cached results."""))) conf.registerGlobalValue(RSS, 'sortFeedItems', FeedItemSortOrder('asInFeed', _("""Determines whether feed items should be sorted by their publication/update timestamp or kept in the same order as they appear in a feed."""))) conf.registerChannelValue(RSS, 'notice', registry.Boolean(False, _("""Determines whether announces will be sent as notices instead of privmsgs."""))) conf.registerChannelValue(RSS, 'maximumAnnounceHeadlines', registry.PositiveInteger(5, _("""Indicates how many new news entries may be sent at the same time. Extra entries will be discarded."""))) #################### # Headlines filtering conf.registerChannelValue(RSS, 'defaultNumberOfHeadlines', registry.PositiveInteger(1, _("""Indicates how many headlines an rss feed will output by default, if no number is provided."""))) conf.registerChannelValue(RSS, 'initialAnnounceHeadlines', registry.Integer(5, _("""Indicates how many headlines an rss feed will output when it is first added to announce for a channel."""))) conf.registerChannelValue(RSS, 'keywordWhitelist', registry.SpaceSeparatedSetOfStrings([], _("""Space separated list of strings, lets you filter headlines to those containing one or more items in this whitelist."""))) conf.registerChannelValue(RSS, 'keywordBlacklist', registry.SpaceSeparatedSetOfStrings([], _("""Space separated list of strings, lets you filter headlines to those not containing any items in this blacklist."""))) def register_feed_config(name, url=''): RSS.feeds().add(name) conf.registerGlobalValue(RSS.feeds, name, registry.String(url, _("""The URL for the feed %s. Note that because announced lines are cached, you may need to reload this plugin after changing this option.""" % name))) feed_group = conf.registerGroup(RSS.feeds, name) conf.registerChannelValue(feed_group, 'format', registry.String('', _("""Feed-specific format. Defaults to supybot.plugins.RSS.format if empty."""))) conf.registerChannelValue(feed_group, 'announceFormat', registry.String('', _("""Feed-specific announce format. Defaults to supybot.plugins.RSS.announceFormat if empty."""))) conf.registerGlobalValue(feed_group, 'waitPeriod', registry.NonNegativeInteger(0, _("""If set to a non-zero value, overrides supybot.plugins.RSS.waitPeriod for this particular feed."""))) for name in RSS.feeds(): register_feed_config(name) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�010212� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1702131037.3537545 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/locales/������������������������������������������������������������0000755�0001750�0001750�00000000000�14535072535�015674� 5����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/locales/de.po�������������������������������������������������������0000644�0001750�0001750�00000030243�14535072470�016624� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-29 17:09+0100\n" "Last-Translator: Valentin Lorentz \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" #: config.py:50 msgid "Valid values include 'asInFeed', 'oldestFirst', 'newestFirst'." msgstr "" #: config.py:57 msgid "" "Determines what feeds should be accessible as\n" " commands." msgstr "Legt fest auf welche Feeds per Befehl zugegriffen werden darf." #: config.py:64 msgid "" "Determines what string is\n" " used to separate headlines in new feeds." msgstr "" "Gibt an welche Zeichenkette verwendet wird um Kopfzeilen neuer Feeds zu " "separieren." #: config.py:67 msgid "$date: $title <$link>" msgstr "" #: config.py:67 msgid "" "The format the bot\n" " will use for displaying headlines of a RSS feed that is triggered\n" " manually. In addition to fields defined by feedparser ($published\n" " (the entry date), $title, $link, $description, $id, etc.), the " "following\n" " variables can be used: $feed_name, $date (parsed date, as defined in\n" " supybot.reply.format.time)" msgstr "" #: config.py:74 msgid "News from $feed_name: $title <$link>" msgstr "" #: config.py:75 msgid "" "The format the bot will use for displaying headlines of a RSS feed\n" " that is announced. See supybot.plugins.RSS.format for the available\n" " variables." msgstr "" #: config.py:83 msgid "" "Determines which RSS feeds\n" " should be announced in the channel; valid input is a list of strings\n" " (either registered RSS feeds or RSS feed URLs) separated by spaces." msgstr "" "Legt fest welche RSS Feeds in einem Kanal verkündet werden. Zulässige " "eingaben sind Listen von Zeichenketten (entweder registrierte RSS Feed oder " "URLs zu RSS Feed), mit einem Leerzeichen getrennt" #: config.py:87 msgid "" "Indicates how many seconds the bot will\n" " wait between retrieving RSS feeds; requests made within this period " "will\n" " return cached results." msgstr "" "Zeigt wieviele Sekunden der Bot wartet, zwischen dem Empfangen von RSS " "Feeds; Anfragen die innerhalb dieser Zeit gemachten werden, führt zu " "zwischengespeicherten Ergebnissen." #: config.py:91 msgid "" "Determines whether feed items should be\n" " sorted by their publication/update timestamp or kept in the same order " "as\n" " they appear in a feed." msgstr "" #: config.py:95 msgid "" "Determines whether announces will be sent\n" " as notices instead of privmsgs." msgstr "" #: config.py:98 msgid "" "Indicates how many new news entries may\n" " be sent at the same time. Extra entries will be discarded." msgstr "" #: config.py:104 msgid "" "Indicates how many headlines an rss feed\n" " will output by default, if no number is provided." msgstr "" #: config.py:107 msgid "" "Indicates how many headlines an rss feed\n" " will output when it is first added to announce for a channel." msgstr "" #: config.py:110 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those containing one or more " "items\n" " in this whitelist." msgstr "" #: config.py:114 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those not containing any items\n" " in this blacklist." msgstr "" #: config.py:127 msgid "" "Feed-specific format. Defaults to\n" " supybot.plugins.RSS.format if empty." msgstr "" #: config.py:130 msgid "" "Feed-specific announce format.\n" " Defaults to supybot.plugins.RSS.announceFormat if empty." msgstr "" #: config.py:133 msgid "" "If set to a non-zero\n" " value, overrides supybot.plugins.RSS.waitPeriod for this\n" " particular feed." msgstr "" #: plugin.py:140 msgid "" "[]\n" "\n" " Reports the titles for %s at the RSS feed %u. If\n" " is given, returns only that many headlines.\n" " RSS feeds are only looked up every supybot.plugins.RSS.waitPeriod\n" " seconds, which defaults to 1800 (30 minutes) since that's what most\n" " websites prefer." msgstr "" #: plugin.py:170 msgid "Return feed items, sorted according to sortFeedItems." msgstr "" #: plugin.py:190 msgid "" "\n" " This plugin is useful both for announcing updates to RSS feeds in a\n" " channel, and for retrieving the headlines of RSS feeds via command. " "Use\n" " the \"add\" command to add feeds to this plugin, and use the \"announce" "\"\n" " command to determine what feeds should be announced in a given channel.\n" "\n" " Basic usage\n" " ^^^^^^^^^^^\n" "\n" " 1. Add a feed using\n" " ``@rss add limnoria https://github.com/progval/Limnoria/tags.atom``.\n" "\n" " * This is RSS feed of Limnoria's stable releases.\n" " * You can now check the latest news from the feed with " "``@limnoria``.\n" "\n" " 2. To have new news automatically announced on the channel, use\n" " ``@rss announce add Limnoria``.\n" "\n" " To add another feed, simply replace limnoria and the address using name\n" " of the feed and address of the feed. For example, YLE News:\n" "\n" " 1. ``@rss add yle http://yle.fi/uutiset/rss/uutiset.rss?osasto=news``\n" " 2. ``@rss announce add yle``\n" "\n" " News on their own lines\n" " ^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " If you want the feed topics to be on their own lines instead of being " "separated by\n" " the separator which you have configured you can set `reply.onetoone` to " "False.\n" "\n" " Please first read the help for that configuration variable\n" "\n" " ``@config help reply.onetoone``\n" "\n" " and understand what it says and then you can do\n" "\n" " ``@config reply.onetoone False``\n" "\n" " " msgstr "" #: plugin.py:278 msgid "I already have a command in this plugin named %s." msgstr "" #: plugin.py:284 msgid "I already have a feed with that URL named %s." msgstr "" #: plugin.py:508 msgid "" " \n" "\n" " Adds a command to this plugin that will look up the RSS feed at the\n" " given URL.\n" " " msgstr "" " \n" "\n" "Fügt einen Befehl zu diesem Plugin hinzu, der den RSS Feed von angegebener " "URL abruft." #: plugin.py:521 msgid "" "\n" "\n" " Removes the command for looking up RSS feeds at from\n" " this plugin.\n" " " msgstr "" "\n" "\n" "Entfernt den Befehl um RSS Feeds von abzurufen aus diesem Plugin" #: plugin.py:528 msgid "That's not a valid RSS feed command name." msgstr "Das ist kein gültiger RSS Feed Befehl." #: plugin.py:547 msgid "" "[]\n" "\n" " Returns the list of feeds announced in . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[] \n" "\n" "Gibt eine Liste der Feeds, die in einem verkündet werden, zurück. " " ist nur nötig falls der Befehl nicht direkt im Kanal abgegeben wird." #: plugin.py:556 msgid "I am currently not announcing any feeds." msgstr "Momentan kündige ich keine Feeds an." #: plugin.py:561 msgid "" "[] [ ...]\n" "\n" " Adds the list of feeds to the current list of announced feeds " "in\n" " . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Fügt die Liste der Feeds zur momentanen Liste der verkündeten Feeds eines " " hinzu. Zulässige Feeds umfassen die Namen von registrierten Feed als " "auch URLs für RSS Feeds. ist nur nötig falls die Nachricht nicht im " "Kanal gesendet wird." #: plugin.py:572 msgid "These feeds are unknown: %L" msgstr "" #: plugin.py:593 msgid "" "[] [ ...]\n" "\n" " Removes the list of feeds from the current list of announced " "feeds\n" " in . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Entfernt die Liste der Feeds von der momentanen Liste der verkündeten Feeds " "eines . Zulässige Feeds umfassen die Namen von registrierten Feed als " "auch URLs für RSS Feeds. ist nur nötig falls die Nachricht nicht im " "Kanal gesendet wird." #: plugin.py:618 msgid "" "\n" "\n" " Returns a list of channels that the given feed name or URL is " "being\n" " announced to.\n" " " msgstr "" #: plugin.py:642 #, fuzzy msgid "" " []\n" "\n" " Gets the title components of the given RSS feed.\n" " If is given, return only that many headlines.\n" " " msgstr "" " []\n" "\n" "Empfängt die Titel des angegeben RSS Feeds. Falls " "angegeben wurde, werden nur soviele Kopfzeilen ausgegeben." #: plugin.py:658 msgid "Couldn't get RSS feed." msgstr "Konnte den RSS Feed nicht bekommen." #: plugin.py:661 msgid " Parser error: " msgstr "" #: plugin.py:677 msgid "" "\n" "\n" " Returns information from the given RSS feed, namely the title,\n" " URL, description, and last update date, if available.\n" " " msgstr "" "\n" "\n" "Gibt Informationen zum angegeben RSS Feed aus. Den Titel, URL, Beschreibung " "und das Datum des letzten Updates, wenn verfügbar." #: plugin.py:692 msgid "I couldn't retrieve that RSS feed." msgstr "Ich konnte den RSS Feed nicht empfangen." #: plugin.py:700 msgid "time unavailable" msgstr "" #: plugin.py:701 plugin.py:702 plugin.py:703 msgid "unavailable" msgstr "" #: plugin.py:705 msgid "Title: %s; URL: %u; Description: %s; Last updated: %s." msgstr "Titel: %s; URL: %u; Beschreibung: %s; Zuletzt aktualisiert: %s." #, fuzzy #~ msgid "" #~ "Determines whether the bot will bold the title of the feed when\n" #~ " it announces news." #~ msgstr "" #~ "Legt fest ob der Bot Titel fett schreibt, wenn er er Neuigkeiten " #~ "verkündet." #, fuzzy #~ msgid "" #~ "Determines what\n" #~ " prefix is prepended (if any) to the news item announcements made in " #~ "the\n" #~ " channel." #~ msgstr "" #~ "Legt fest welcher Prefix (falls überhaupt) den News vorangestellt wird, " #~ "wenn neue Verkündungen in einem Kanal gemacht werden." #, fuzzy #~ msgid "News from " #~ msgstr "Neuigkeiten von " #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when the rss command is called.\n" #~ " supybot.plugins.RSS.announce.showLinks affects whether links will be\n" #~ " listed when a feed is automatically announced." #~ msgstr "" #~ "Legt fest ob der Bot den Link zusammen mit dem Titel des Feeds " #~ "aufgelistet wird, wenn der RSS Befehl aufgerufen wird. supybot.plugins." #~ "RSS.announce.showLinks ist zuständig dafür falls der Feed automatisch " #~ "verkündet wird." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when a feed is automatically\n" #~ " announced." #~ msgstr "" #~ "Legt fest ob der Bot einen Link mit dem Feed Titel angeben soll wenn ein " #~ "Feed automatisch verkündet wird." #~ msgid "" #~ "This plugin is useful both for announcing updates to RSS feeds in a\n" #~ " channel, and for retrieving the headlines of RSS feeds via command. " #~ "Use\n" #~ " the \"add\" command to add feeds to this plugin, and use the " #~ "\"announce\"\n" #~ " command to determine what feeds should be announced in a given " #~ "channel." #~ msgstr "" #~ "Dieses plugin wird verwendet um Updates eines RSS Feed in einem Kanal zu " #~ "verkünden und um die Kopfzeilen des RSS Feeds über einen Befehl zu " #~ "empfangen. Benutze \"add\" um Feeds zu diesem Plugin hinzuzufügen und " #~ "benutze \"announce\" um festzulegen ob der Feed im gegeben Kanal " #~ "verkündet werden soll." �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/locales/fi.po�������������������������������������������������������0000644�0001750�0001750�00000036355�14535072470�016644� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# RSS plugin in Limnoria. # Copyright (C) Limnoria 2011-2014 # Mikaela Suomalainen , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: RSS plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:12+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:50 msgid "Valid values include 'asInFeed', 'oldestFirst', 'newestFirst'." msgstr "" "Kelvolliset arvot ovat 'asInFeed' (kuten syötteessä), 'oldestFirst' (vanhin " "ensin), 'newestFirst' (uusin ensin)." #: config.py:57 msgid "" "Determines what feeds should be accessible as\n" " commands." msgstr "" "Määrittää mitten syötteiden pitäisi olla käytettävissä\n" " komentoina." #: config.py:64 msgid "" "Determines what string is\n" " used to separate headlines in new feeds." msgstr "" "Määrittää mitä merkkiketjua käytetään\n" " erottamaan otsikot uusissa syötteissä." # This looks like something that shouldn't be translated. #: config.py:67 msgid "$date: $title <$link>" msgstr "$date: $title <$link>" #: config.py:67 #, fuzzy msgid "" "The format the bot\n" " will use for displaying headlines of a RSS feed that is triggered\n" " manually. In addition to fields defined by feedparser ($published\n" " (the entry date), $title, $link, $description, $id, etc.), the " "following\n" " variables can be used: $feed_name, $date (parsed date, as defined in\n" " supybot.reply.format.time)" msgstr "" "Muoto, jota käytetään manuaalisesti pyydetyn RSS-syltteen otsikoissa.\n" " Feedparserin määrittämien kenttien ($published (julkaisuaika), $title, " "$link\n" " $description, $id jne.) lisäksi voidaan käyttää seuraavia muuttujia: " "$feed_name,\n" " $date (parsittu päiväys, jonka määrittää asetus supybot.reply.format.time)" #: config.py:74 msgid "News from $feed_name: $title <$link>" msgstr "Uutisia lähteestä $feed_name: $title <$link>" #: config.py:75 msgid "" "The format the bot will use for displaying headlines of a RSS feed\n" " that is announced. See supybot.plugins.RSS.format for the available\n" " variables." msgstr "" "Muoto, jotaa botti käyttää kuulutettavan RSS-syötteen otsikoiden " "näyttämiseen.\n" " Kelvolliset muuttujat näkee asetuksesta supybot.plugins.RSS.format." #: config.py:83 msgid "" "Determines which RSS feeds\n" " should be announced in the channel; valid input is a list of strings\n" " (either registered RSS feeds or RSS feed URLs) separated by spaces." msgstr "" "Määrittää kanavalla kuulutettavat RSS-syötteet. Kelvollinen asetusarvo\n" " on merkkiketju luettelo (joka sisältää rekisteröityjä RSS-syötteitä tai " "syöte\n" "-osoitteita) välilyönneillä erotettuina." #: config.py:87 msgid "" "Indicates how many seconds the bot will\n" " wait between retrieving RSS feeds; requests made within this period " "will\n" " return cached results." msgstr "" "Määrittää kuinka monta sekuntia botti odottaa\n" " RSS syötteiden päivittämisten välillä; pyynnöt, jotka tehdän tällä " "ajalla\n" " palauttavat välimuistissa olevia tuloksia." #: config.py:91 #, fuzzy msgid "" "Determines whether feed items should be\n" " sorted by their publication/update timestamp or kept in the same order " "as\n" " they appear in a feed." msgstr "" "Määrittää pitäisikö syötteiden kohteiden olla järjestettyinä\n" " päivitysaikaleiman mukaan vai pitää samassa järjestyksessä, kuin ne " "ilmestyvät\n" " syötteessä." #: config.py:95 msgid "" "Determines whether announces will be sent\n" " as notices instead of privmsgs." msgstr "" #: config.py:98 msgid "" "Indicates how many new news entries may\n" " be sent at the same time. Extra entries will be discarded." msgstr "" #: config.py:104 msgid "" "Indicates how many headlines an rss feed\n" " will output by default, if no number is provided." msgstr "" "Ilmaisee kuinka monta otsikkoa RSS-syötteen ulostulossa on oletuksena,\n" " jos määrää ei määritetä komennossa." #: config.py:107 msgid "" "Indicates how many headlines an rss feed\n" " will output when it is first added to announce for a channel." msgstr "" "Ilmaisee kuinka monta otsikkoa RSS-syötteen ulostulossa on ensimmäisellä " "kerralla, kun sen kuuluttaminen\n" " lisätään kanavalle." #: config.py:110 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those containing one or more " "items\n" " in this whitelist." msgstr "" "Välilyönneillä erotettu lista merkkiketjuista, sallii otsikoiden " "suodattamisen\n" " yhteen tai useampaan kohteeseen, jotka ovat tällä valkoisella listalla." #: config.py:114 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those not containing any items\n" " in this blacklist." msgstr "" "Välilyönneillä erotettu lista merkkiketjuista,\n" " tämä sallii kohteiden, jotka ovat tällä mustalla listalla, suodattamisen " "pois." #: config.py:127 #, fuzzy msgid "" "Feed-specific format. Defaults to\n" " supybot.plugins.RSS.format if empty." msgstr "Syötekohtainen muoto. Oletuksena supybot.plugins.RSS.format." #: config.py:130 #, fuzzy msgid "" "Feed-specific announce format.\n" " Defaults to supybot.plugins.RSS.announceFormat if empty." msgstr "" "Syötekohtainen kuulutusmuoto. Oletuksena supybot.plugins.RSS.announceFormat." #: config.py:133 msgid "" "If set to a non-zero\n" " value, overrides supybot.plugins.RSS.waitPeriod for this\n" " particular feed." msgstr "" #: plugin.py:140 msgid "" "[]\n" "\n" " Reports the titles for %s at the RSS feed %u. If\n" " is given, returns only that many headlines.\n" " RSS feeds are only looked up every supybot.plugins.RSS.waitPeriod\n" " seconds, which defaults to 1800 (30 minutes) since that's what most\n" " websites prefer." msgstr "" "[]\n" "\n" " Raportoi otsikot kohteelle %s RSS-syötteessä %u. Jos on\n" " annettu, tämä palauttaa vain sen määrän otsikoita. RSS-syötteitä " "tarkistetaan\n" " asetuksen supybot.plugins.RSS.waitPeriod sekuntejen mukaan, joka on\n" " oletuksena 1800 (30 minuuttia), koska se on useimpien verkkosivujen suosima " "aika." #: plugin.py:170 msgid "Return feed items, sorted according to sortFeedItems." msgstr "Palauttaa syötteen kohteet järjestettynä sortFeedItems :in mukaan." #: plugin.py:190 msgid "" "\n" " This plugin is useful both for announcing updates to RSS feeds in a\n" " channel, and for retrieving the headlines of RSS feeds via command. " "Use\n" " the \"add\" command to add feeds to this plugin, and use the \"announce" "\"\n" " command to determine what feeds should be announced in a given channel.\n" "\n" " Basic usage\n" " ^^^^^^^^^^^\n" "\n" " 1. Add a feed using\n" " ``@rss add limnoria https://github.com/progval/Limnoria/tags.atom``.\n" "\n" " * This is RSS feed of Limnoria's stable releases.\n" " * You can now check the latest news from the feed with " "``@limnoria``.\n" "\n" " 2. To have new news automatically announced on the channel, use\n" " ``@rss announce add Limnoria``.\n" "\n" " To add another feed, simply replace limnoria and the address using name\n" " of the feed and address of the feed. For example, YLE News:\n" "\n" " 1. ``@rss add yle http://yle.fi/uutiset/rss/uutiset.rss?osasto=news``\n" " 2. ``@rss announce add yle``\n" "\n" " News on their own lines\n" " ^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " If you want the feed topics to be on their own lines instead of being " "separated by\n" " the separator which you have configured you can set `reply.onetoone` to " "False.\n" "\n" " Please first read the help for that configuration variable\n" "\n" " ``@config help reply.onetoone``\n" "\n" " and understand what it says and then you can do\n" "\n" " ``@config reply.onetoone False``\n" "\n" " " msgstr "" #: plugin.py:278 msgid "I already have a command in this plugin named %s." msgstr "Minulla on jo komento %s tässä lisä-osassa." #: plugin.py:284 msgid "I already have a feed with that URL named %s." msgstr "Minulla on jo syöte nimeltä %s." #: plugin.py:508 msgid "" " \n" "\n" " Adds a command to this plugin that will look up the RSS feed at the\n" " given URL.\n" " " msgstr "" " \n" "\n" " Lisää tähän lisäosaan komennon, joka hakee RSS syötteen annetusta\n" " URL osoitteesta..\n" " " #: plugin.py:521 msgid "" "\n" "\n" " Removes the command for looking up RSS feeds at from\n" " this plugin.\n" " " msgstr "" "\n" "\n" " Poistaa komennon, joka hakee RSS syötteet ,\n" " lisäosasta.\n" " " #: plugin.py:528 msgid "That's not a valid RSS feed command name." msgstr "Tuo ei ole kelvollinen RSS sylte komento nimi." #: plugin.py:547 msgid "" "[]\n" "\n" " Returns the list of feeds announced in . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa listan RSS syötteistä, joita kuulutetaan . " " on\n" " vaadittu vain jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:556 msgid "I am currently not announcing any feeds." msgstr "Minä en tällä hetkellä kuuluta yhtään syötettä." #: plugin.py:561 msgid "" "[] [ ...]\n" "\n" " Adds the list of feeds to the current list of announced feeds " "in\n" " . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Lisää listan syötteistä tällä hetkellä\n" " kuulutettaviin syötteisiin. Kelvolliset syötteet " "sisältävät rekisteröityjen syötteiden nimet,\n" " kuten myös RSS syötteiden URL-osoitteet. on vaadittu " "vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:572 #, fuzzy msgid "These feeds are unknown: %L" msgstr "Nämä syötteet ovat tuntemattomia: %L" #: plugin.py:593 msgid "" "[] [ ...]\n" "\n" " Removes the list of feeds from the current list of announced " "feeds\n" " in . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Poistaa listan RSS syötteistä botin tällä hetkellä " "kuuluttamista\n" " syötteistä. Kelvolliset syötteen nimet sisältävät\n" " URL osoitteet, kuten myös rekisteröidyt RSS syötteet. " "on vaadittu vain, jos viestiä\n" " ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:618 msgid "" "\n" "\n" " Returns a list of channels that the given feed name or URL is " "being\n" " announced to.\n" " " msgstr "" #: plugin.py:642 #, fuzzy msgid "" " []\n" "\n" " Gets the title components of the given RSS feed.\n" " If is given, return only that many headlines.\n" " " msgstr "" " []\n" "\n" " Hakee annetun RSS syötteen otsikko komponentit.\n" " Jos on annettu, palauttaa vain niin monta " "otsikkoa.\n" " " #: plugin.py:658 msgid "Couldn't get RSS feed." msgstr "RSS syötettä ei pystytty hakemaan." #: plugin.py:661 msgid " Parser error: " msgstr "" #: plugin.py:677 msgid "" "\n" "\n" " Returns information from the given RSS feed, namely the title,\n" " URL, description, and last update date, if available.\n" " " msgstr "" "\n" "\n" " Palauttaa tietoja annetusta RSS syötteestä, nimellisesti otsikon,\n" " URL, kuvauksen, ja viimeisen päivityksen, jos saatavilla.\n" " " #: plugin.py:692 msgid "I couldn't retrieve that RSS feed." msgstr "En voinut noutaa tuota RSS syötettä." #: plugin.py:700 #, fuzzy msgid "time unavailable" msgstr "aika ei ole saatavilla" #: plugin.py:701 plugin.py:702 plugin.py:703 msgid "unavailable" msgstr "ei saatavilla" #: plugin.py:705 msgid "Title: %s; URL: %u; Description: %s; Last updated: %s." msgstr "Otsikko: %s; URL: %u; Kuvaus: %s; Viimeeksi päivitetty: %s." #~ msgid "" #~ "This plugin is useful both for announcing updates to RSS feeds in a\n" #~ " channel, and for retrieving the headlines of RSS feeds via command. " #~ "Use\n" #~ " the \"add\" command to add feeds to this plugin, and use the " #~ "\"announce\"\n" #~ " command to determine what feeds should be announced in a given " #~ "channel." #~ msgstr "" #~ "Tämä lisäosa on hyödyllinen kumpaankin, RSS syötteiden päivitysten " #~ "kuuluttamiseen\n" #~ " kanavalla, ja hakemaan RSS syötteen uusimmat otsikot komennon " #~ "kautta. Käytä\n" #~ " \"add\" komentoa lisätäksesi syötteitä tähän lisäosaan ja \"announce" #~ "\"\n" #~ " komentoa määrittämään mitkä syötteet pitäisi kuuluttaa annetulla " #~ "kanavalla." #~ msgid "" #~ "Determines whether the bot will bold the title of the feed when\n" #~ " it announces news." #~ msgstr "Määrittää korostetaanko syötteen otsikko uutisia kuuluttaessa." #~ msgid "" #~ "Determines what\n" #~ " prefix is prepended (if any) to the news item announcements made in " #~ "the\n" #~ " channel." #~ msgstr "" #~ "Määrittää kanavalla tehtyihin uutiskuulutuksiin liitettävän etuliitteen " #~ "(jos mikään)." #~ msgid "News from " #~ msgstr "Uutisia kohteesta " #~ msgid ": " #~ msgstr ": " #~ msgid "" #~ "Determines what\n" #~ " suffix is appended to the feed name in a news item." #~ msgstr "Määrittää uutissyötteen nimeen liitettävän jälkiliitteen." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when the rss command is called.\n" #~ " supybot.plugins.RSS.announce.showLinks affects whether links will be\n" #~ " listed when a feed is automatically announced." #~ msgstr "" #~ "Määrittää luetteleeko botti linkin\n" #~ " syötteen otsikon kanssa, kun rss komentoa kutsutaan.\n" #~ " supybot.plugins.RSS.announce.showLinks vaikuttaa luetellaanko\n" #~ " linkit, kun syöte on kuulutettu automaattisesti." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when a feed is automatically\n" #~ " announced." #~ msgstr "" #~ "Määrittää luetteleeko botti linkin\n" #~ " syötteen otsikon kautta, kun syöte kuulutetaan\n" #~ " automaattisesti." �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/locales/fr.po�������������������������������������������������������0000644�0001750�0001750�00000031635�14535072470�016651� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:50 msgid "Valid values include 'asInFeed', 'oldestFirst', 'newestFirst'." msgstr "Les valeurs valides sont 'asInFeed', 'oldestFirst', et 'newestFirst'." #: config.py:57 msgid "" "Determines what feeds should be accessible as\n" " commands." msgstr "Détermine quels flux sont accessibles en tant que commande." #: config.py:64 msgid "" "Determines what string is\n" " used to separate headlines in new feeds." msgstr "" "Détermine quelle chaîne est utilisé pour séparer les titres dans les " "nouveaux flux." #: config.py:67 msgid "$date: $title <$link>" msgstr "" #: config.py:67 msgid "" "The format the bot\n" " will use for displaying headlines of a RSS feed that is triggered\n" " manually. In addition to fields defined by feedparser ($published\n" " (the entry date), $title, $link, $description, $id, etc.), the " "following\n" " variables can be used: $feed_name, $date (parsed date, as defined in\n" " supybot.reply.format.time)" msgstr "" #: config.py:74 msgid "News from $feed_name: $title <$link>" msgstr "" #: config.py:75 msgid "" "The format the bot will use for displaying headlines of a RSS feed\n" " that is announced. See supybot.plugins.RSS.format for the available\n" " variables." msgstr "" #: config.py:83 msgid "" "Determines which RSS feeds\n" " should be announced in the channel; valid input is a list of strings\n" " (either registered RSS feeds or RSS feed URLs) separated by spaces." msgstr "" "Détermine quels flux RSS seront annoncés sur le canal ; une entrée valide " "est une liste de chaînes (des flux enregistrés ou des URLs de flux RSS), " "séparées par des espaces." #: config.py:87 msgid "" "Indicates how many seconds the bot will\n" " wait between retrieving RSS feeds; requests made within this period " "will\n" " return cached results." msgstr "" "Détermine le temps (en secondes) entre deux rafraichissement des flux RSS. " "Durant cette période, les flux seront mis en cache." #: config.py:91 #, fuzzy msgid "" "Determines whether feed items should be\n" " sorted by their publication/update timestamp or kept in the same order " "as\n" " they appear in a feed." msgstr "" "Détermine si les éléments du flux doivent être triés selon la date de leur " "mise à jour ou si ils doivent être conservés dans l'ordre original du flux." #: config.py:95 msgid "" "Determines whether announces will be sent\n" " as notices instead of privmsgs." msgstr "" #: config.py:98 msgid "" "Indicates how many new news entries may\n" " be sent at the same time. Extra entries will be discarded." msgstr "" #: config.py:104 msgid "" "Indicates how many headlines an rss feed\n" " will output by default, if no number is provided." msgstr "" "Indique combien d'éléments un flux rss affichera par défaut, si aucun nombre " "n'est donné." #: config.py:107 msgid "" "Indicates how many headlines an rss feed\n" " will output when it is first added to announce for a channel." msgstr "" "Indique combien d'éléments un flux rss affichera lorsque qu'il vient d'être " "configuré pour être annoncé sur le salon." #: config.py:110 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those containing one or more " "items\n" " in this whitelist." msgstr "" "Liste séparée par des espaces de chaînes, qui vous permet de filtrer les " "éléments par liste blanche." #: config.py:114 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those not containing any items\n" " in this blacklist." msgstr "" "Liste séparée par des espaces de chaînes, qui vous permet de filtrer les " "éléments par liste noire." #: config.py:127 msgid "" "Feed-specific format. Defaults to\n" " supybot.plugins.RSS.format if empty." msgstr "" #: config.py:130 msgid "" "Feed-specific announce format.\n" " Defaults to supybot.plugins.RSS.announceFormat if empty." msgstr "" #: config.py:133 msgid "" "If set to a non-zero\n" " value, overrides supybot.plugins.RSS.waitPeriod for this\n" " particular feed." msgstr "" #: plugin.py:140 msgid "" "[]\n" "\n" " Reports the titles for %s at the RSS feed %u. If\n" " is given, returns only that many headlines.\n" " RSS feeds are only looked up every supybot.plugins.RSS.waitPeriod\n" " seconds, which defaults to 1800 (30 minutes) since that's what most\n" " websites prefer." msgstr "" #: plugin.py:170 msgid "Return feed items, sorted according to sortFeedItems." msgstr "." #: plugin.py:190 msgid "" "\n" " This plugin is useful both for announcing updates to RSS feeds in a\n" " channel, and for retrieving the headlines of RSS feeds via command. " "Use\n" " the \"add\" command to add feeds to this plugin, and use the \"announce" "\"\n" " command to determine what feeds should be announced in a given channel.\n" "\n" " Basic usage\n" " ^^^^^^^^^^^\n" "\n" " 1. Add a feed using\n" " ``@rss add limnoria https://github.com/progval/Limnoria/tags.atom``.\n" "\n" " * This is RSS feed of Limnoria's stable releases.\n" " * You can now check the latest news from the feed with " "``@limnoria``.\n" "\n" " 2. To have new news automatically announced on the channel, use\n" " ``@rss announce add Limnoria``.\n" "\n" " To add another feed, simply replace limnoria and the address using name\n" " of the feed and address of the feed. For example, YLE News:\n" "\n" " 1. ``@rss add yle http://yle.fi/uutiset/rss/uutiset.rss?osasto=news``\n" " 2. ``@rss announce add yle``\n" "\n" " News on their own lines\n" " ^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " If you want the feed topics to be on their own lines instead of being " "separated by\n" " the separator which you have configured you can set `reply.onetoone` to " "False.\n" "\n" " Please first read the help for that configuration variable\n" "\n" " ``@config help reply.onetoone``\n" "\n" " and understand what it says and then you can do\n" "\n" " ``@config reply.onetoone False``\n" "\n" " " msgstr "" #: plugin.py:278 msgid "I already have a command in this plugin named %s." msgstr "" #: plugin.py:284 msgid "I already have a feed with that URL named %s." msgstr "" #: plugin.py:508 msgid "" " \n" "\n" " Adds a command to this plugin that will look up the RSS feed at the\n" " given URL.\n" " " msgstr "" " \n" "\n" "Ajoute un commande à ce plugin qui permet de regarder le flux situé à " "l'." #: plugin.py:521 msgid "" "\n" "\n" " Removes the command for looking up RSS feeds at from\n" " this plugin.\n" " " msgstr "" "\n" "\n" "Supprime le flux des flux qui peuvent être lus grâce à une commande." #: plugin.py:528 msgid "That's not a valid RSS feed command name." msgstr "Ce n'est pas une commande de flux RSS valide" #: plugin.py:547 msgid "" "[]\n" "\n" " Returns the list of feeds announced in . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne la liste des flux annoncés sur le . n'est nécessaire " "que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:556 msgid "I am currently not announcing any feeds." msgstr "Je n'annonce actuellement aucun flux." #: plugin.py:561 msgid "" "[] [ ...]\n" "\n" " Adds the list of feeds to the current list of announced feeds " "in\n" " . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Ajoute la liste de flux à la liste actuelle des flux annoncés sur le " ". Vous devez indiquer le du flux si il est déjà enregistré, ou " "l' dans le cas contraire. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:572 msgid "These feeds are unknown: %L" msgstr "" #: plugin.py:593 msgid "" "[] [ ...]\n" "\n" " Removes the list of feeds from the current list of announced " "feeds\n" " in . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Supprime la liste de flux de la liste actuelle des flux annoncés sur le " ". Vous devez indiquer le du flux si il est déjà enregistré, ou " "l' dans le cas contraire. n'est nécessaire que si le message " "n'est pas envoyé sur le canal lui-même." #: plugin.py:618 msgid "" "\n" "\n" " Returns a list of channels that the given feed name or URL is " "being\n" " announced to.\n" " " msgstr "" #: plugin.py:642 #, fuzzy msgid "" " []\n" "\n" " Gets the title components of the given RSS feed.\n" " If is given, return only that many headlines.\n" " " msgstr "" " []\n" "\n" "Récupère le titre des éléments du flux RSS donné. si le " "est donné, ne retourne que ce nombre de lignes d'en-tête." #: plugin.py:658 msgid "Couldn't get RSS feed." msgstr "Ne peut récupérer le flux RSS." #: plugin.py:661 msgid " Parser error: " msgstr "" #: plugin.py:677 msgid "" "\n" "\n" " Returns information from the given RSS feed, namely the title,\n" " URL, description, and last update date, if available.\n" " " msgstr "" "\n" "\n" "Retourne des informations sur le flux RSS donné : le titre, l'URL, la " "description, et la dernière mise à jour." #: plugin.py:692 msgid "I couldn't retrieve that RSS feed." msgstr "Je ne peux récupérer ce flux RSS." #: plugin.py:700 msgid "time unavailable" msgstr "" #: plugin.py:701 plugin.py:702 plugin.py:703 msgid "unavailable" msgstr "" #: plugin.py:705 msgid "Title: %s; URL: %u; Description: %s; Last updated: %s." msgstr "Titre : %s , URL : %u ; description : %s ; dernière mise à jour : %s." #~ msgid "" #~ "Determines whether the bot will bold the title of the feed when\n" #~ " it announces news." #~ msgstr "" #~ "Détermine si le bot mettera en gras le titre des flux lorsqu'il annoncera " #~ "des news." #~ msgid "" #~ "Determines what\n" #~ " prefix is prepended (if any) to the news item announcements made in " #~ "the\n" #~ " channel." #~ msgstr "" #~ "Détermine quel préfixe (s'il y en a un) est utilisé pour annoncer les " #~ "news sur le canal." #~ msgid "New news from " #~ msgstr "Nouvelle(s) news de " #~ msgid ": " #~ msgstr " : " #~ msgid "" #~ "Determines what\n" #~ " suffix is appended to the feed name in a news item." #~ msgstr "" #~ "Détermine quel suffixe est utilisé pour annoncer les news sur le canal." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when the rss command is called.\n" #~ " supybot.plugins.RSS.announce.showLinks affects whether links will be\n" #~ " listed when a feed is automatically announced." #~ msgstr "" #~ "Détermine si le bot listera le lien de chaque flus avec son titre, " #~ "lorsque la commande rss est appelée. supybot.plugins.RSS.announce." #~ "showLinks affecte si les liens sont affichés lorsqu'un flux est annoncé " #~ "automatiquement." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when a feed is automatically\n" #~ " announced." #~ msgstr "" #~ "Détermine si le bot listera le lien de chaque flux avec le titre " #~ "lorsqu'un flux est automatiquement annoncé." #~ msgid "" #~ "This plugin is useful both for announcing updates to RSS feeds in a\n" #~ " channel, and for retrieving the headlines of RSS feeds via command. " #~ "Use\n" #~ " the \"add\" command to add feeds to this plugin, and use the " #~ "\"announce\"\n" #~ " command to determine what feeds should be announced in a given " #~ "channel." #~ msgstr "" #~ "Ce plugin est utile pour annoncer des flux RSS sur un canal, et pour " #~ "récupérer les en-tête des flux RSS via une commande. Utilisez la commande " #~ "\"add\" pour ajouter des flux au plugin, et utilisez la commande \"annonce" #~ "\" pour détermine quels flux pourront être annoncés sur un canal donné." ���������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/locales/hu.po�������������������������������������������������������0000644�0001750�0001750�00000031311�14535072470�016645� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Limnoria RSS plugin. # Copyright (C) YEAR ORGANIZATION # nyuszika7h , 2011-2014. # msgid "" msgstr "" "Project-Id-Version: Limnoria RSS\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-05-10 23:46+0200\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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" #: config.py:50 msgid "Valid values include 'asInFeed', 'oldestFirst', 'newestFirst'." msgstr "" #: config.py:57 msgid "" "Determines what feeds should be accessible as\n" " commands." msgstr "" "Meghatározza, hogy melyik hírcsatornák legyenek elérhetők parancsokként." #: config.py:64 msgid "" "Determines what string is\n" " used to separate headlines in new feeds." msgstr "" "Meghatározza, hogy milyen karakterlánc használt a főcímek elválasztására az " "új hírcsatornákban." #: config.py:67 msgid "$date: $title <$link>" msgstr "" #: config.py:67 msgid "" "The format the bot\n" " will use for displaying headlines of a RSS feed that is triggered\n" " manually. In addition to fields defined by feedparser ($published\n" " (the entry date), $title, $link, $description, $id, etc.), the " "following\n" " variables can be used: $feed_name, $date (parsed date, as defined in\n" " supybot.reply.format.time)" msgstr "" #: config.py:74 msgid "News from $feed_name: $title <$link>" msgstr "" #: config.py:75 msgid "" "The format the bot will use for displaying headlines of a RSS feed\n" " that is announced. See supybot.plugins.RSS.format for the available\n" " variables." msgstr "" #: config.py:83 msgid "" "Determines which RSS feeds\n" " should be announced in the channel; valid input is a list of strings\n" " (either registered RSS feeds or RSS feed URLs) separated by spaces." msgstr "" "Meghatározza, hogy melyik RSS hírcsatornák legyenek bejelentve a " "csatornában; az érvényes bemenet: karakterláncok (regisztrált RSS " "hírcsatornák vagy RSS hírcsatorna hivatkozások) szóközzel elválasztott " "listája." #: config.py:87 msgid "" "Indicates how many seconds the bot will\n" " wait between retrieving RSS feeds; requests made within this period " "will\n" " return cached results." msgstr "" "Azt jelzi, hány másodpercet vár a bot az RSS hírcsatornák letöltése között; " "az ebben az időszakban végrehajtott kérések gyorsítótárazott eredményeket " "adnak vissza." #: config.py:91 msgid "" "Determines whether feed items should be\n" " sorted by their publication/update timestamp or kept in the same order " "as\n" " they appear in a feed." msgstr "" #: config.py:95 msgid "" "Determines whether announces will be sent\n" " as notices instead of privmsgs." msgstr "" #: config.py:98 msgid "" "Indicates how many new news entries may\n" " be sent at the same time. Extra entries will be discarded." msgstr "" #: config.py:104 msgid "" "Indicates how many headlines an rss feed\n" " will output by default, if no number is provided." msgstr "" #: config.py:107 msgid "" "Indicates how many headlines an rss feed\n" " will output when it is first added to announce for a channel." msgstr "" #: config.py:110 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those containing one or more " "items\n" " in this whitelist." msgstr "" #: config.py:114 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those not containing any items\n" " in this blacklist." msgstr "" #: config.py:127 msgid "" "Feed-specific format. Defaults to\n" " supybot.plugins.RSS.format if empty." msgstr "" #: config.py:130 msgid "" "Feed-specific announce format.\n" " Defaults to supybot.plugins.RSS.announceFormat if empty." msgstr "" #: config.py:133 msgid "" "If set to a non-zero\n" " value, overrides supybot.plugins.RSS.waitPeriod for this\n" " particular feed." msgstr "" #: plugin.py:140 msgid "" "[]\n" "\n" " Reports the titles for %s at the RSS feed %u. If\n" " is given, returns only that many headlines.\n" " RSS feeds are only looked up every supybot.plugins.RSS.waitPeriod\n" " seconds, which defaults to 1800 (30 minutes) since that's what most\n" " websites prefer." msgstr "" #: plugin.py:170 msgid "Return feed items, sorted according to sortFeedItems." msgstr "" #: plugin.py:190 msgid "" "\n" " This plugin is useful both for announcing updates to RSS feeds in a\n" " channel, and for retrieving the headlines of RSS feeds via command. " "Use\n" " the \"add\" command to add feeds to this plugin, and use the \"announce" "\"\n" " command to determine what feeds should be announced in a given channel.\n" "\n" " Basic usage\n" " ^^^^^^^^^^^\n" "\n" " 1. Add a feed using\n" " ``@rss add limnoria https://github.com/progval/Limnoria/tags.atom``.\n" "\n" " * This is RSS feed of Limnoria's stable releases.\n" " * You can now check the latest news from the feed with " "``@limnoria``.\n" "\n" " 2. To have new news automatically announced on the channel, use\n" " ``@rss announce add Limnoria``.\n" "\n" " To add another feed, simply replace limnoria and the address using name\n" " of the feed and address of the feed. For example, YLE News:\n" "\n" " 1. ``@rss add yle http://yle.fi/uutiset/rss/uutiset.rss?osasto=news``\n" " 2. ``@rss announce add yle``\n" "\n" " News on their own lines\n" " ^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " If you want the feed topics to be on their own lines instead of being " "separated by\n" " the separator which you have configured you can set `reply.onetoone` to " "False.\n" "\n" " Please first read the help for that configuration variable\n" "\n" " ``@config help reply.onetoone``\n" "\n" " and understand what it says and then you can do\n" "\n" " ``@config reply.onetoone False``\n" "\n" " " msgstr "" #: plugin.py:278 msgid "I already have a command in this plugin named %s." msgstr "" #: plugin.py:284 msgid "I already have a feed with that URL named %s." msgstr "" #: plugin.py:508 msgid "" " \n" "\n" " Adds a command to this plugin that will look up the RSS feed at the\n" " given URL.\n" " " msgstr "" " \n" "\n" "Hozzáad egy parancsot ehhez a bővítményhez, amely le fogja kérdezni az RSS " "hírcsatornát a megadott hivatkozáson." #: plugin.py:521 msgid "" "\n" "\n" " Removes the command for looking up RSS feeds at from\n" " this plugin.\n" " " msgstr "" "\n" "\n" "Eltávolítja a RSS hírcsatornáinak lekérdezéséhez a bővítményből." #: plugin.py:528 msgid "That's not a valid RSS feed command name." msgstr "Ez nem egy érvényes RSS hírcsatorna parancsnév." #: plugin.py:547 msgid "" "[]\n" "\n" " Returns the list of feeds announced in . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Kiírja a -n bejelentett hírcsatornákat. csak akkor " "szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:556 msgid "I am currently not announcing any feeds." msgstr "Jelenleg nem jelentek be semmilyen hírcsatornát." #: plugin.py:561 msgid "" "[] [ ...]\n" "\n" " Adds the list of feeds to the current list of announced feeds " "in\n" " . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Hozzáadja a hírcsatornák listáját a bejelentett hírcsatornák listájához " "-ban. Érvényes hírcsatornák közé tartoznak a regisztrált " "hírcsatornák nevei és az RSS hírcsatornák hivatkozásai. csak " "akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:572 msgid "These feeds are unknown: %L" msgstr "" #: plugin.py:593 msgid "" "[] [ ...]\n" "\n" " Removes the list of feeds from the current list of announced " "feeds\n" " in . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" "Eltávolítja a hírcsatornák listáját a jelenleg bejelentett hírcsatornák " "listájából -ban. Érvényes hírcsatornák közé tartoznak a " "regisztrált hírcsatornák nevei és az RSS hírcsatornák hivatkozásai. " " csak akkor szükséges, ha az üzenet nem a csatornában van elküldve." #: plugin.py:618 msgid "" "\n" "\n" " Returns a list of channels that the given feed name or URL is " "being\n" " announced to.\n" " " msgstr "" #: plugin.py:642 #, fuzzy msgid "" " []\n" "\n" " Gets the title components of the given RSS feed.\n" " If is given, return only that many headlines.\n" " " msgstr "" " []\n" "\n" "Lekérdezi a főcímeket a megadott RSS hírcsatornából. Ha meg " "van adva, csak annyi főcímet ír ki." #: plugin.py:658 msgid "Couldn't get RSS feed." msgstr "Nem sikerült elérni az RSS hírcsatornát." #: plugin.py:661 msgid " Parser error: " msgstr "" #: plugin.py:677 msgid "" "\n" "\n" " Returns information from the given RSS feed, namely the title,\n" " URL, description, and last update date, if available.\n" " " msgstr "" "\n" "\n" "Információt ír ki a megadott RSS hírcsatornáról, név szerint a címet, a " "hivatkozást, a leírást és a legutóbbi frissítés idejét, ha elérhető." #: plugin.py:692 msgid "I couldn't retrieve that RSS feed." msgstr "Nem tudtam lekérdezni a megadott RSS hírcsatornát." #: plugin.py:700 msgid "time unavailable" msgstr "" #: plugin.py:701 plugin.py:702 plugin.py:703 msgid "unavailable" msgstr "" #: plugin.py:705 msgid "Title: %s; URL: %u; Description: %s; Last updated: %s." msgstr "Cím: %s; Hivatkozás: %u; Leírás: %s; Legutóbbi frissítés: %s." #~ msgid "" #~ "Determines whether the bot will bold the title of the feed when\n" #~ " it announces news." #~ msgstr "" #~ "Meghatározza, hogy a bot félkövér betűvel írja-e ki a hírcsatorna címét, " #~ "amikor bejelenti a híreket." #~ msgid "" #~ "Determines what\n" #~ " prefix is prepended (if any) to the news item announcements made in " #~ "the\n" #~ " channel." #~ msgstr "" #~ "Meghatározza, hogy milyen előtag (ha van ilyen) legyen a csatornában " #~ "bejelentett hírek előtt." #~ msgid "News from " #~ msgstr "Hírek innen: " #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when the rss command is called.\n" #~ " supybot.plugins.RSS.announce.showLinks affects whether links will be\n" #~ " listed when a feed is automatically announced." #~ msgstr "" #~ "Meghatározza, hogy a bot kiírja-e a hírcsatorna hivatkozását a címmel " #~ "együtt az rss command meghívásakor. A supybot.plugins.RSS.announce." #~ "showLinks befolyásolja, hogy a linkek ki legyenek-e írva, amikor egy " #~ "hírcsatorna automatikusan be van jelentve." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when a feed is automatically\n" #~ " announced." #~ msgstr "" #~ "Meghatározza, hogy a bot kiírja-e a hírcsatorna hivatkozását a címmel " #~ "együtt amikor egy hírcsatorna automatikusan be van jelentve." #~ msgid "" #~ "This plugin is useful both for announcing updates to RSS feeds in a\n" #~ " channel, and for retrieving the headlines of RSS feeds via command. " #~ "Use\n" #~ " the \"add\" command to add feeds to this plugin, and use the " #~ "\"announce\"\n" #~ " command to determine what feeds should be announced in a given " #~ "channel." #~ msgstr "" #~ "Ez a bővítmény hasznos mind az RSS hírcsatornák frissítéseinek " #~ "bejelentésére egy csatornában, és az RSS hírcsatornák főhivatkozásainek " #~ "letöltésére egy parancssal. Használd az \"add\" parancsot hírcsatornák " #~ "hozzáadásához, és használd az \"announce\" parancsot, hogy meghatározd, " #~ "milyen hírcsatornák legyenek bejelentve a megadott csatornában." �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131000.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/locales/it.po�������������������������������������������������������0000644�0001750�0001750�00000030670�14535072470�016654� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-06-03 04:49+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 "Valid values include 'asInFeed', 'oldestFirst', 'newestFirst'." msgstr "I valori validi sono 'asInFeed', 'oldestFirst', 'newestFirst'." #: config.py:57 msgid "" "Determines what feeds should be accessible as\n" " commands." msgstr "Determina quali feed siano accessibili come comandi." #: config.py:64 msgid "" "Determines what string is\n" " used to separate headlines in new feeds." msgstr "" "Determina quale stringa utilizzare per separare i titoli nei nuovi feed." #: config.py:67 msgid "$date: $title <$link>" msgstr "" #: config.py:67 msgid "" "The format the bot\n" " will use for displaying headlines of a RSS feed that is triggered\n" " manually. In addition to fields defined by feedparser ($published\n" " (the entry date), $title, $link, $description, $id, etc.), the " "following\n" " variables can be used: $feed_name, $date (parsed date, as defined in\n" " supybot.reply.format.time)" msgstr "" #: config.py:74 msgid "News from $feed_name: $title <$link>" msgstr "" #: config.py:75 msgid "" "The format the bot will use for displaying headlines of a RSS feed\n" " that is announced. See supybot.plugins.RSS.format for the available\n" " variables." msgstr "" #: config.py:83 msgid "" "Determines which RSS feeds\n" " should be announced in the channel; valid input is a list of strings\n" " (either registered RSS feeds or RSS feed URLs) separated by spaces." msgstr "" "Determina quali feed RSS sono annunciati in canale; una voce valida è un " "elenco di stringhe\n" " (sia di feed registrati sia di URL di feed) separate da spazi." #: config.py:87 msgid "" "Indicates how many seconds the bot will\n" " wait between retrieving RSS feeds; requests made within this period " "will\n" " return cached results." msgstr "" "Indica ogni quanti secondi il bot recupererà i feed RSS; le richieste " "effettuate\n" " entro questo periodo verranno memorizzate nella cache." #: config.py:91 #, fuzzy msgid "" "Determines whether feed items should be\n" " sorted by their publication/update timestamp or kept in the same order " "as\n" " they appear in a feed." msgstr "" "Determina se gli elementi del feed debbano essere ordinati per data di " "aggiornamento\n" " o mantenuti nello stesso ordine con il quale appaiono." #: config.py:95 msgid "" "Determines whether announces will be sent\n" " as notices instead of privmsgs." msgstr "" #: config.py:98 msgid "" "Indicates how many new news entries may\n" " be sent at the same time. Extra entries will be discarded." msgstr "" #: config.py:104 msgid "" "Indicates how many headlines an rss feed\n" " will output by default, if no number is provided." msgstr "" #: config.py:107 msgid "" "Indicates how many headlines an rss feed\n" " will output when it is first added to announce for a channel." msgstr "" #: config.py:110 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those containing one or more " "items\n" " in this whitelist." msgstr "" #: config.py:114 msgid "" "Space separated list of \n" " strings, lets you filter headlines to those not containing any items\n" " in this blacklist." msgstr "" #: config.py:127 msgid "" "Feed-specific format. Defaults to\n" " supybot.plugins.RSS.format if empty." msgstr "" #: config.py:130 msgid "" "Feed-specific announce format.\n" " Defaults to supybot.plugins.RSS.announceFormat if empty." msgstr "" #: config.py:133 msgid "" "If set to a non-zero\n" " value, overrides supybot.plugins.RSS.waitPeriod for this\n" " particular feed." msgstr "" #: plugin.py:140 msgid "" "[]\n" "\n" " Reports the titles for %s at the RSS feed %u. If\n" " is given, returns only that many headlines.\n" " RSS feeds are only looked up every supybot.plugins.RSS.waitPeriod\n" " seconds, which defaults to 1800 (30 minutes) since that's what most\n" " websites prefer." msgstr "" #: plugin.py:170 msgid "Return feed items, sorted according to sortFeedItems." msgstr "Riporta gli elementi del feed in base a sortFeedItems." #: plugin.py:190 msgid "" "\n" " This plugin is useful both for announcing updates to RSS feeds in a\n" " channel, and for retrieving the headlines of RSS feeds via command. " "Use\n" " the \"add\" command to add feeds to this plugin, and use the \"announce" "\"\n" " command to determine what feeds should be announced in a given channel.\n" "\n" " Basic usage\n" " ^^^^^^^^^^^\n" "\n" " 1. Add a feed using\n" " ``@rss add limnoria https://github.com/progval/Limnoria/tags.atom``.\n" "\n" " * This is RSS feed of Limnoria's stable releases.\n" " * You can now check the latest news from the feed with " "``@limnoria``.\n" "\n" " 2. To have new news automatically announced on the channel, use\n" " ``@rss announce add Limnoria``.\n" "\n" " To add another feed, simply replace limnoria and the address using name\n" " of the feed and address of the feed. For example, YLE News:\n" "\n" " 1. ``@rss add yle http://yle.fi/uutiset/rss/uutiset.rss?osasto=news``\n" " 2. ``@rss announce add yle``\n" "\n" " News on their own lines\n" " ^^^^^^^^^^^^^^^^^^^^^^^\n" "\n" " If you want the feed topics to be on their own lines instead of being " "separated by\n" " the separator which you have configured you can set `reply.onetoone` to " "False.\n" "\n" " Please first read the help for that configuration variable\n" "\n" " ``@config help reply.onetoone``\n" "\n" " and understand what it says and then you can do\n" "\n" " ``@config reply.onetoone False``\n" "\n" " " msgstr "" #: plugin.py:278 msgid "I already have a command in this plugin named %s." msgstr "" #: plugin.py:284 msgid "I already have a feed with that URL named %s." msgstr "" #: plugin.py:508 msgid "" " \n" "\n" " Adds a command to this plugin that will look up the RSS feed at the\n" " given URL.\n" " " msgstr "" " \n" "\n" " Aggiunge un comando a questo plugin che cercherà i feed RSS all'URL " "specificato.\n" " " #: plugin.py:521 msgid "" "\n" "\n" " Removes the command for looking up RSS feeds at from\n" " this plugin.\n" " " msgstr "" "\n" "\n" " Rimuove il comando per cercare feed RSS con .\n" " " #: plugin.py:528 msgid "That's not a valid RSS feed command name." msgstr "Questo non è un comando di feed RSS valido." #: plugin.py:547 msgid "" "[]\n" "\n" " Returns the list of feeds announced in . is\n" " only necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Restituisce l'elenco dei feed annunciati in . " "è\n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:556 msgid "I am currently not announcing any feeds." msgstr "Attualmente non sto annunciando alcun feed." #: plugin.py:561 msgid "" "[] [ ...]\n" "\n" " Adds the list of feeds to the current list of announced feeds " "in\n" " . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Aggiunge l'elenco dei feed all'attuale elenco di quelli da " "annunciare in\n" " . Valori validi includono sia i nomi dei feed registrati " "sia i loro URL.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:572 msgid "These feeds are unknown: %L" msgstr "" #: plugin.py:593 msgid "" "[] [ ...]\n" "\n" " Removes the list of feeds from the current list of announced " "feeds\n" " in . Valid feeds include the names of registered feeds " "as\n" " well as URLs for RSS feeds. is only necessary if the\n" " message isn't sent in the channel itself.\n" " " msgstr "" "[] [ ...]\n" "\n" " Rimuove l'elenco dei feed dall'attuale elenco dei feed da " "annunciare in.\n" " . Valori validi includono sia i nomi dei feed registrati " "sia i loro URL.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:618 msgid "" "\n" "\n" " Returns a list of channels that the given feed name or URL is " "being\n" " announced to.\n" " " msgstr "" #: plugin.py:642 #, fuzzy msgid "" " []\n" "\n" " Gets the title components of the given RSS feed.\n" " If is given, return only that many headlines.\n" " " msgstr "" " []\n" "\n" " Recupera i titoli del feed RSS specificato.\n" " Se è fornito, restituisce solo quella quantità.\n" " " #: plugin.py:658 msgid "Couldn't get RSS feed." msgstr "Impossibile recuperare il feed RSS." #: plugin.py:661 msgid " Parser error: " msgstr "" #: plugin.py:677 msgid "" "\n" "\n" " Returns information from the given RSS feed, namely the title,\n" " URL, description, and last update date, if available.\n" " " msgstr "" "\n" "\n" " Riporta informazioni sul feed RSS specificato: titolo,\n" " URL, descrizione e data dell'ultimo aggiornamento.\n" " " #: plugin.py:692 msgid "I couldn't retrieve that RSS feed." msgstr "Non riesco a recuperare questo feed RSS." #: plugin.py:700 msgid "time unavailable" msgstr "" #: plugin.py:701 plugin.py:702 plugin.py:703 msgid "unavailable" msgstr "" #: plugin.py:705 msgid "Title: %s; URL: %u; Description: %s; Last updated: %s." msgstr "Titolo: %s; URL: %u; Descrizione: %s; Ultimo aggiornamento: %s." #, fuzzy #~ msgid "" #~ "Determines whether the bot will bold the title of the feed when\n" #~ " it announces news." #~ msgstr "" #~ "Determina se il bot mostrerà il titolo del feed in grassetto quando " #~ "annuncia nuove notizie." #, fuzzy #~ msgid "" #~ "Determines what\n" #~ " prefix is prepended (if any) to the news item announcements made in " #~ "the\n" #~ " channel." #~ msgstr "" #~ "Determina quale prefisso (eventuale) utilizzare per annunciare le notizie " #~ "in canale." #, fuzzy #~ msgid "News from " #~ msgstr "Nuove notizie da " #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when the rss command is called.\n" #~ " supybot.plugins.RSS.announce.showLinks affects whether links will be\n" #~ " listed when a feed is automatically announced." #~ msgstr "" #~ "Determina se il bot elencherà i link con il titolo del feed quando viene " #~ "richiamato\n" #~ " il comando \"rss\". La variabile supybot.plugins.RSS.announce." #~ "showLinks determina\n" #~ " se i link saranno mostrati quando un feed è annunciato " #~ "automaticamente." #~ msgid "" #~ "Determines whether the bot will list the link\n" #~ " along with the title of the feed when a feed is automatically\n" #~ " announced." #~ msgstr "" #~ "Determina se il bot elencherà i link con il titolo del feed quando questo " #~ "è annunciato automaticamente." #~ msgid "" #~ "This plugin is useful both for announcing updates to RSS feeds in a\n" #~ " channel, and for retrieving the headlines of RSS feeds via command. " #~ "Use\n" #~ " the \"add\" command to add feeds to this plugin, and use the " #~ "\"announce\"\n" #~ " command to determine what feeds should be announced in a given " #~ "channel." #~ msgstr "" #~ "Questo plugin è utile sia per annunciare feed RSS in un canale, sia per " #~ "recuperare\n" #~ " i titoli dei feed tramite un comando. Utilizza il comando \"add\" per " #~ "aggiungere\n" #~ " feed e \"announce\" per determinare quali feed devono essere " #~ "annunciati in un dato canale." ������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/plugin.py�����������������������������������������������������������0000644�0001750�0001750�00000073170�14535072477�016137� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import os import sys import json import time import types import string import socket import threading import feedparser import supybot.log as log import supybot.conf as conf import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.utils.minisix as minisix import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('RSS') if world.testing: INIT_DELAY = 1 else: INIT_DELAY = 10 if minisix.PY2: from urllib2 import ProxyHandler else: from urllib.request import ProxyHandler from .config import register_feed_config def get_feedName(irc, msg, args, state): if irc.isChannel(args[0]): state.errorInvalid('feed name', args[0], 'must not be channel names.') if not registry.isValidRegistryName(args[0]): state.errorInvalid('feed name', args[0], 'Feed names must not include spaces.') if "." in args[0]: state.errorInvalid('feed name', args[0], 'Feed names must not include dots.') state.args.append(callbacks.canonicalName(args.pop(0))) addConverter('feedName', get_feedName) announced_headlines_filename = \ conf.supybot.directories.data.dirize('RSS_announced.flat') def only_one_at_once(f): lock = [False] def newf(*args, **kwargs): if lock[0]: return lock[0] = True try: f(*args, **kwargs) finally: lock[0] = False return newf def get_entry_id(entry): # in order, try elements to use as unique identifier. # http://validator.w3.org/feed/docs/rss2.html#hrelementsOfLtitemgt id_elements = ('id', 'link', 'title', 'description') for id_element in id_elements: try: return getattr(entry, id_element) except AttributeError: pass raise ValueError('Feed entry is missing both title and description') class InvalidFeedUrl(ValueError): pass class Feed: __slots__ = ('url', 'name', 'data', 'last_update', 'entries', 'etag', 'modified', 'initial', 'lock', 'announced_entries', 'last_exception') def __init__(self, name, url, initial, plugin_is_loading=False, announced=None): assert name, name if not url: url = name if not utils.web.httpUrlRe.match(url): raise InvalidFeedUrl(url) self.name = name self.url = url self.initial = initial self.data = None # We don't want to fetch feeds right after the plugin is # loaded (the bot could be starting, and thus already busy) self.last_update = 0 self.entries = [] self.etag = None self.modified = None self.lock = threading.Lock() self.announced_entries = announced or \ utils.structures.TruncatableSet() self.last_exception = None def __repr__(self): return 'Feed(%r, %r, %r, , %r)' % \ (self.name, self.url, self.initial, self.announced_entries) def get_command(self, plugin): docstring = format(_("""[] Reports the titles for %s at the RSS feed %u. If is given, returns only that many headlines. RSS feeds are only looked up every supybot.plugins.RSS.waitPeriod seconds, which defaults to 1800 (30 minutes) since that's what most websites prefer."""), self.name, self.url) def f(self2, irc, msg, args): args.insert(0, self.name) self2.rss(irc, msg, args) f = utils.python.changeFunctionName(f, self.name, docstring) f = types.MethodType(f, plugin) return f _sort_parameters = { 'oldestFirst': (('published_parsed', 'updated_parsed'), False), 'newestFirst': (('published_parsed', 'updated_parsed'), True), 'outdatedFirst': (('updated_parsed', 'published_parsed'), False), 'updatedFirst': (('updated_parsed', 'published_parsed'), True), } def _sort_arguments(order): (fields, reverse) = _sort_parameters[order] def key(entry): for field in fields: if field in entry: return entry[field] raise KeyError('No date field in entry.') return (key, reverse) def sort_feed_items(items, order): """Return feed items, sorted according to sortFeedItems.""" if order == 'asInFeed': return items (key, reverse) = _sort_arguments(order) try: sitems = sorted(items, key=key, reverse=reverse) except KeyError: # feedparser normalizes required timestamp fields in ATOM and RSS # to the "published"/"updated" fields. Feeds missing it are unsortable by date. return items return sitems def load_announces_db(fd): return dict((name, utils.structures.TruncatableSet(entries)) for (name, entries) in json.load(fd).items()) def save_announces_db(db, fd): json.dump(dict((name, list(entries)) for (name, entries) in db), fd) class RSS(callbacks.Plugin): """ This plugin is useful both for announcing updates to RSS feeds in a channel, and for retrieving the headlines of RSS feeds via command. Use the "add" command to add feeds to this plugin, and use the "announce" command to determine what feeds should be announced in a given channel. Basic usage ^^^^^^^^^^^ 1. Add a feed using ``@rss add limnoria https://github.com/progval/Limnoria/tags.atom``. * This is RSS feed of Limnoria's stable releases. * You can now check the latest news from the feed with ``@limnoria``. 2. To have new news automatically announced on the channel, use ``@rss announce add Limnoria``. To add another feed, simply replace limnoria and the address using name of the feed and address of the feed. For example, YLE News: 1. ``@rss add yle http://yle.fi/uutiset/rss/uutiset.rss?osasto=news`` 2. ``@rss announce add yle`` News on their own lines ^^^^^^^^^^^^^^^^^^^^^^^ If you want the feed topics to be on their own lines instead of being separated by the separator which you have configured you can set `reply.onetoone` to False. Please first read the help for that configuration variable ``@config help reply.onetoone`` and understand what it says and then you can do ``@config reply.onetoone False`` """ threaded = True def __init__(self, irc): self.__parent = super(RSS, self) self.__parent.__init__(irc) if world.starting: self._init_time = time.time() # To delay loading the feeds else: self._init_time = 0 # Scheme: {name: url} self.feed_names = callbacks.CanonicalNameDict() # Scheme: {url: feed} self.feeds = {} if os.path.isfile(announced_headlines_filename): with open(announced_headlines_filename) as fd: announced = load_announces_db(fd) else: announced = {} for name in self.registryValue('feeds'): try: url = self.registryValue(registry.join(['feeds', name])) except registry.NonExistentRegistryEntry: self.log.warning('%s is not a registered feed, removing.',name) continue try: self.register_feed(name, url, True, True, announced.get(name, [])) except InvalidFeedUrl: self.log.error('%s is not a valid feed, removing.', name) continue world.flushers.append(self._flush) def die(self): self._flush() world.flushers.remove(self._flush) self.__parent.die() def _flush(self): l = [(f.name, f.announced_entries) for f in self.feeds.values()] with utils.file.AtomicFile(announced_headlines_filename, 'w', backupDir='/dev/null') as fd: save_announces_db(l, fd) ################## # Feed registering def assert_feed_does_not_exist(self, name, url=None): if self.isCommandMethod(name): s = format(_('I already have a command in this plugin named %s.'), name) raise callbacks.Error(s) if url: feed = self.feeds.get(url) if feed and feed.name != feed.url: s = format(_('I already have a feed with that URL named %s.'), feed.name) raise callbacks.Error(s) def register_feed(self, name, url, initial, plugin_is_loading, announced=None): if name != url: # If name == url, then it's an anonymous feed self.feed_names[name] = url self.feeds[url] = Feed(name, url, initial, plugin_is_loading, announced) def remove_feed(self, name_or_url): self.feed_names.pop(name_or_url, None) while True: try: conf.supybot.plugins.RSS.feeds().remove(name_or_url) except KeyError: break try: conf.supybot.plugins.RSS.feeds.unregister(name_or_url) except (KeyError, registry.NonExistentRegistryEntry): pass ################## # Methods handling def isCommandMethod(self, name): if not self.__parent.isCommandMethod(name): return bool(self.get_feed(name)) else: return True def listCommands(self): return self.__parent.listCommands(self.feed_names.keys()) def getCommandMethod(self, command): try: return self.__parent.getCommandMethod(command) except AttributeError: return self.get_feed(command[0]).get_command(self) def __call__(self, irc, msg): self.__parent.__call__(irc, msg) threading.Thread(target=self.update_feeds).start() ################## # Status accessors def get_feed(self, name): return self.feeds.get(self.feed_names.get(name, name), None) def is_expired(self, feed): assert feed period = self.registryValue('waitPeriod') if feed.name != feed.url: # Named feed specific_period = self.registryValue('feeds.%s.waitPeriod' % feed.name) if specific_period: period = specific_period event_horizon = time.time() - period return feed.last_update < event_horizon ############### # Feed fetching @log.firewall def update_feed(self, feed): handlers = [] if utils.web.proxy(): handlers.append(ProxyHandler( {'http': utils.force(utils.web.proxy())})) handlers.append(ProxyHandler( {'https': utils.force(utils.web.proxy())})) with feed.lock: try: d = feedparser.parse(feed.url, etag=feed.etag, modified=feed.modified, handlers=handlers) except socket.error as e: self.log.warning("Network error while fetching <%s>: %s", feed.url, e) feed.last_exception = e return except Exception as e: self.log.error("Failed to fetch <%s>: %s", feed.url, e) raise # reraise so @log.firewall prints the traceback if 'status' not in d or d.status != 304: # Not modified if 'etag' in d: feed.etag = d.etag if 'modified' in d: feed.modified = d.modified feed.data = d.feed feed.entries = d.entries feed.last_update = time.time() # feedparser will store soft errors in bozo_exception and set # the "bozo" bit to 1 on supported platforms: # https://pythonhosted.org/feedparser/bozo.html # If this error caused us to e.g. not get any entries at all, # it may be helpful to show it as well. if getattr(d, 'bozo', 0) and hasattr(d, 'bozo_exception'): feed.last_exception = d.bozo_exception else: feed.last_exception = None (initial, feed.initial) = (feed.initial, False) self.announce_feed(feed, initial) def update_feed_if_needed(self, feed): if self.is_expired(feed) and \ self._init_time + INIT_DELAY < time.time(): self.update_feed(feed) @only_one_at_once def update_feeds(self): announced_feeds = {} for irc in world.ircs: for channel in list(irc.state.channels): channel_feed_names = self.registryValue( 'announce', channel, irc.network) for name in channel_feed_names: announced_feeds[name] = (channel, irc.network) for (name, (channel, network)) in announced_feeds.items(): feed = self.get_feed(name) if not feed: self.log.warning( 'Feed %s is announced in %s@%s, but does not exist. ' 'Use "rss announce remove %s %s" to remove it from ' 'announcements.', name, channel, network, channel, name) continue self.update_feed_if_needed(feed) def get_new_entries(self, feed): with feed.lock: entries = feed.entries new_entries = [entry for entry in entries if get_entry_id(entry) not in feed.announced_entries] if not new_entries: return [] feed.announced_entries |= set(get_entry_id(entry) for entry in new_entries) # We keep a little more because we don't want to re-announce # oldest entries if one of the newest gets removed. feed.announced_entries.truncate(10*len(entries)) return new_entries def announce_feed(self, feed, initial): new_entries = self.get_new_entries(feed) order = self.registryValue('sortFeedItems') new_entries = sort_feed_items(new_entries, 'newestFirst') for irc in world.ircs: for channel in list(irc.state.channels): # Old bots have it set in plugins.RSS.announce.#channel, # new bots set it in plugins.RSS.announce.:network.#channel, # so we want to read from both. channel_feeds = self.registryValue('announce', channel) \ | self.registryValue('announce', channel, irc.network) if feed.name not in channel_feeds: continue if initial: max_entries = self.registryValue( 'initialAnnounceHeadlines', channel, irc.network) else: max_entries = self.registryValue( 'maximumAnnounceHeadlines', channel, irc.network) announced_entries = new_entries[0:max_entries] announced_entries = sort_feed_items(announced_entries, order) for entry in announced_entries: self.announce_entry(irc, channel, feed, entry) ################# # Entry rendering def should_send_entry(self, network, channel, entry): whitelist = self.registryValue('keywordWhitelist', channel, network) blacklist = self.registryValue('keywordBlacklist', channel, network) # fix shadowing by "from supybot.commands import *" try: all = __builtins__.all any = __builtins__.any except AttributeError: all = __builtins__['all'] any = __builtins__['any'] title = getattr(entry, 'title', '') description = getattr(entry, 'description', '') if whitelist: if all(kw not in title and kw not in description for kw in whitelist): return False if blacklist: if any(kw in title or kw in description for kw in blacklist): return False return True _normalize_entry = utils.str.multipleReplacer( {'\r': ' ', '\n': ' ', '\x00': ''}) def format_entry(self, network, channel, feed, entry, is_announce): key_name = 'announceFormat' if is_announce else 'format' if feed.name in self.registryValue('feeds'): specific_key_name = registry.join(['feeds', feed.name, key_name]) template = self.registryValue(specific_key_name, channel, network) or \ self.registryValue(key_name, channel, network) else: template = self.registryValue(key_name, channel, network) date = entry.get('published_parsed') date = utils.str.timestamp(date) kwargs = {"feed_%s" % k: v for (k, v) in feed.data.items() if isinstance(v, str)} kwargs["feed_name"] = feed.name kwargs.update(entry) for (key, value) in list(kwargs.items()): # First look for plain text if isinstance(value, list): for item in value: if isinstance(item, dict) and 'value' in item and \ item.get('type') == 'text/plain': value = item['value'] break # Then look for HTML text or URL if isinstance(value, list): for item in value: if isinstance(item, dict) and item.get('type') in \ ('text/html', 'application/xhtml+xml'): if 'value' in item: value = utils.web.htmlToText(item['value']) elif 'href' in item: value = item['href'] # Then fall back to any URL if isinstance(value, list): for item in value: if isinstance(item, dict) and 'href' in item: value = item['href'] break # Finally, as a last resort, use the value as-is if isinstance(value, list): for item in value: if isinstance(item, dict) and 'value' in item: value = item['value'] kwargs[key] = value for key in ('summary', 'title'): detail = kwargs.get('%s_detail' % key) if isinstance(detail, dict) and detail.get('type') in \ ('text/html', 'application/xhtml+xml'): kwargs[key] = utils.web.htmlToText(detail['value']) if 'description' not in kwargs and kwargs[key]: kwargs['description'] = kwargs[key] if 'description' not in kwargs and kwargs.get('content'): kwargs['description'] = kwargs['content'] s = string.Template(template).safe_substitute(entry, **kwargs, date=date) return self._normalize_entry(s) def announce_entry(self, irc, channel, feed, entry): if self.should_send_entry(irc.network, channel, entry): s = self.format_entry(irc.network, channel, feed, entry, True) if self.registryValue('notice', channel, irc.network): m = ircmsgs.notice(channel, s) else: m = ircmsgs.privmsg(channel, s) irc.queueMsg(m) ########## # Commands @internationalizeDocstring def add(self, irc, msg, args, name, url): """ Adds a command to this plugin that will look up the RSS feed at the given URL. """ self.assert_feed_does_not_exist(name, url) register_feed_config(name, url) self.register_feed(name, url, True, False) irc.replySuccess() add = wrap(add, ['feedName', 'url']) @internationalizeDocstring def remove(self, irc, msg, args, name): """ Removes the command for looking up RSS feeds at from this plugin. """ feed = self.get_feed(name) if not feed: irc.error(_('That\'s not a valid RSS feed command name.')) return # If the feed was first created "anonymously", eg. with # `@rss announce add http://example.org/rss`, then as a named feed # with `@rss add example http://example.org/rss`, # `self.get_feed(name)` above gets only one of them; so let's # remove the aliased name or URL from the feed names too, # or we would have a dangling entry here. self.remove_feed(feed.url) self.remove_feed(name) assert self.get_feed(name) is None irc.replySuccess() remove = wrap(remove, ['feedName']) class announce(callbacks.Commands): @internationalizeDocstring def list(self, irc, msg, args, channel): """[] Returns the list of feeds announced in . is only necessary if the message isn't sent in the channel itself. """ announce = conf.supybot.plugins.RSS.announce channel_feeds = announce.getSpecific(channel=channel)() \ | announce.getSpecific(channel=channel, network=irc.network)() feeds = format('%L', set(channel_feeds)) # set() to deduplicate irc.reply(feeds or _('I am currently not announcing any feeds.')) list = wrap(list, ['channel',]) @internationalizeDocstring def add(self, irc, msg, args, channel, feeds): """[] [ ...] Adds the list of feeds to the current list of announced feeds in . Valid feeds include the names of registered feeds as well as URLs for RSS feeds. is only necessary if the message isn't sent in the channel itself. """ plugin = irc.getCallback('RSS') invalid_feeds = [x for x in feeds if not plugin.get_feed(x) and not utils.web.urlRe.match(x)] if invalid_feeds: irc.error(format(_('These feeds are unknown: %L'), invalid_feeds), Raise=True) announce = conf.supybot.plugins.RSS.announce S = announce.getSpecific(channel=channel, network=irc.network)() for name in feeds: S.add(name) announce.getSpecific(channel=channel, network=irc.network, fallback_to_channel=False).setValue(S) irc.replySuccess() for name in feeds: feed = plugin.get_feed(name) if not feed: register_feed_config(name, name) plugin.register_feed(name, name, True, False) feed = plugin.get_feed(name) plugin.announce_feed(feed, True) add = wrap(add, [('checkChannelCapability', 'op'), many(first('url', 'feedName'))]) @internationalizeDocstring def remove(self, irc, msg, args, channel, feeds): """[] [ ...] Removes the list of feeds from the current list of announced feeds in . Valid feeds include the names of registered feeds as well as URLs for RSS feeds. is only necessary if the message isn't sent in the channel itself. """ announce = conf.supybot.plugins.RSS.announce def remove_from_var(var): S = var() for feed in feeds: S.discard(feed) var.setValue(S) remove_from_var(announce.get(channel)) remove_from_var(announce.getSpecific( channel=channel, network=irc.network, fallback_to_channel=False)) irc.replySuccess() remove = wrap(remove, [('checkChannelCapability', 'op'), many(first('url', 'feedName'))]) @internationalizeDocstring def channels(self, irc, msg, args, feed): """ Returns a list of channels that the given feed name or URL is being announced to. """ plugin = irc.getCallback('RSS') if not plugin.get_feed(feed): irc.error(_("Unknown feed %s" % feed), Raise=True) channels = [] for ircnet in world.ircs: for channel in list(ircnet.state.channels): if feed in plugin.registryValue('announce', channel, ircnet.network): channels.append(ircnet.network + channel) if channels: irc.reply(format("%s is announced to %L.", feed, channels)) else: irc.reply("%s is not announced to any channels." % feed) channels = wrap(channels, ['feedName']) @internationalizeDocstring def rss(self, irc, msg, args, url, n): """ [] Gets the title components of the given RSS feed. If is given, return only that many headlines. """ self.log.debug('Fetching %u', url) try: feed = self.get_feed(url) if not feed: feed = Feed(url, url, True) except InvalidFeedUrl: irc.error('%s is not a valid feed URL or name.' % url, Raise=True) channel = msg.channel self.update_feed_if_needed(feed) entries = feed.entries if not entries: s = _('Couldn\'t get RSS feed.') # If we got a soft parsing exception on our last run, show the error. if feed.last_exception is not None: s += _(' Parser error: ') s += str(feed.last_exception) irc.error(s) return n = n or self.registryValue('defaultNumberOfHeadlines', channel, irc.network) entries = list(filter(lambda e:self.should_send_entry(irc.network, channel, e), feed.entries)) entries = sort_feed_items(entries, 'newestFirst') entries = entries[:n] entries = sort_feed_items(entries, self.registryValue('sortFeedItems')) headlines = map(lambda e:self.format_entry(irc.network, channel, feed, e, False), entries) sep = self.registryValue('headlineSeparator', channel, irc.network) irc.replies(headlines, joiner=sep) rss = wrap(rss, [first('url', 'feedName'), additional('int')]) @internationalizeDocstring def info(self, irc, msg, args, url): """ Returns information from the given RSS feed, namely the title, URL, description, and last update date, if available. """ try: url = self.registryValue('feeds.%s' % url) except registry.NonExistentRegistryEntry: pass feed = self.get_feed(url) if not feed: feed = Feed(url, url, True) self.update_feed_if_needed(feed) info = feed.data if not info: irc.error(_('I couldn\'t retrieve that RSS feed.')) return # check the 'modified_parsed' key, if it's there, convert it here first if 'modified' in info: seconds = time.mktime(info['modified_parsed']) now = time.mktime(time.gmtime()) when = utils.timeElapsed(now - seconds) + ' ago' else: when = _('time unavailable') title = info.get('title', _('unavailable')) desc = info.get('description', _('unavailable')) link = info.get('link', _('unavailable')) # The rest of the entries are all available in the channel key response = format(_('Title: %s; URL: %u; ' 'Description: %s; Last updated: %s.'), title, link, desc, when) irc.reply(utils.str.normalizeWhitespace(response)) info = wrap(info, [first('url', 'feedName')]) RSS = internationalizeDocstring(RSS) Class = RSS # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�010213� x����������������������������������������������������������������������������������������������������ustar�00�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1702131007.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������limnoria-2023.11.18/plugins/RSS/test.py�������������������������������������������������������������0000644�0001750�0001750�00000061603�14535072477�015616� 0����������������������������������������������������������������������������������������������������ustar�00val�����������������������������val��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 functools from unittest.mock import patch import socket import sys import feedparser from supybot.test import * import supybot.conf as conf import supybot.utils.minisix as minisix xkcd_old = """ xkcd.comhttp://xkcd.com/xkcd.com: A webcomic of romance and math humor.enSnake Factshttp://xkcd.com/1398/<img src="http://imgs.xkcd.com/comics/snake_facts.png" title="Biologically speaking, what we call a 'snake' is actually a human digestive tract which has escaped from its host." alt="Biologically speaking, what we call a 'snake' is actually a human digestive tract which has escaped from its host." />Wed, 23 Jul 2014 04:00:00 -0000http://xkcd.com/1398/ """ xkcd_new = """ xkcd.comhttp://xkcd.com/xkcd.com: A webcomic of romance and math humor.enTelescopes: Refractor vs Reflectorhttp://xkcd.com/1791/<img src="http://imgs.xkcd.com/comics/telescopes_refractor_vs_reflector.png" title="On the other hand, the refractor's limited light-gathering means it's unable to make out shadow people or the dark god Chernabog." alt="On the other hand, the refractor's limited light-gathering means it's unable to make out shadow people or the dark god Chernabog." />Fri, 27 Jan 2017 05:00:00 -0000http://xkcd.com/1791/Chaoshttp://xkcd.com/1399/<img src="http://imgs.xkcd.com/comics/chaos.png" title="Although the oral exam for the doctorate was just 'can you do that weird laugh?'" alt="Although the oral exam for the doctorate was just 'can you do that weird laugh?'" />Fri, 25 Jul 2014 04:00:00 -0000http://xkcd.com/1399/Snake Factshttp://xkcd.com/1398/<img src="http://imgs.xkcd.com/comics/snake_facts.png" title="Biologically speaking, what we call a 'snake' is actually a human digestive tract which has escaped from its host." alt="Biologically speaking, what we call a 'snake' is actually a human digestive tract which has escaped from its host." />Wed, 23 Jul 2014 04:00:00 -0000http://xkcd.com/1398/ """ not_well_formed = """ this is missing a close tag http://example.com/ this dummy feed has no elements en """ class MockResponse: headers = {} url = '' def read(self): return self._data.encode() def close(self): pass def geturl(self): return url def mock_urllib(f): mock = MockResponse() @functools.wraps(f) def newf(self): with patch("urllib.request.OpenerDirector.open", return_value=mock): f(self, mock) return newf url = 'http://www.advogato.org/rss/articles.xml' class RSSTestCase(ChannelPluginTestCase): plugins = ('RSS','Plugin') timeout = 1 def testRssAddBadName(self): self.assertError('rss add "foo bar" %s' % url) def testCantAddFeedNamedRss(self): self.assertError('rss add rss %s' % url) def testCantRemoveMethodThatIsntFeed(self): self.assertError('rss remove rss') def testCantAddDuplicatedFeed(self): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') try: self.assertError('rss add xkcddup http://xkcd.com/rss.xml') finally: self.assertNotError('rss remove xkcd') @mock_urllib def testRemoveAliasedFeed(self, mock): mock._data = xkcd_old try: self.assertNotError('rss announce add http://xkcd.com/rss.xml') # Clear the queue or it's going to mess up the finally block self.assertRegexp(' ', 'Snake Facts') self.assertNoResponse(' ') self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') finally: self.assertNotError('rss announce remove http://xkcd.com/rss.xml') self.assertNotError('rss remove xkcd') self.assertEqual(self.irc.getCallback('RSS').feed_names, {}) self.assertTrue(self.irc.getCallback('RSS').get_feed('http://xkcd.com/rss.xml')) @mock_urllib def testInitialAnnounceNewest(self, mock): mock._data = xkcd_new timeFastForward(1.1) try: with conf.supybot.plugins.RSS.sortFeedItems.context('newestFirst'): with conf.supybot.plugins.RSS.initialAnnounceHeadlines.context(1): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertRegexp(' ', 'Telescopes') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testInitialAnnounceOldest(self, mock): mock._data = xkcd_new timeFastForward(1.1) try: with conf.supybot.plugins.RSS.initialAnnounceHeadlines.context(1): with conf.supybot.plugins.RSS.sortFeedItems.context('oldestFirst'): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertRegexp(' ', 'Telescopes') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testNoInitialAnnounce(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: with conf.supybot.plugins.RSS.initialAnnounceHeadlines.context(0): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertNoResponse(' ', timeout=0.1) finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testAnnounce(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: self.assertError('rss announce add xkcd') self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertNotError(' ') with conf.supybot.plugins.RSS.sortFeedItems.context('oldestFirst'): with conf.supybot.plugins.RSS.waitPeriod.context(1): timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) mock._data = xkcd_new self.assertNoResponse(' ', timeout=0.1) timeFastForward(1.1) self.assertRegexp(' ', 'Chaos') self.assertRegexp(' ', 'Telescopes') self.assertNoResponse(' ') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testMaxAnnounces(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: self.assertError('rss announce add xkcd') self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertNotError(' ') with conf.supybot.plugins.RSS.sortFeedItems.context('oldestFirst'): with conf.supybot.plugins.RSS.waitPeriod.context(1): with conf.supybot.plugins.RSS.maximumAnnounceHeadlines.context(1): timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) mock._data = xkcd_new log.debug('set return value to: %r', xkcd_new) self.assertNoResponse(' ', timeout=0.1) timeFastForward(1.1) self.assertRegexp(' ', 'Telescopes') self.assertNoResponse(' ') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testAnnounceAnonymous(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: self.assertNotError('rss announce add http://xkcd.com/rss.xml') self.assertNotError(' ') with conf.supybot.plugins.RSS.waitPeriod.context(1): timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) mock._data = xkcd_new self.assertNoResponse(' ', timeout=0.1) timeFastForward(1.1) self.assertRegexp(' ', 'Telescopes') self.assertRegexp(' ', 'Chaos') self.assertNoResponse(' ', timeout=0.1) self.assertResponse('announce list', 'http://xkcd.com/rss.xml') finally: self._feedMsg('rss announce remove http://xkcd.com/rss.xml') self._feedMsg('rss remove http://xkcd.com/rss.xml') @mock_urllib def testAnnounceReload(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: with conf.supybot.plugins.RSS.waitPeriod.context(1): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertNotError(' ', timeout=0.1) self.assertNotError('reload RSS') self.assertNoResponse(' ', timeout=0.1) timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) self.assertResponse('announce list', 'xkcd') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testReload(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: with conf.supybot.plugins.RSS.waitPeriod.context(1): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertRegexp(' ', 'Snake Facts') mock._data = xkcd_new self.assertNotError('reload RSS') self.assertRegexp(' ', 'Telescopes') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testReloadNoDelay(self, mock): # https://github.com/progval/Limnoria/issues/922 mock._data = xkcd_old timeFastForward(1.1) try: with conf.supybot.plugins.RSS.waitPeriod.context(1): self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertRegexp('xkcd', 'Snake Facts') self.assertNotError('reload RSS') self.assertRegexp('xkcd', 'Snake Facts') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testReannounce(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: self.assertError('rss announce add xkcd') self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd') self.assertRegexp(' ', 'Snake Facts') with conf.supybot.plugins.RSS.waitPeriod.context(1): with conf.supybot.plugins.RSS.initialAnnounceHeadlines.context(1): with conf.supybot.plugins.RSS.sortFeedItems.context('oldestFirst'): timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) self._feedMsg('rss announce remove xkcd') mock._data = xkcd_new timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) self.assertNotError('rss announce add xkcd') timeFastForward(1.1) self.assertRegexp(' ', 'Chaos') self.assertRegexp(' ', 'Telescopes') self.assertNoResponse(' ') finally: self._feedMsg('rss announce remove xkcd') self._feedMsg('rss remove xkcd') @mock_urllib def testFeedSpecificFormat(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: self.assertNotError('rss add xkcd http://xkcd.com/rss.xml') self.assertNotError('rss add xkcdsec https://xkcd.com/rss.xml') self.assertNotError('config plugins.RSS.feeds.xkcd.format foo') self.assertRegexp('config plugins.RSS.feeds.xkcd.format', 'foo') self.assertRegexp('xkcd', 'foo') self.assertNotRegexp('xkcdsec', 'foo') finally: self._feedMsg('rss remove xkcd') self._feedMsg('rss remove xkcdsec') @mock_urllib def testFeedSpecificWaitPeriod(self, mock): mock._data = xkcd_old timeFastForward(1.1) try: self.assertNotError('rss add xkcd1 http://xkcd.com/rss.xml') self.assertNotError('rss announce add xkcd1') self.assertNotError('rss add xkcd2 http://xkcd.com/rss.xml&foo') self.assertNotError('rss announce add xkcd2') self.assertNotError(' ') self.assertNotError(' ') with conf.supybot.plugins.RSS.sortFeedItems.context('oldestFirst'): with conf.supybot.plugins.RSS.feeds.xkcd1.waitPeriod.context(1): timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) mock._data = xkcd_new self.assertNoResponse(' ', timeout=0.1) timeFastForward(1.1) self.assertRegexp(' ', 'xkcd1.*Chaos') self.assertRegexp(' ', 'xkcd1.*Telescopes') self.assertNoResponse(' ') timeFastForward(1.1) self.assertNoResponse(' ', timeout=0.1) finally: self._feedMsg('rss announce remove xkcd1') self._feedMsg('rss remove xkcd1') self._feedMsg('rss announce remove xkcd2') self._feedMsg('rss remove xkcd2') @mock_urllib def testDescription(self, mock): timeFastForward(1.1) with conf.supybot.plugins.RSS.format.context('$description'): mock._data = xkcd_new self.assertRegexp('rss http://xkcd.com/rss.xml', 'On the other hand, the refractor\'s') @mock_urllib def testAtomContentHtmlOnly(self, mock): timeFastForward(1.1) mock._data = """ Recent Commits to anope:2.0 2023-10-04T16:14:39Z title with <pre>HTML</pre> 2023-10-04T16:14:39Z content with <pre>HTML</pre> """ with conf.supybot.plugins.RSS.format.context('$content'): self.assertRegexp('rss https://example.org', 'content with HTML') with conf.supybot.plugins.RSS.format.context('$description'): self.assertRegexp('rss https://example.org', 'content with HTML') @mock_urllib def testAtomContentXhtmlOnly(self, mock): timeFastForward(1.1) mock._data = """ Recent Commits to anope:2.0 2023-10-04T16:14:39Z title with <pre>HTML</pre> 2023-10-04T16:14:39Z
content with
XHTML
""" with conf.supybot.plugins.RSS.format.context('$content'): self.assertRegexp('rss https://example.org', 'content with XHTML') with conf.supybot.plugins.RSS.format.context('$description'): self.assertRegexp('rss https://example.org', 'content with XHTML') @mock_urllib def testAtomContentHtmlAndPlaintext(self, mock): timeFastForward(1.1) mock._data = """ Recent Commits to anope:2.0 2023-10-04T16:14:39Z title with <pre>HTML</pre> 2023-10-04T16:14:39Z content with <pre>HTML</pre> content with plaintext """ with conf.supybot.plugins.RSS.format.context('$content'): self.assertRegexp('rss https://example.org', 'content with plaintext') with conf.supybot.plugins.RSS.format.context('$description'): self.assertRegexp('rss https://example.org', 'content with plaintext') @mock_urllib def testAtomContentPlaintextAndHtml(self, mock): timeFastForward(1.1) mock._data = """ Recent Commits to anope:2.0 2023-10-04T16:14:39Z title with <pre>HTML</pre> 2023-10-04T16:14:39Z content with plaintext content with <pre>HTML</pre> """ with conf.supybot.plugins.RSS.format.context('$content'): self.assertRegexp('rss https://example.org', 'content with plaintext') with conf.supybot.plugins.RSS.format.context('$description'): self.assertRegexp('rss https://example.org', 'content with plaintext') @mock_urllib def testRssDescriptionHtml(self, mock): timeFastForward(1.1) mock._data = """ feed title en title with <pre>HTML</pre> description with <pre>HTML</pre> """ with conf.supybot.plugins.RSS.format.context('$description'): self.assertRegexp('rss https://example.org', 'description with HTML') @mock_urllib def testFeedAttribute(self, mock): timeFastForward(1.1) with conf.supybot.plugins.RSS.format.context('$feed_title: $title'): mock._data = xkcd_new self.assertRegexp('rss http://xkcd.com/rss.xml', r'xkcd\.com: Telescopes') @mock_urllib def testBadlyFormedFeedWithNoItems(self, mock): # This combination will cause the RSS command to show the last parser # error. timeFastForward(1.1) mock._data = not_well_formed self.assertRegexp('rss http://example.com/', 'Parser error: .*mismatch') def testSocketError(self): class MockResponse: headers = {} url = '' def read(self): raise socket.error("oh no") def close(self): pass mock = MockResponse() with patch("urllib.request.OpenerDirector.open", return_value=mock): timeFastForward(1.1) self.assertRegexp('rss http://example.com/', 'Parser error: .*oh no') if network: timeout = 5 # Note this applies also to the above tests def testRssinfo(self): timeFastForward(1.1) self.assertNotError('rss info %s' % url) self.assertNotError('rss add advogato %s' % url) self.assertNotError('rss info advogato') self.assertNotError('rss info AdVogATo') self.assertNotError('rss remove advogato') def testRssinfoDoesTimeProperly(self): timeFastForward(1.1) self.assertNotRegexp('rss info http://slashdot.org/slashdot.rss', '-1 years') def testAnnounceAdd(self): timeFastForward(1.1) self.assertNotError('rss add advogato %s' % url) self.assertNotError('rss announce add advogato') self.assertRegexp('rss announce channels advogato', 'advogato is announced to.*%s%s' % (self.irc.network, self.channel)) self.assertNotRegexp('rss announce', r'ValueError') self.assertNotError('rss announce remove advogato') self.assertRegexp('rss announce channels advogato', 'advogato is not announced to any channels') self.assertNotError('rss remove advogato') self.assertRegexp('rss announce channels advogato', 'Unknown feed') def testRss(self): timeFastForward(1.1) self.assertNotError('rss %s' % url) m = self.assertNotError('rss %s 2' % url) self.assertEqual(m.args[1].count(' | '), 1) def testRssAdd(self): timeFastForward(1.1) self.assertNotError('rss add advogato %s' % url) self.assertNotError('advogato') self.assertNotError('rss advogato') self.assertNotError('rss remove advogato') self.assertNotRegexp('list RSS', 'advogato') self.assertError('advogato') self.assertError('rss advogato') def testNonAsciiFeeds(self): timeFastForward(1.1) self.assertNotError('rss http://www.heise.de/newsticker/heise.rdf') self.assertNotError('rss info http://br-linux.org/main/index.xml') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3537545 limnoria-2023.11.18/plugins/Relay/0000755000175000017500000000000014535072535014657 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/__init__.py0000644000175000017500000000472514535072470016776 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Handles relaying between networks. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/config.py0000644000175000017500000001121014535072470016467 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.ircutils as ircutils import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Relay') def configure(advanced): from supybot.questions import output, expect, anything, something, yn conf.registerPlugin('Relay', True) if yn(_('Would you like to relay between any channels?')): channels = anything(_('What channels? Separate them by spaces.')) conf.supybot.plugins.Relay.channels.set(channels) if yn(_('Would you like to use color to distinguish between nicks?')): conf.supybot.plugins.Relay.color.setValue(True) output("""Right now there's no way to configure the actual connection to the server. What you'll need to do when the bot finishes starting up is use the 'start' command followed by the 'connect' command. Use the 'help' command to see how these two commands should be used.""") class Ignores(registry.SpaceSeparatedListOf): List = ircutils.IrcSet Value = conf.ValidHostmask sorted = True class Networks(registry.SpaceSeparatedListOf): List = ircutils.IrcSet Value = registry.String sorted = True Relay = conf.registerPlugin('Relay') conf.registerChannelValue(Relay, 'color', registry.Boolean(True, _("""Determines whether the bot will color relayed PRIVMSGs so as to make the messages easier to read."""))) conf.registerChannelValue(Relay, 'topicSync', registry.Boolean(True, _("""Determines whether the bot will synchronize topics between networks in the channels it relays."""))) conf.registerChannelValue(Relay, 'hostmasks', registry.Boolean(True, _("""Determines whether the bot will relay the hostmask of the person joining or parting the channel when they join or part."""))) conf.registerChannelValue(Relay, 'includeNetwork', registry.Boolean(True, _("""Determines whether the bot will include the network in relayed PRIVMSGs; if you're only relaying between two networks, it's somewhat redundant, and you may wish to save the space."""))) conf.registerChannelValue(Relay, 'punishOtherRelayBots', registry.Boolean(True, _("""Determines whether the bot will detect other bots relaying and respond by kickbanning them."""))) conf.registerGlobalValue(Relay, 'channels', conf.SpaceSeparatedSetOfChannels([], _("""Determines which channels the bot will relay in."""))) conf.registerChannelValue(Relay.channels, 'joinOnAllNetworks', registry.Boolean(False, _("""Determines whether the bot will always join the channel(s) it relays when connecting to any network. """))) conf.registerChannelValue(Relay, 'ignores', Ignores([], _("""Determines what hostmasks will not be relayed on a channel."""))) conf.registerChannelValue(Relay, 'noticeNonPrivmsgs', registry.Boolean(False, _("""Determines whether the bot will used NOTICEs rather than PRIVMSGs for non-PRIVMSG relay messages (i.e., joins, parts, nicks, quits, modes, etc.)"""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3537545 limnoria-2023.11.18/plugins/Relay/locales/0000755000175000017500000000000014535072535016301 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/locales/fi.po0000644000175000017500000002012214535072470017232 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Relay plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12:17+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:40 msgid "Would you like to relay between any channels?" msgstr "Haluasitko botin välittävän joidenkin kanavien välillä?" #: config.py:41 msgid "What channels? Separate them by spaces." msgstr "Minkä kanavien? Erota ne välilyönnillä." #: config.py:43 msgid "Would you like to use color to distinguish between nicks?" msgstr "Haluaisitko botin käyttävän värejä erottamaan nimimerkit toisistaan?" #: config.py:60 msgid "" "Determines whether the bot will color relayed\n" " PRIVMSGs so as to make the messages easier to read." msgstr "" "Määrittää värjääkö botti välitetyt\n" " PRIVMSG:eet, jotta viestit olisivat helpommin luettavia." #: config.py:63 msgid "" "Determines whether the bot will synchronize\n" " topics between networks in the channels it relays." msgstr "" "Määrittää synkronoiko botti\n" " aiheet kanavilla, joita se välittää kaikkiin verkkoihin." #: config.py:66 msgid "" "Determines whether the bot will relay the\n" " hostmask of the person joining or parting the channel when they join\n" " or part." msgstr "" "Määrittää välittääkö botti\n" " liittyvän tai poistuvan henkilön hostmaskin, hänen liittyessään/" "poistuessaan." #: config.py:70 msgid "" "Determines whether the bot will include the\n" " network in relayed PRIVMSGs; if you're only relaying between two " "networks,\n" " it's somewhat redundant, and you may wish to save the space." msgstr "" "Määrittää sisällyttääkö botti\n" " verkon välitetyissä PRIVMSG:issä; jos välität vain kahta verkkoa,\n" " se on aika tarpeeton, ja saatat tahtoa säästää tilaa." #: config.py:74 msgid "" "Determines whether the bot will detect other\n" " bots relaying and respond by kickbanning them." msgstr "" "Määrittää havaitseeko botti toiset\n" " välittävät botit ja vastaa potkimalla ne ja antamalla niille " "porttikiellon." #: config.py:77 msgid "" "Determines which channels the bot\n" " will relay in." msgstr "" "Määrittää mitä kanavia botti\n" " välittää." #: config.py:80 #, fuzzy msgid "" "Determines whether the bot\n" " will always join the channel(s) it relays when connecting to any " "network.\n" " " msgstr "" "Määrittää liittyykö botti aina\n" " kanavalle( tai kanaville), joita se välittää, kaikissa verkoissa, " "joihin\n" " botti on yhdistänyt." #: config.py:84 msgid "" "Determines what hostmasks will not be relayed on a\n" " channel." msgstr "" "Määrittää mitä hostmaskeja ei välitetä\n" " kanavalla." #: config.py:87 msgid "" "Determines whether the bot will used NOTICEs\n" " rather than PRIVMSGs for non-PRIVMSG relay messages (i.e., joins, " "parts,\n" " nicks, quits, modes, etc.)" msgstr "" "Määrittää käyttääkö botti huomautuksia (NOTICE)\n" " mielummin kuin PRIVMSG:ssiä muissa kuin PRIVMSG välitysviesteissä (esim. " "liittymiset, poistumiset\n" " nimimerkit, lopetukset, tilat, jne.)" #: plugin.py:48 msgid "" "\n" " This plugin allows you to setup a relay between networks.\n" "\n" " Note that you must tell the bot to join the channel you wish to relay " "on\n" " all networks with the ``join`` command or\n" " ``network command join ``\n" " or to join the channel on all networks ``network cmdall join " "``.\n" "\n" " There are several advanced alternatives to this plugin, available as\n" " third-party plugins. You can check them out at\n" " https://limnoria.net/plugins.xhtml#messaging\n" " " msgstr "" #: plugin.py:88 msgid "" "[]\n" "\n" " Starts relaying between the channel on all networks. If " "on a\n" " network the bot isn't in , it'll join. This commands is\n" " required even if the bot is in the channel on both networks; it " "won't\n" " relay between those channels unless it's told to join both\n" " channels. If is not given, starts relaying on the " "channel\n" " the message was sent in.\n" " " msgstr "" "[]\n" "\n" " Alkaa välittämään kaikissa verkoissa. Jos botti ei ole\n" " jossain verkossa, se liittyy. Tämä komento on vaadittu, " "jopa jos\n" " botti on kanavalla molemmissa verkoissa; se ei välitä\n" " noiden kanavien välillä, ennen kuin se on käsketty liittymään " "molemmille\n" " kanaville. Jos ei ole annettu, botti alkaa välittämään " "kanavaa, jolla\n" " viesti lähetettiin.\n" " " #: plugin.py:107 msgid "" "\n" "\n" " Ceases relaying between the channel on all networks. The " "bot\n" " will part from the channel on all networks in which it is on the\n" " channel.\n" " " msgstr "" "\n" "\n" " Pysäyttää välittämisen kaikissa verkoissa. Botti\n" " poistuu kanavalta kaikissa verkoissa, joilla se on\n" " kanavalla.\n" " " #: plugin.py:122 msgid "" "[]\n" "\n" " Returns the nicks of the people in the channel on the various " "networks\n" " the bot is connected to. is only necessary if the " "message\n" " isn't sent on the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa kanavalla olevien ihmisten nimimerkit niissä verkoissa,\n" " joihin botti on yhdistänyt. on vaadittu vain. jos viestiä\n" " ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:207 msgid "There is no %s on %s." msgstr "verkossa %s ei ole %s:ää." #: plugin.py:292 msgid "You seem to be relaying, punk." msgstr "Sinä näytät välittävän, punkki." #: plugin.py:349 msgid "%s%s has joined on %s" msgstr "%s%s on liittynyt verkossa %s" #: plugin.py:364 msgid "%s%s has left on %s (%s)" msgstr "%s%s on lähtenyt verkossa %s (%s)" #: plugin.py:367 msgid "%s%s has left on %s" msgstr "%s%s on lähtenyt verkossa %s" #: plugin.py:377 msgid "mode change by %s on %s: %s" msgstr "tilan muutos %s verkossa %s: %s" #: plugin.py:389 msgid "%s was kicked by %s on %s (%s)" msgstr "%s potkittiin, potkija %s verkossa %s (%s)" #: plugin.py:392 msgid "%s was kicked by %s on %s" msgstr "%s potkittiin, potkija %s verkossa %s" #: plugin.py:401 msgid "nick change by %s to %s on %s" msgstr "nimimerkin vaihto %s %s:ksi verkossa %s" #: plugin.py:431 msgid "topic change by %s on %s: %s" msgstr "aiheen vaihto, vaihtanut %s %s: %s:ksi." #: plugin.py:440 msgid "%s has quit %s (%s)" msgstr "%s on lopettanut %s verkossa (%s)" #: plugin.py:442 msgid "%s has quit %s." msgstr "%on lopettanut %s." #: plugin.py:452 msgid "disconnected from %s: %s" msgstr "yhteys katkaistu verkosta %s: %s" #~ msgid "This plugin allows you to setup a relay between networks." #~ msgstr "Tämä plugin sallii välityksen asettamisen verkkojen välille." #~ msgid "is an op on %L" #~ msgstr "on operaattori kanavalla %L" #~ msgid "is a halfop on %L" #~ msgstr "on puolioperaattori kanavalla %L" #~ msgid "is voiced on %L" #~ msgstr "omaa äänen kanavalla %L" #~ msgid "is also on %L" #~ msgstr "on myös kanavilla %L" #~ msgid "is on %L" #~ msgstr "on myös kanavalla %L" #~ msgid "isn't on any non-secret channels" #~ msgstr "ei ole yhdelläkään ei salaisella kanavalla" #~ msgid "" #~ msgstr "" #~ msgid " %s is away: %s." #~ msgstr " %s on poissa: %s." #~ msgid " identified" #~ msgstr "tunnistautunut" #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s) and %s.%s" #~ msgstr "" #~ "%s (%s) on ollut %s palvelimella %s %s lähtien (idlannut %s) ja %s.%s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/locales/fr.po0000644000175000017500000001657114535072470017260 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:40 msgid "Would you like to relay between any channels?" msgstr "Voulez-vous relayer entre des canaux ?" #: config.py:41 msgid "What channels? Separate them by spaces." msgstr "Quels canaux ? Séparez-les par des espaces." #: config.py:43 msgid "Would you like to use color to distinguish between nicks?" msgstr "Voulez-vous utiliser de la couleur pour distinguer les nicks ?" #: config.py:60 msgid "" "Determines whether the bot will color relayed\n" " PRIVMSGs so as to make the messages easier to read." msgstr "" "Détermine si le bot colorera les PRIVMSGs relayez, pour rendre les messages " "plus faciles à lire." #: config.py:63 msgid "" "Determines whether the bot will synchronize\n" " topics between networks in the channels it relays." msgstr "" "Détermine si le bot synchronisera les topics entre les réseaux sur les " "canaux qu'il relaye." #: config.py:66 #, fuzzy msgid "" "Determines whether the bot will relay the\n" " hostmask of the person joining or parting the channel when they join\n" " or part." msgstr "" "Détermine si le bot relayera le masque d'hôte d'une personne joignant ou " "partant d'un canal." #: config.py:70 msgid "" "Determines whether the bot will include the\n" " network in relayed PRIVMSGs; if you're only relaying between two " "networks,\n" " it's somewhat redundant, and you may wish to save the space." msgstr "" "Détermine si le bot inclurera le réseau dans les PRIVMSG relayés ; si vous " "ne relayez qu'entre deux réseaux, ce sera quelque chose de redondant que " "vous pouvez supprimer pour gagner de la place." #: config.py:74 msgid "" "Determines whether the bot will detect other\n" " bots relaying and respond by kickbanning them." msgstr "" "Détermine si le bot détectera d'autres bots relayant et y répondra en les " "kickbannissant." #: config.py:77 msgid "" "Determines which channels the bot\n" " will relay in." msgstr "Détermine sur quels canaux le bot relayera." #: config.py:80 #, fuzzy msgid "" "Determines whether the bot\n" " will always join the channel(s) it relays when connecting to any " "network.\n" " " msgstr "" "Détermine si le bot rejoindra toujours le(s) canal(aux) qu'il relaye sur " "tous les réseaux auxquels il est connecté." #: config.py:84 msgid "" "Determines what hostmasks will not be relayed on a\n" " channel." msgstr "Détermine quels masques d'hôte ne seront pas relayés sur un canal." #: config.py:87 msgid "" "Determines whether the bot will used NOTICEs\n" " rather than PRIVMSGs for non-PRIVMSG relay messages (i.e., joins, " "parts,\n" " nicks, quits, modes, etc.)" msgstr "" "Détermine si le bot utilisera des NOTICEs plutôt que des PRIVMSG pour les " "messages relayés qui ne concernent pas un PRIVMSG (arrivée, départ, " "changement de nick, quits, modes, etc.)" #: plugin.py:48 msgid "" "\n" " This plugin allows you to setup a relay between networks.\n" "\n" " Note that you must tell the bot to join the channel you wish to relay " "on\n" " all networks with the ``join`` command or\n" " ``network command join ``\n" " or to join the channel on all networks ``network cmdall join " "``.\n" "\n" " There are several advanced alternatives to this plugin, available as\n" " third-party plugins. You can check them out at\n" " https://limnoria.net/plugins.xhtml#messaging\n" " " msgstr "" #: plugin.py:88 msgid "" "[]\n" "\n" " Starts relaying between the channel on all networks. If " "on a\n" " network the bot isn't in , it'll join. This commands is\n" " required even if the bot is in the channel on both networks; it " "won't\n" " relay between those channels unless it's told to join both\n" " channels. If is not given, starts relaying on the " "channel\n" " the message was sent in.\n" " " msgstr "" "[]\n" "\n" "Commence à relayer le canal sur tous les réseaux. Si il y a un " "réseau sur lequel le bot n'est pas sur , il y entrera. Cette commande " "est requise même si le bot est sur le canal sur tous les réseaux ; il ne " "relayera pas tant qu'on ne lui a pas dit de rejoindre les canaux sur tous " "les réseaux. Si n'est pas donné, il commencera à relayer sur le " "canal où a été envoyé le message." #: plugin.py:107 msgid "" "\n" "\n" " Ceases relaying between the channel on all networks. The " "bot\n" " will part from the channel on all networks in which it is on the\n" " channel.\n" " " msgstr "" "\n" "\n" "Cesse de relayer entre les canaux sur tous les réseaux. Le bot " "partira de ces canaux sur tous les réseaux si il y est." #: plugin.py:122 msgid "" "[]\n" "\n" " Returns the nicks of the people in the channel on the various " "networks\n" " the bot is connected to. is only necessary if the " "message\n" " isn't sent on the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne les nicks des personnes sur le canal sur les différents réseaux sur " "lesquels est connecté le bot. n'est nécessaire que si l'on n'est pas " "sur le canal lui-même." #: plugin.py:207 msgid "There is no %s on %s." msgstr "Il n'y a pas de %s sur %s." #: plugin.py:292 msgid "You seem to be relaying, punk." msgstr "Tu sembles relayer, enfoiré" #: plugin.py:349 msgid "%s%s has joined on %s" msgstr "%s%s est arrivé sur %s" #: plugin.py:364 msgid "%s%s has left on %s (%s)" msgstr "%s%s est parti de %s (%s)" #: plugin.py:367 msgid "%s%s has left on %s" msgstr "%s%s est parti de %s" #: plugin.py:377 msgid "mode change by %s on %s: %s" msgstr "changement de mode par %s sur %s : %s" #: plugin.py:389 msgid "%s was kicked by %s on %s (%s)" msgstr "%s a été kické par %s sur %s (%s)" #: plugin.py:392 msgid "%s was kicked by %s on %s" msgstr "%s a été kické par %s sur %s" #: plugin.py:401 msgid "nick change by %s to %s on %s" msgstr "changement de nick : %s -> %s sur %s" #: plugin.py:431 msgid "topic change by %s on %s: %s" msgstr "changement de topic par %s sur %s : %s" #: plugin.py:440 msgid "%s has quit %s (%s)" msgstr "%s a quitté %s (%s)" #: plugin.py:442 msgid "%s has quit %s." msgstr "%s a quitté %s." #: plugin.py:452 msgid "disconnected from %s: %s" msgstr "déconnecté de %s : %s" #~ msgid "is an op on %L" #~ msgstr "est op sur %L" #~ msgid "is a halfop on %L" #~ msgstr "est halfop sur %L" #~ msgid "is voiced on %L" #~ msgstr "est voice sur %L" #~ msgid "is also on %L" #~ msgstr "est aussi sur %L" #~ msgid "is on %L" #~ msgstr "est sur %L" #~ msgid "isn't on any non-secret channels" #~ msgstr "n'est sur aucun canal non-secret" #~ msgid "" #~ msgstr "" #~ msgid " %s is away: %s." #~ msgstr "%s est away : %s" #~ msgid " identified" #~ msgstr "identifié" #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s) and %s.%s" #~ msgstr "" #~ "%s (%s) a été vu%s sur le serveur %s depuis %s (idle depuis %s) et %s.%s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/locales/it.po0000644000175000017500000001647714535072470017272 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-05-11 17:50+0200\n" "Last-Translator: Mikaela Suomalainen \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" "X-Generator: Poedit 1.5.4\n" #: config.py:40 msgid "Would you like to relay between any channels?" msgstr "Vuoi trasmettere messaggi tra canali diversi?" #: config.py:41 msgid "What channels? Separate them by spaces." msgstr "Quali canali? Separali con spazi." #: config.py:43 msgid "Would you like to use color to distinguish between nicks?" msgstr "Vuoi usare colori per distinguere i nick?" #: config.py:60 msgid "" "Determines whether the bot will color relayed\n" " PRIVMSGs so as to make the messages easier to read." msgstr "" "Determina se il bot colorerà i PRIVMSG inoltrati per renderne più semplice " "la lettura." #: config.py:63 msgid "" "Determines whether the bot will synchronize\n" " topics between networks in the channels it relays." msgstr "" "Determina se il bot sincronizzerà i topic tra le reti nei canali in cui " "inoltra." #: config.py:66 #, fuzzy msgid "" "Determines whether the bot will relay the\n" " hostmask of the person joining or parting the channel when they join\n" " or part." msgstr "" "Determina se il bot inoltrerà l'hostmask dell'utente che entra o esce dal " "canale." #: config.py:70 msgid "" "Determines whether the bot will include the\n" " network in relayed PRIVMSGs; if you're only relaying between two " "networks,\n" " it's somewhat redundant, and you may wish to save the space." msgstr "" "Determina se il bot includerà la rete nei PRIVMSG inoltrati; se si " "trasmette\n" " tra due sole reti è alquanto ridondante e si preferirà risparmiare " "spazio." #: config.py:74 msgid "" "Determines whether the bot will detect other\n" " bots relaying and respond by kickbanning them." msgstr "" "Determina se il bot rileverà altri bot che stanno inoltrando rispondendo con " "un kickban." #: config.py:77 msgid "" "Determines which channels the bot\n" " will relay in." msgstr "Determina in quale canale il bot inoltrerà i messaggi." #: config.py:80 #, fuzzy msgid "" "Determines whether the bot\n" " will always join the channel(s) it relays when connecting to any " "network.\n" " " msgstr "" "Determina se il bot entrerà sempre nei canali in cui trasmette per tutte le " "reti a cui è connesso." #: config.py:84 msgid "" "Determines what hostmasks will not be relayed on a\n" " channel." msgstr "Determina quale hostmask non verrà inoltrata su un canale." #: config.py:87 msgid "" "Determines whether the bot will used NOTICEs\n" " rather than PRIVMSGs for non-PRIVMSG relay messages (i.e., joins, " "parts,\n" " nicks, quits, modes, etc.)" msgstr "" "Determina se il bot userà i NOTICE piuttosto che i PRIVMSG per messaggi che " "non\n" " riguardano le comunicazioni tra utenti (join, part, nick, quit, mode, " "ecc.)" #: plugin.py:48 msgid "" "\n" " This plugin allows you to setup a relay between networks.\n" "\n" " Note that you must tell the bot to join the channel you wish to relay " "on\n" " all networks with the ``join`` command or\n" " ``network command join ``\n" " or to join the channel on all networks ``network cmdall join " "``.\n" "\n" " There are several advanced alternatives to this plugin, available as\n" " third-party plugins. You can check them out at\n" " https://limnoria.net/plugins.xhtml#messaging\n" " " msgstr "" #: plugin.py:88 msgid "" "[]\n" "\n" " Starts relaying between the channel on all networks. If " "on a\n" " network the bot isn't in , it'll join. This commands is\n" " required even if the bot is in the channel on both networks; it " "won't\n" " relay between those channels unless it's told to join both\n" " channels. If is not given, starts relaying on the " "channel\n" " the message was sent in.\n" " " msgstr "" "[]\n" "\n" " Inizia l'inoltro dei messaggi di su tutte le reti. Se su " "una rete\n" " il bot non è in , ci entrerà. Questo comando è necessario " "anche se\n" " il bot è presente nel canale di entrambe le reti; non trasmetterà i " "messaggi\n" " a meno che gli sia stato detto di entrare su entrambi i canali. Se " "\n" " non è specificato, inizia l'inoltro sul canale in cui è stato " "inviato il messaggio.\n" " " #: plugin.py:107 msgid "" "\n" "\n" " Ceases relaying between the channel on all networks. The " "bot\n" " will part from the channel on all networks in which it is on the\n" " channel.\n" " " msgstr "" "\n" "\n" " Interrompe l'inoltro dei messaggi di su tutte le reti.\n" " Il bot uscirà dal canale su tutte le reti a cui è connesso.\n" " " #: plugin.py:122 msgid "" "[]\n" "\n" " Returns the nicks of the people in the channel on the various " "networks\n" " the bot is connected to. is only necessary if the " "message\n" " isn't sent on the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta i nick degli utenti presenti nel canale sulle diverse reti " "alle quali è connesso\n" " il bot. è necessario solo se il messaggio non viene inviato " "nel canale stesso.\n" " " #: plugin.py:207 msgid "There is no %s on %s." msgstr "Non c'è nessun %s su %s." #: plugin.py:292 msgid "You seem to be relaying, punk." msgstr "Sembra che tu stia inoltrando, giovane padawan." #: plugin.py:349 msgid "%s%s has joined on %s" msgstr "%s%s è entrato in %s" #: plugin.py:364 msgid "%s%s has left on %s (%s)" msgstr "%s%s ha lasciato %s (%s)" #: plugin.py:367 msgid "%s%s has left on %s" msgstr "%s%s ha lasciato %s" #: plugin.py:377 msgid "mode change by %s on %s: %s" msgstr "mode cambiato da %s su %s: %s" #: plugin.py:389 msgid "%s was kicked by %s on %s (%s)" msgstr "%s è stato cacciato da %s su %s (%s)" #: plugin.py:392 msgid "%s was kicked by %s on %s" msgstr "%s è stato cacciato da %s su %s" #: plugin.py:401 msgid "nick change by %s to %s on %s" msgstr "nick cambiato da %s a %s su %s" #: plugin.py:431 msgid "topic change by %s on %s: %s" msgstr "topic cambiato da %s su %s: %s" #: plugin.py:440 msgid "%s has quit %s (%s)" msgstr "%s è uscito da %s (%s)" #: plugin.py:442 msgid "%s has quit %s." msgstr "%s è uscito da %s." #: plugin.py:452 msgid "disconnected from %s: %s" msgstr "disconnesso da %s: %s" #~ msgid "is an op on %L" #~ msgstr "è un op su %L" #~ msgid "is a halfop on %L" #~ msgstr "è un halfop su %L" #~ msgid "is voiced on %L" #~ msgstr "ha il voice su %L" #~ msgid "is also on %L" #~ msgstr "è anche su %L" #~ msgid "is on %L" #~ msgstr "è su %L" #~ msgid "isn't on any non-secret channels" #~ msgstr "non è in alcun canale non segreto" #~ msgid "" #~ msgstr "" #~ msgid " %s is away: %s." #~ msgstr " %s è away: %s." #~ msgid " identified" #~ msgstr " identificato" #~ msgid "%s (%s) has been%s on server %s since %s (idle for %s) and %s.%s" #~ msgstr "%s (%s) era%s sul server %s dalle %s (inattivo da %s) e %s.%s" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/plugin.py0000644000175000017500000004634214535072470016536 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010,2015 James McCoy # Copyright (c) 2010-2021, 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 copy import time import supybot.conf as conf import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.utils.structures import MultiSet, TimeoutQueue from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Relay') class Relay(callbacks.Plugin): """ This plugin allows you to setup a relay between networks. Note that you must tell the bot to join the channel you wish to relay on all networks with the ``join`` command or ``network command join `` or to join the channel on all networks ``network cmdall join ``. There are several advanced alternatives to this plugin, available as third-party plugins. You can check them out at https://limnoria.net/plugins.xhtml#messaging """ noIgnore = True def __init__(self, irc): self.__parent = super(Relay, self) self.__parent.__init__(irc) self._whois = {} self.queuedTopics = MultiSet() self.lastRelayMsgs = ircutils.IrcDict() def do376(self, irc, msg): networkGroup = conf.supybot.networks.get(irc.network) for channel in self.registryValue('channels'): if self.registryValue('channels.joinOnAllNetworks', channel): if channel not in irc.state.channels: irc.queueMsg(networkGroup.channels.join(channel)) do377 = do422 = do376 def _getRealIrc(self, irc): if isinstance(irc, irclib.Irc): return irc else: return irc.getRealIrc() def _getIrcName(self, irc): # We should allow abbreviations at some point. return irc.network @internationalizeDocstring def join(self, irc, msg, args, channel): """[] Starts relaying between the channel on all networks. If on a network the bot isn't in , it'll join. This commands is required even if the bot is in the channel on both networks; it won't relay between those channels unless it's told to join both channels. If is not given, starts relaying on the channel the message was sent in. """ self.registryValue('channels').add(channel) for otherIrc in world.ircs: if channel not in otherIrc.state.channels: networkGroup = conf.supybot.networks.get(otherIrc.network) otherIrc.queueMsg(networkGroup.channels.join(channel)) irc.replySuccess() join = wrap(join, ['channel', 'admin']) @internationalizeDocstring def part(self, irc, msg, args, channel): """ Ceases relaying between the channel on all networks. The bot will part from the channel on all networks in which it is on the channel. """ self.registryValue('channels').discard(channel) for otherIrc in world.ircs: if channel in otherIrc.state.channels: otherIrc.queueMsg(ircmsgs.part(channel)) irc.replySuccess() part = wrap(part, ['channel', 'admin']) @internationalizeDocstring def nicks(self, irc, msg, args, channel): """[] Returns the nicks of the people in the channel on the various networks the bot is connected to. is only necessary if the message isn't sent on the channel itself. """ realIrc = self._getRealIrc(irc) if channel not in self.registryValue('channels'): irc.error(format('I\'m not relaying in %s.', channel)) return users = [] for otherIrc in world.ircs: network = self._getIrcName(otherIrc) ops = [] halfops = [] voices = [] usersS = [] if network != self._getIrcName(realIrc): try: Channel = otherIrc.state.channels[channel] except KeyError: users.append(format('(not in %s on %s)',channel,network)) continue numUsers = 0 for s in Channel.users: s = s.strip() if not s: continue numUsers += 1 if s in Channel.ops: ops.append('@' + s) elif s in Channel.halfops: halfops.append('%' + s) elif s in Channel.voices: voices.append('+' + s) else: usersS.append(s) utils.sortBy(ircutils.toLower, ops) utils.sortBy(ircutils.toLower, voices) utils.sortBy(ircutils.toLower, halfops) utils.sortBy(ircutils.toLower, usersS) usersS = ', '.join(filter(None, list(map(', '.join, (ops,halfops,voices,usersS))))) users.append(format('%s (%i): %s', ircutils.bold(network), numUsers, usersS)) users.sort() irc.reply('; '.join(users)) nicks = wrap(nicks, ['channel']) def do311(self, irc, msg): irc = self._getRealIrc(irc) nick = ircutils.toLower(msg.args[1]) if (irc, nick) not in self._whois: return else: self._whois[(irc, nick)][-1][msg.command] = msg # These are all sent by a WHOIS response. do301 = do311 do312 = do311 do317 = do311 do319 = do311 do320 = do311 def do318(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] d['318'] = msg s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] def do402(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)] s = format(_('There is no %s on %s.'), nick, self._getIrcName(irc)) replyIrc.reply(s) do401 = do402 def _formatDisplayName(self, nick, network, channel): displayName = nick if self.registryValue('includeNetwork', channel): displayName += '@' + network return displayName def _formatPrivmsg(self, nick, network, msg): channel = msg.channel # colorize nicks color = self.registryValue('color', channel) # Also used further down. if color: nick = ircutils.IrcString(nick) newnick = ircutils.mircColor(nick, *ircutils.canonicalColor(nick)) colors = ircutils.canonicalColor(nick, shift=4) nick = newnick if ircmsgs.isAction(msg): if color: t = ircutils.mircColor('*', *colors) else: t = '*' displayName = self._formatDisplayName(nick, network, msg.channel) s = format('%s %s %s', t, displayName, ircmsgs.unAction(msg)) else: if color: lt = ircutils.mircColor('<', *colors) gt = ircutils.mircColor('>', *colors) else: lt = '<' gt = '>' displayName = self._formatDisplayName(nick, network, msg.channel) s = format('%s%s%s %s', lt, displayName, gt, msg.args[1]) return s def _sendToOthers(self, irc, msg, nick): assert msg.command in ('PRIVMSG', 'NOTICE', 'TOPIC') for otherIrc in world.ircs: if otherIrc != irc and not otherIrc.zombie: if msg.channel in otherIrc.state.channels: self._sendToOther(irc, otherIrc, msg, nick) def _sendToOther(self, sourceIrc, destIrc, msg, nick): msg = copy.deepcopy(msg) msg.tag('relayedMsg') if 'message-tags' in destIrc.state.capabilities_ack \ and conf.supybot.protocols.irc.experimentalExtensions(): displayName = self._formatDisplayName( nick, sourceIrc.network, msg.channel) # https://github.com/ircv3/ircv3-specifications/pull/452 msg.server_tags['+draft/display-name'] = displayName destIrc.queueMsg(msg) def _checkRelayMsg(self, msg): channel = msg.channel if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) for s in q: if s in normalized: return True return False def _punishRelayers(self, msg): assert self._checkRelayMsg(msg), 'Punishing without checking.' who = msg.prefix channel = msg.channel def notPunishing(irc, s, *args): self.log.info('Not punishing %s in %s on %s: %s.', msg.prefix, channel, irc.network, s, *args) for irc in world.ircs: if channel in irc.state.channels: if irc.nick in irc.state.channels[channel].ops: if who in irc.state.channels[channel].bans: notPunishing(irc, 'already banned') else: self.log.info('Punishing %s in %s on %s for relaying.', who, channel, irc.network) irc.sendMsg(ircmsgs.ban(channel, who)) kmsg = _('You seem to be relaying, punk.') irc.sendMsg(ircmsgs.kick(channel, msg.nick, kmsg)) else: notPunishing(irc, 'not opped') def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return text = msg.args[1] if msg.channel: irc = self._getRealIrc(irc) if msg.channel not in self.registryValue('channels'): return ignores = self.registryValue('ignores', msg.channel, irc.network) for ignore in ignores: if ircutils.hostmaskPatternEqual(ignore, msg.prefix): self.log.debug('Refusing to relay %s, ignored by %s.', msg.prefix, ignore) return # Let's try to detect other relay bots. if self._checkRelayMsg(msg): if self.registryValue('punishOtherRelayBots', msg.channel, irc.network): self._punishRelayers(msg) # Either way, we don't relay the message. else: self.log.warning('Refusing to relay message from %s, ' 'it appears to be a relay message.', msg.prefix) else: network = self._getIrcName(irc) s = self._formatPrivmsg(msg.nick, network, msg) m = self._msgmaker(msg.channel, network, s) self._sendToOthers(irc, m, msg.nick) def _msgmaker(self, target, network, s): msg = dynamic.msg if self.registryValue('noticeNonPrivmsgs', target, network) and \ msg.command != 'PRIVMSG': m = ircmsgs.notice(target, s) else: m = ircmsgs.privmsg(target, s) m.channel = target return m def doJoin(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('hostmasks', channel) and '!' in msg.prefix: hostmask = format(' (%s)', msg.prefix.split('!')[1]) else: hostmask = '' s = format(_('%s%s has joined on %s'), msg.nick, hostmask, network) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doPart(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('hostmasks', channel) and '!' in msg.prefix: hostmask = format(' (%s)', msg.prefix.split('!')[1]) else: hostmask = '' if len(msg.args) > 1: s = format(_('%s%s has left on %s (%s)'), msg.nick, hostmask, network, msg.args[1]) else: s = format(_('%s%s has left on %s'), msg.nick, hostmask, network) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doMode(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) s = format(_('mode change by %s on %s: %s'), msg.nick, network, ' '.join(msg.args[1:])) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doKick(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if len(msg.args) == 3: s = format(_('%s was kicked by %s on %s (%s)'), msg.args[1], msg.nick, network, msg.args[2]) else: s = format(_('%s was kicked by %s on %s'), msg.args[1], msg.nick, network) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doNick(self, irc, msg): irc = self._getRealIrc(irc) newNick = msg.args[0] network = self._getIrcName(irc) s = format(_('nick change by %s to %s on %s'), msg.nick,newNick,network) for channel in self.registryValue('channels'): if channel not in msg.tagged('channels'): continue m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doTopic(self, irc, msg): irc = self._getRealIrc(irc) (channel, newTopic) = msg.args if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('topicSync', channel): m = ircmsgs.topic(channel, newTopic) for otherIrc in world.ircs: if irc != otherIrc: try: if otherIrc.state.getTopic(channel) != newTopic: if (otherIrc, newTopic) not in self.queuedTopics: self.queuedTopics.add((otherIrc, newTopic)) otherIrc.queueMsg(m) else: self.queuedTopics.remove((otherIrc, newTopic)) except KeyError: self.log.warning('Not on %s on %s, ' 'can\'t sync topics.', channel, otherIrc.network) else: s = format(_('topic change by %s on %s: %s'), msg.nick, network, newTopic) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doQuit(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) if msg.args: s = format(_('%s has quit %s (%s)'), msg.nick, network, msg.args[0]) else: s = format(_('%s has quit %s.'), msg.nick, network) for channel in self.registryValue('channels'): if channel not in msg.tagged('channels'): continue m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doError(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) s = format(_('disconnected from %s: %s'), network, msg.args[0]) for channel in self.registryValue('channels'): m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def outFilter(self, irc, msg): irc = self._getRealIrc(irc) if msg.command == 'PRIVMSG': if msg.relayedMsg: self._addRelayMsg(msg) else: if msg.channel in self.registryValue('channels'): network = self._getIrcName(irc) s = self._formatPrivmsg(irc.nick, network, msg) relayMsg = self._msgmaker(msg.args[0], network, s) self._sendToOthers(irc, relayMsg, irc.nick) return msg def _addRelayMsg(self, msg): channel = msg.channel if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] else: q = TimeoutQueue(60) # XXX Make this configurable. self.lastRelayMsgs[channel] = q unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) q.enqueue(normalized) Class = Relay # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Relay/test.py0000644000175000017500000000337314535072470016214 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class RelayTestCase(PluginTestCase): plugins = ('Relay',) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3537545 limnoria-2023.11.18/plugins/Reply/0000755000175000017500000000000014535072535014676 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/__init__.py0000644000175000017500000000501414535072470017005 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ This plugin contains various commands which elicit certain types of responses from the bot. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/config.py0000644000175000017500000000472714535072470016525 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Reply') 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('Reply', True) Reply = conf.registerPlugin('Reply') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Reply, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Reply/locales/0000755000175000017500000000000014535072535016320 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/locales/de.po0000644000175000017500000000452014535072470017247 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-29 10:22+0100\n" "Last-Translator: Florian Besser \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:38 msgid "" "This plugin contains a few commands that construct various types of\n" " replies. Some bot owners would be wise to not load this plugin because " "it\n" " can be easily abused.\n" " " msgstr "" "Dieses Plugin enthält einige befehl um verschiedene Antworten zu " "konstruieren. Einige Bot Besitzer sollten dieses Plugin nicht laden, weil es " "einfach missbraucht werden kann." #: plugin.py:44 msgid "" "\n" "\n" " Replies with in private. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" "Antwortet mit privat. Du kannst hier nutzen aus verschachtelte " "Befehle ziehen." #: plugin.py:54 msgid "" "\n" "\n" " Replies with as an action. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" "Antwortet mit als Aktion. Du kannst hier nutzen aus verschachtelte " "Befehle ziehen." #: plugin.py:67 msgid "" "\n" "\n" " Replies with in a notice. Use nested commands to your " "benefit\n" " here. If you want a private notice, nest the private command.\n" " " msgstr "" "\n" "\n" "Antwortet mit als Notiz.Du kannst hier nutzen aus verschachtelte " "Befehle ziehen. Falls du eine private Notiz nutzen willst niste den privaten " "Befehl ein." #: plugin.py:77 msgid "" "\n" "\n" " Replies with . Equivalent to the alias, 'echo $nick: $1'.\n" " " msgstr "" "\n" "\n" "Antworte mit . Gleich wie der Alias 'echo $nick: $1'." #: plugin.py:86 msgid "" " [ ...]\n" "\n" " Replies with each of its arguments in separate replies, " "depending\n" " the configuration of supybot.reply.oneToOne.\n" " " msgstr "" " [ ...]\n" "\n" "Antwortet mit jedem Argument in separaten Antworten, hängt " "von der Konfiguration von supybot.reply.oneToOne ab." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/locales/fi.po0000644000175000017500000000510614535072470017256 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-10 13:06+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: plugin.py:38 msgid "" "This plugin contains a few commands that construct various types of\n" " replies. Some bot owners would be wise to not load this plugin because " "it\n" " can be easily abused.\n" " " msgstr "" "Tämä lisäosa sisältää muutamia erilaisia vastauksia.\n" " Joidenkin botin omistajien ei olisi viisasta ladata tätä lisäosaa, " "korka\n" " sitä voidaan väärinkäyttää hyvin helposti.\n" " " #: plugin.py:44 msgid "" "\n" "\n" " Replies with in private. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" " Vastaa yksityisviestissä. Käytä sisäkkäisiä komentoja " "eduksesi\n" " tässä.\n" " " #: plugin.py:54 msgid "" "\n" "\n" " Replies with as an action. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" " Vastaa toimintoja. Käytä sisäkkäisiä komentoja " "eduksesi\n" " tässä.\n" " " #: plugin.py:67 msgid "" "\n" "\n" " Replies with in a notice. Use nested commands to your " "benefit\n" " here. If you want a private notice, nest the private command.\n" " " msgstr "" "\n" "\n" " Vastaa huomautuksena. Käytä sisäkkäisiä komentoja " "eduksesi\n" " tässä. Jos haluat yksityisen huomautuksen, sisällytä komento " "\"private\".\n" " " #: plugin.py:77 msgid "" "\n" "\n" " Replies with . Equivalent to the alias, 'echo $nick: $1'.\n" " " msgstr "" "\n" "\n" " Vastaa . Vastaava aliakseen, 'echo $nick: $1'.\n" " " #: plugin.py:86 msgid "" " [ ...]\n" "\n" " Replies with each of its arguments in separate replies, " "depending\n" " the configuration of supybot.reply.oneToOne.\n" " " msgstr "" " [ ...]\n" "\n" " Vastaa jokaisen parametrin eri vastauksessa, riippuen\n" " asetuksesta supybot.reply.oneToOne.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/locales/fr.po0000644000175000017500000000443314535072470017271 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:38 msgid "" "This plugin contains a few commands that construct various types of\n" " replies. Some bot owners would be wise to not load this plugin because " "it\n" " can be easily abused.\n" " " msgstr "" "Ce plugin contient quelques commandes pour construire différents types de " "réponses. Certains propriétaires de bots pourraient ne pas vouloir le " "charger, car on peut facilement en abuser." #: plugin.py:44 msgid "" "\n" "\n" " Replies with in private. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" "Répond avec le en privé. Utile pour les commandes imbriquées." #: plugin.py:54 #, fuzzy msgid "" "\n" "\n" " Replies with as an action. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" "Répond avec le comme une action. Utile pour les commandes imbriquées." #: plugin.py:67 msgid "" "\n" "\n" " Replies with in a notice. Use nested commands to your " "benefit\n" " here. If you want a private notice, nest the private command.\n" " " msgstr "" "\n" "\n" "Répond avec le en notice. Utile pour les commandes imbriquées." #: plugin.py:77 msgid "" "\n" "\n" " Replies with . Equivalent to the alias, 'echo $nick: $1'.\n" " " msgstr "" "\n" "\n" "Répond avec le . Équivalent à l'alias 'echo $nick: $i'." #: plugin.py:86 msgid "" " [ ...]\n" "\n" " Replies with each of its arguments in separate replies, " "depending\n" " the configuration of supybot.reply.oneToOne.\n" " " msgstr "" " [ ...]\n" "\n" "Répond avec chacun des s dans des réponses séparées, en fonction " "de la configutation de supybot.reply.oneToOne." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/locales/hu.po0000644000175000017500000000515514535072470017300 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Limnoria Reply\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-15 14:49+0200\n" "Last-Translator: nyuszika7h \n" "Language-Team: \n" "Language: hu_HU\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:38 msgid "" "This plugin contains a few commands that construct various types of\n" " replies. Some bot owners would be wise to not load this plugin because " "it\n" " can be easily abused.\n" " " msgstr "" "Ez a bővítmény néhány parancsot tartalmaz, amelyek különféle típusú " "válaszokat gyárt. Néhány bot tulajdonos bölcs lenne nem betölteni ezt a " "bővítményt, mert könnyen vissza lehet élni vele." #: plugin.py:44 msgid "" "\n" "\n" " Replies with in private. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" "Privát üzenetben válaszol -gel. Itt az előnyödre használhatpd a " "beágyazott parancsokat." #: plugin.py:54 msgid "" "\n" "\n" " Replies with as an action. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "Ez a bővítmény néhány parancsot tartalmaz, amelyek különféle típusú " "válaszokat gyárt. Néhány bot tulajdonos bölcs lenne nem betölteni ezt a " "bővítményt, mert könnyen vissza lehet élni vele." #: plugin.py:67 msgid "" "\n" "\n" " Replies with in a notice. Use nested commands to your " "benefit\n" " here. If you want a private notice, nest the private command.\n" " " msgstr "" "\n" "\n" "Egy közleményben válaszol -gel. Itt az előnyödre használhatod a " "beágyazott parancsokat. Ha egy privát közleményt szeretnál, ágyazd be a " "private parancsot." #: plugin.py:77 msgid "" "\n" "\n" " Replies with . Equivalent to the alias, 'echo $nick: $1'.\n" " " msgstr "" "\n" "\n" "Válaszol -gel. Ugyanaz, mint az álnév, 'echo $nick: $1'." #: plugin.py:86 msgid "" " [ ...]\n" "\n" " Replies with each of its arguments in separate replies, " "depending\n" " the configuration of supybot.reply.oneToOne.\n" " " msgstr "" " [ ...]\n" "\n" "Az összes paraméterével külön üzenetben válaszol, a supybot." "reply.oneToOne konfigurációjától függően." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/locales/it.po0000644000175000017500000000460114535072470017273 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-15 12:49+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:38 msgid "" "This plugin contains a few commands that construct various types of\n" " replies. Some bot owners would be wise to not load this plugin because " "it\n" " can be easily abused.\n" " " msgstr "" "Questo plugin contiene alcuni comandi per creare vari tipi di risposte.\n" " Alcuni proprietari avranno l'accortezza di non caricarlo in quanto\n" " se ne può abusare facilmente.\n" " " #: plugin.py:44 msgid "" "\n" "\n" " Replies with in private. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" " Risponde con in privato. Utilizza i comandi nidificati a tuo " "vantaggio. " #: plugin.py:54 msgid "" "\n" "\n" " Replies with as an action. Use nested commands to your " "benefit\n" " here.\n" " " msgstr "" "\n" "\n" " Risponde con come azione. Utilizza i comandi nidificati a " "tuo vantaggio.\n" " " #: plugin.py:67 msgid "" "\n" "\n" " Replies with in a notice. Use nested commands to your " "benefit\n" " here. If you want a private notice, nest the private command.\n" " " msgstr "" "\n" "\n" " Risponde con in un notice. Utilizza i comandi nidificati a " "tuo vantaggio.\n" " Se desideri un notice privato, nidifica il comando \"private\".\n" " " #: plugin.py:77 msgid "" "\n" "\n" " Replies with . Equivalent to the alias, 'echo $nick: $1'.\n" " " msgstr "" "\n" "\n" " Risponde con . Equivale all'alias \"echo $nick: $1\".\n" " " #: plugin.py:86 msgid "" " [ ...]\n" "\n" " Replies with each of its arguments in separate replies, " "depending\n" " the configuration of supybot.reply.oneToOne.\n" " " msgstr "" " [ ...]\n" "\n" " Risponde con ognuno degli argomenti in risposte separate, " "in base\n" " alla configurazione di supybot.reply.oneToOne.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/plugin.py0000644000175000017500000000715614535072470016555 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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.commands import * import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Reply') class Reply(callbacks.Plugin): """This plugin contains a few commands that construct various types of replies. Some bot owners would be wise to not load this plugin because it can be easily abused. """ @internationalizeDocstring def private(self, irc, msg, args, text): """ Replies with in private. Use nested commands to your benefit here. """ irc.reply(text, private=True) private = wrap(private, ['text']) @internationalizeDocstring def action(self, irc, msg, args, text): """ Replies with as an action. Use nested commands to your benefit here. """ if text: irc.reply(text, action=True) else: raise callbacks.ArgumentError action = wrap(action, ['text']) @internationalizeDocstring def notice(self, irc, msg, args, text): """ Replies with in a notice. Use nested commands to your benefit here. If you want a private notice, nest the private command. """ irc.reply(text, notice=True) notice = wrap(notice, ['text']) @internationalizeDocstring def reply(self, irc, msg, args, text): """ Replies with . Equivalent to the alias, 'echo $nick: $1'. """ irc.reply(text, prefixNick=True) reply = wrap(reply, ['text']) @internationalizeDocstring def replies(self, irc, msg, args, strings): """ [ ...] Replies with each of its arguments in separate replies, depending the configuration of supybot.reply.oneToOne. """ irc.replies(strings) replies = wrap(replies, [many('something')]) Reply = internationalizeDocstring(Reply) Class = Reply # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Reply/test.py0000644000175000017500000000575314535072470016237 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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 * import supybot.ircutils as ircutils class ReplyTestCase(ChannelPluginTestCase): plugins = ('Reply',) def testPrivate(self): m = self.getMsg('private [list]') self.assertFalse(self.irc.isChannel(m.args[0])) def testNotice(self): m = self.getMsg('notice [list]') self.assertEqual(m.command, 'NOTICE') def testNoticePrivate(self): m = self.assertNotError('notice [private [list]]') self.assertEqual(m.command, 'NOTICE') self.assertEqual(m.args[0], self.nick) m = self.assertNotError('private [notice [list]]') self.assertEqual(m.command, 'NOTICE') self.assertEqual(m.args[0], self.nick) def testReplies(self): self.assertResponse('replies a b c', 'a, b, and c') self.assertNotError('config channel supybot.reply.oneToOne False') self.assertResponse('replies a b c', 'a') self.assertResponse(' ', 'b') self.assertResponse(' ', 'c') self.assertNotError('config channel supybot.reply.oneToOne True') class ReplyNonChannelTestCase(PluginTestCase): plugins = ('Reply',) def testAction(self): self.prefix = 'something!else@somewhere.else' self.nick = 'something' m = self.assertAction('action foo', 'foo') self.assertNotEqual(m.args[0], self.irc.nick) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Scheduler/0000755000175000017500000000000014535072535015521 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/__init__.py0000644000175000017500000000530514535072470017633 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Gives the user the ability to schedule commands to run at a particular time, or repeatedly run at a particular interval. For example, ``scheduler add [time seconds 30m] "utilities echo [status cpu]"`` will schedule the command `cpu` to be sent to the channel in 30 minutes. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/config.py0000644000175000017500000000475514535072470017351 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Scheduler') 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('Scheduler', True) Scheduler = conf.registerPlugin('Scheduler') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Scheduler, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Scheduler/locales/0000755000175000017500000000000014535072535017143 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/locales/fi.po0000644000175000017500000001002414535072470020074 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Scheduler plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:38+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" #: plugin.py:53 msgid "This plugin allows you to schedule commands to execute at a later time." msgstr "" "Tämä laajennus sallii komentojen ajastuksen myöhäisemmäksi ajankohdaksi." #: plugin.py:143 msgid "Makes a function suitable for scheduling from command." msgstr "Tekee toiminnon sopivaksi komennosta ajastamiseen." #: plugin.py:155 #, fuzzy msgid "Makes a function suitable for scheduling text" msgstr "Tekee toiminnon sopivaksi komennosta ajastamiseen." #: plugin.py:160 msgid "Reminder: %s" msgstr "" #: plugin.py:181 msgid "" " \n" "\n" " Schedules the command string to run seconds in " "the\n" " future. For example, 'scheduler add [seconds 30m] \"echo [cpu]\"' " "will\n" " schedule the command \"cpu\" to be sent to the channel the schedule " "add\n" " command was given in (with no prefixed nick, a consequence of using\n" " echo). Do pay attention to the quotes in that example.\n" " " msgstr "" " \n" "\n" " Ajastaa suoriutumaan sekunteja " "tulevaisuuteen\n" " Esimerkiksi, 'scheduler add [seconds 30m] \"echo [cpu]\"'\n" " ajastaa komennon \"cpu\" lähetyksen kanavalle jolla,\n" " komento annettiin (ilman nimimerkki etuliitettä, johtuen\n" " \"echo\" komennon käytöstä). Kiinnitä huomiota lainausmerkkeihin " "tuossa esimerkissä.\n" " " #: plugin.py:191 msgid "Event #%i added." msgstr "Tapahtuma #%i lisätty." #: plugin.py:196 msgid "" " \n" "\n" " Sets a reminder with string to run seconds in the\n" " future. For example, 'scheduler remind [seconds 30m] \"Hello World" "\"'\n" " will return ' Reminder: Hello World' 30 minutes after being " "set.\n" " " msgstr "" #: plugin.py:204 #, fuzzy msgid "Reminder Event #%i added." msgstr "Tapahtuma #%i lisätty." #: plugin.py:209 msgid "" "\n" "\n" " Removes the event scheduled with id from the schedule.\n" " " msgstr "" "\n" "\n" " Poistaa ajastetun komennon ajastuksesta.\n" " " #: plugin.py:223 plugin.py:225 msgid "Invalid event id." msgstr "Viallinen tapahtuma id." #: plugin.py:244 #, fuzzy msgid "" "[--delay ] \n" "\n" " Schedules the command to run every seconds,\n" " starting now (i.e., the command runs now, and every " "seconds\n" " thereafter). is a name by which the command can be\n" " unscheduled.\n" " If --delay is given, starts in seconds instead of now.\n" " " msgstr "" " \n" "\n" " Ajastaa suoritumaan, joka ,\n" " alkaen nyt (esim., komento suoriutuu nyt, ja joka \n" " sen jälkeen). on nimi, jolla komennon ajastus voidaan\n" " poistaa.\n" " " #: plugin.py:255 msgid "There is already an event with that name, please choose another name." msgstr "" "On jo olemassa tapahtuma tuolla nimellä, ole hyvä ja käytä toista nimeä." #: plugin.py:270 msgid "" "takes no arguments\n" "\n" " Lists the currently scheduled events.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Luettelee tällä hetkellä ajastetut komennot.\n" " " #: plugin.py:290 msgid "There are currently no scheduled commands." msgstr "Tällä hetkellä ei ole ajastettuja komentoja." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/locales/fr.po0000644000175000017500000000732514535072470020117 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: plugin.py:53 msgid "This plugin allows you to schedule commands to execute at a later time." msgstr "" #: plugin.py:143 msgid "Makes a function suitable for scheduling from command." msgstr "" "Crée une fonction utilisable pour la programmation à partir d'une commande." #: plugin.py:155 #, fuzzy msgid "Makes a function suitable for scheduling text" msgstr "" "Crée une fonction utilisable pour la programmation à partir d'une commande." #: plugin.py:160 msgid "Reminder: %s" msgstr "" #: plugin.py:181 msgid "" " \n" "\n" " Schedules the command string to run seconds in " "the\n" " future. For example, 'scheduler add [seconds 30m] \"echo [cpu]\"' " "will\n" " schedule the command \"cpu\" to be sent to the channel the schedule " "add\n" " command was given in (with no prefixed nick, a consequence of using\n" " echo). Do pay attention to the quotes in that example.\n" " " msgstr "" " \n" "\n" "Exécute la dans un certain nombre de . Par exemple, " "'scheduler add [seconds 30m] \"echo [cpu]\"' programmera la commande 'cpu' " "pour être envoyée sur le canal. Faites attention à l'utilisateur des " "guillemets dans cet exemple." #: plugin.py:191 msgid "Event #%i added." msgstr "Évènement #%i ajouté." #: plugin.py:196 msgid "" " \n" "\n" " Sets a reminder with string to run seconds in the\n" " future. For example, 'scheduler remind [seconds 30m] \"Hello World" "\"'\n" " will return ' Reminder: Hello World' 30 minutes after being " "set.\n" " " msgstr "" #: plugin.py:204 #, fuzzy msgid "Reminder Event #%i added." msgstr "Évènement #%i ajouté." #: plugin.py:209 msgid "" "\n" "\n" " Removes the event scheduled with id from the schedule.\n" " " msgstr "" "\n" "\n" "Déprogramme l'évènement programmé d' donné." #: plugin.py:223 plugin.py:225 msgid "Invalid event id." msgstr "Id d'évènement invalide." #: plugin.py:244 #, fuzzy msgid "" "[--delay ] \n" "\n" " Schedules the command to run every seconds,\n" " starting now (i.e., the command runs now, and every " "seconds\n" " thereafter). is a name by which the command can be\n" " unscheduled.\n" " If --delay is given, starts in seconds instead of now.\n" " " msgstr "" " \n" "\n" "Programme la pour être lancée toutes les , à partir de " "maintenant (c'est à dire que la commande est lancée maintenant, dans un " "certain nombres de , puis dans deux fois ce temps, etc). Le " "est utilisé pour déprogrammer la commande." #: plugin.py:255 msgid "There is already an event with that name, please choose another name." msgstr "Il y a déjà un évènement avec ce nom, veuillez en choisir un autre." #: plugin.py:270 msgid "" "takes no arguments\n" "\n" " Lists the currently scheduled events.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Liste tous les évènement actuellement programmés" #: plugin.py:290 msgid "There are currently no scheduled commands." msgstr "Il n'y a actuellement aucune commande programmée." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/locales/it.po0000644000175000017500000000741214535072470020121 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-31 11:51+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:53 msgid "This plugin allows you to schedule commands to execute at a later time." msgstr "" #: plugin.py:143 msgid "Makes a function suitable for scheduling from command." msgstr "" "Rende disponibile una funzione per la programmazione a partire da un comando." #: plugin.py:155 #, fuzzy msgid "Makes a function suitable for scheduling text" msgstr "" "Rende disponibile una funzione per la programmazione a partire da un comando." #: plugin.py:160 msgid "Reminder: %s" msgstr "" #: plugin.py:181 msgid "" " \n" "\n" " Schedules the command string to run seconds in " "the\n" " future. For example, 'scheduler add [seconds 30m] \"echo [cpu]\"' " "will\n" " schedule the command \"cpu\" to be sent to the channel the schedule " "add\n" " command was given in (with no prefixed nick, a consequence of using\n" " echo). Do pay attention to the quotes in that example.\n" " " msgstr "" " \n" "\n" " Programma per essere eseguito entro un certo numero di " ".\n" " Ad esempio, 'scheduler add [seconds 30m] \"echo [cpu]\"' programmerà " "il\n" " comando \"cpu\" per essere inviato in canale (senza il nick come " "prefisso,\n" " come utilizzare echo). Presta attenzione alle virgolette usate " "nell'esempio.\n" " " #: plugin.py:191 msgid "Event #%i added." msgstr "Aggiunto l'evento #%i." #: plugin.py:196 msgid "" " \n" "\n" " Sets a reminder with string to run seconds in the\n" " future. For example, 'scheduler remind [seconds 30m] \"Hello World" "\"'\n" " will return ' Reminder: Hello World' 30 minutes after being " "set.\n" " " msgstr "" #: plugin.py:204 #, fuzzy msgid "Reminder Event #%i added." msgstr "Aggiunto l'evento #%i." #: plugin.py:209 msgid "" "\n" "\n" " Removes the event scheduled with id from the schedule.\n" " " msgstr "" "\n" "\n" " Rimuove l'evento programmato tramite l' fornito.\n" " " #: plugin.py:223 plugin.py:225 msgid "Invalid event id." msgstr "Id di evento non valido." #: plugin.py:244 #, fuzzy msgid "" "[--delay ] \n" "\n" " Schedules the command to run every seconds,\n" " starting now (i.e., the command runs now, and every " "seconds\n" " thereafter). is a name by which the command can be\n" " unscheduled.\n" " If --delay is given, starts in seconds instead of now.\n" " " msgstr "" " \n" "\n" " Programma il per essere eseguito ogni certo numero di " "\n" " a partire da subito (il comando viene eseguito ora e successivamente " "ogni\n" " tot ). è il nome secondo il quale il comando può " "essere\n" " rimosso dalla programmazione.\n" " " #: plugin.py:255 msgid "There is already an event with that name, please choose another name." msgstr "C'è già un evento con quel nome, scegline un altro." #: plugin.py:270 msgid "" "takes no arguments\n" "\n" " Lists the currently scheduled events.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Elenca gli eventi attualmente programmati.\n" " " #: plugin.py:290 msgid "There are currently no scheduled commands." msgstr "Al momento non ci sono comandi programmati." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/plugin.py0000644000175000017500000003106414535072470017373 0ustar00valval### # Copyright (c) 2003-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 time import os import math import shutil import tempfile import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.schedule as schedule import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Scheduler') import supybot.world as world import supybot.utils.minisix as minisix pickle = minisix.pickle datadir = conf.supybot.directories.data() filename = conf.supybot.directories.data.dirize('Scheduler.pickle') class Scheduler(callbacks.Plugin): """This plugin allows you to schedule commands to execute at a later time.""" def __init__(self, irc): self.__parent = super(Scheduler, self) self.__parent.__init__(irc) self.events = {} self._restoreEvents(irc) world.flushers.append(self._flush) def _getNextRunIn(self, first_run, now, period, not_right_now=False): next_run_in = period - ((now - first_run) % period) if not_right_now and next_run_in < 5: # don't run immediatly, it might overwhelm the bot on # startup. next_run_in += period return next_run_in def _restoreEvents(self, irc): try: pkl = open(filename, 'rb') try: eventdict = pickle.load(pkl) except Exception as e: self.log.debug('Unable to load pickled data: %s', e) return finally: pkl.close() except IOError as e: self.log.debug('Unable to open pickle file: %s', e) return for name, event in eventdict.items(): # old DBs don't have the "network", let's take the current network # instead. network = event.get('network', irc.network) try: if event['type'] == 'single': # non-repeating event n = None if schedule.schedule.counter > int(name): # counter not reset, we're probably reloading the plugin # though we'll never know for sure, because other # plugins can schedule stuff, too. n = int(name) # Here we use event.get() method instead of event[] # This is to maintain compatibility with older bots # lacking 'is_reminder' in their event dicts is_reminder = event.get('is_reminder', False) self._add(network, event['msg'], event['time'], event['command'], is_reminder, n) elif event['type'] == 'repeat': # repeating event now = time.time() first_run = event.get('first_run') if first_run is None: # old DBs don't have a "first_run"; let's take "now" as # first_run. first_run = now # Preserve the offset over restarts; eg. if event['time'] # is 24hours, we want to keep running the command at the # same time of day. next_run_in = self._getNextRunIn( first_run, now, event['time'], not_right_now=True) self._repeat(network, event['msg'], name, event['time'], event['command'], first_run, next_run_in) except AssertionError as e: if str(e) == 'An event with the same name has already been scheduled.': # we must be reloading the plugin, event is still scheduled self.log.info('Event %s already exists, adding to dict.', name) self.events[name] = event else: raise def _flush(self): try: pklfd, tempfn = tempfile.mkstemp(suffix='scheduler', dir=datadir) pkl = os.fdopen(pklfd, 'wb') try: pickle.dump(self.events, 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 die(self): self._flush() world.flushers.remove(self._flush) self.__parent.die() def _makeCommandFunction(self, network, msg, command, remove=True): """Makes a function suitable for scheduling from command.""" def f(): # If the network isn't available, pick any other one irc = world.getIrc(network) or world.ircs[0] tokens = callbacks.tokenize(command, channel=msg.channel, network=irc.network) if remove: del self.events[str(f.eventId)] # A previous run of the command may have set 'ignored' to True, # causing this run to not include response from nested commands; # as NestedCommandsIrcProxy.reply() would confuse it with the # subcommand setting 'ignored' to True itself. msg.tag('ignored', False) self.Proxy(irc, msg, tokens) return f def _makeReminderFunction(self, network, msg, text): """Makes a function suitable for scheduling text""" def f(): # If the network isn't available, pick any other one irc = world.getIrc(network) or world.ircs[0] replyIrc = callbacks.ReplyIrcProxy(irc, msg) replyIrc.reply(_('Reminder: %s') % text, msg=msg, prefixNick=True) del self.events[str(f.eventId)] return f def _add(self, network, msg, t, command, is_reminder=False, name=None): if is_reminder: f = self._makeReminderFunction(network, msg, command) else: f = self._makeCommandFunction(network, msg, command) id = schedule.addEvent(f, t, name) f.eventId = id self.events[str(id)] = {'command':command, 'is_reminder':is_reminder, 'msg':msg, 'network':network, 'time':t, 'type':'single'} return id @internationalizeDocstring def add(self, irc, msg, args, seconds, command): """ Schedules the command string to run seconds in the future. For example, 'scheduler add [seconds 30m] "echo [cpu]"' will schedule the command "cpu" to be sent to the channel the schedule add command was given in (with no prefixed nick, a consequence of using echo). Do pay attention to the quotes in that example. """ t = time.time() + seconds id = self._add(irc.network, msg, t, command) irc.replySuccess(format(_('Event #%i added.'), id)) add = wrap(add, ['positiveInt', 'text']) @internationalizeDocstring def remind(self, irc, msg, args, seconds, text): """ Sets a reminder with string to run seconds in the future. For example, 'scheduler remind [seconds 30m] "Hello World"' will return ' Reminder: Hello World' 30 minutes after being set. """ t = time.time() + seconds id = self._add(irc.network, msg, t, text, is_reminder=True) irc.replySuccess(format(_('Reminder Event #%i added.'), id)) remind = wrap(remind, ['positiveInt', 'text']) @internationalizeDocstring def remove(self, irc, msg, args, id): """ Removes the event scheduled with id from the schedule. """ if id in self.events: del self.events[id] try: id = int(id) except ValueError: pass try: schedule.removeEvent(id) irc.replySuccess() except KeyError: irc.error(_('Invalid event id.')) else: irc.error(_('Invalid event id.')) remove = wrap(remove, ['lowered']) def _repeat(self, network, msg, name, seconds, command, first_run, next_run_in): f = self._makeCommandFunction(network, msg, command, remove=False) f_wrapper = schedule.schedule.makePeriodicWrapper(f, seconds, name) assert first_run is not None id = schedule.addEvent(f_wrapper, time.time() + next_run_in, name) assert id == name self.events[name] = {'command':command, 'msg':msg, 'network':network, 'time':seconds, 'type':'repeat', 'first_run': first_run, } @internationalizeDocstring def repeat(self, irc, msg, args, optlist, name, seconds, command): """[--delay ] Schedules the command to run every seconds, starting now (i.e., the command runs now, and every seconds thereafter). is a name by which the command can be unscheduled. If --delay is given, starts in seconds instead of now. """ opts = dict(optlist) name = name.lower() if name in self.events: irc.error(_('There is already an event with that name, please ' 'choose another name.'), Raise=True) next_run_in = opts.get('delay', 0) first_run = time.time() + next_run_in self._repeat( irc.network, msg, name, seconds, command, first_run, next_run_in) # We don't reply because the command runs immediately. # But should we? What if the command doesn't have visible output? # irc.replySuccess() repeat = wrap(repeat, [ getopts({'delay': 'positiveInt'}), 'nonInt', 'positiveInt', 'text']) @internationalizeDocstring def list(self, irc, msg, args): """takes no arguments Lists the currently scheduled events. """ L = list(self.events.items()) if L: L.sort() replies = [] now = time.time() for (i, (name, event)) in enumerate(L): if event['type'] == 'single': replies.append(format('%s (in %T): %q', name, event['time'] - now, event['command'])) else: next_run_in = self._getNextRunIn( event['first_run'], now, event['time']) replies.append(format('%s (every %T, next run in %T): %q', name, event['time'], next_run_in, event['command'])) irc.reply(format('%L', replies)) else: irc.reply(_('There are currently no scheduled commands.')) list = wrap(list) Class = Scheduler # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Scheduler/test.py0000644000175000017500000002032214535072470017047 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 * import supybot.schedule as schedule class SchedulerTestCase(ChannelPluginTestCase): plugins = ('Scheduler', 'Utilities') def tearDown(self): schedule.schedule.reset() ChannelPluginTestCase.tearDown(self) def testAddRemove(self): self.assertRegexp('scheduler list', 'no.*commands') m = self.assertNotError('scheduler add 5 echo testAddRemove') self.assertResponse( 'scheduler list', '1 (in 4 seconds): "echo testAddRemove"') timeFastForward(2) self.assertNoResponse(' ', timeout=1) timeFastForward(2) self.assertResponse(' ', 'testAddRemove') m = self.assertNotError('scheduler add 5 echo testAddRemove2') # Get id. id = None for s in m.args[1].split(): s = s.lstrip('#') if s.isdigit(): id = s break self.assertTrue(id, 'Couldn\'t find id in reply.') self.assertNotError('scheduler remove %s' % id) timeFastForward(5) self.assertNoResponse(' ', timeout=1) # Need this test to run first so it has id 0 for its event def test00RemoveZero(self): id = None m = self.assertNotError('scheduler add 5 echo testRemoveZero') for s in m.args[1].split(): s = s.lstrip('#') if s.isdigit(): id = s break self.assertNotError('scheduler remove %s' % id) timeFastForward(5) self.assertNoResponse(' ', timeout=1) def testRemind(self): self.assertNotError('scheduler remind 5 testRemind') self.assertResponse( 'scheduler list', '3 (in 4 seconds): "testRemind"') timeFastForward(3) self.assertNoResponse(' ', timeout=1) timeFastForward(3) self.assertResponse(' ', 'Reminder: testRemind') timeFastForward(5) self.assertNoResponse(' ', timeout=1) self.assertResponse( 'scheduler list', 'There are currently no scheduled commands.') def testRepeat(self): self.assertRegexp('scheduler repeat repeater 5 echo testRepeat', 'testRepeat') timeFastForward(5) self.assertResponse(' ', 'testRepeat') self.assertResponse( 'scheduler list', 'repeater (every 5 seconds, next run in 4 seconds): ' '"echo testRepeat"') timeFastForward(3) self.assertNoResponse(' ', timeout=1) timeFastForward(2) self.assertResponse(' ', 'testRepeat') self.assertNotError('scheduler remove repeater') self.assertRegexp('scheduler list', 'no.*commands') timeFastForward(5) self.assertNoResponse(' ', timeout=1) def testRepeatDelay(self): self.assertNoResponse( 'scheduler repeat --delay 5 repeater 20 echo testRepeat', timeout=1) timeFastForward(5) self.assertResponse(' ', 'testRepeat', timeout=1) timeFastForward(17) self.assertNoResponse(' ', timeout=1) timeFastForward(5) self.assertResponse(' ', 'testRepeat', timeout=1) def testRepeatWorksWithNestedCommands(self): self.assertRegexp('scheduler repeat foo 5 "echo foo [echo nested]"', 'foo nested') timeFastForward(5) self.assertResponse(' ', 'foo nested') timeFastForward(3) self.assertNoResponse(' ', timeout=1) timeFastForward(2) self.assertResponse(' ', 'foo nested') self.assertNotError('scheduler remove foo') timeFastForward(5) self.assertNoResponse(' ', timeout=1) def testRepeatWorksWithNestedCommandsWithNoReply(self): # the 'trylater' command uses ircmsgs.privmsg + irc.noReply(), # similar to how the Anonymous plugin implements sending messages # to channels/users without .reply() (as it is technically not a # reply to the origin message) count = 0 class TryLater(callbacks.Plugin): def trylater(self, irc, msg, args): nonlocal count msg = ircmsgs.privmsg(msg.nick, "%d %s" % (count, args)) irc.queueMsg(msg) irc.noReply() count += 1 cb = TryLater(self.irc) self.irc.addCallback(cb) try: self.assertResponse('scheduler repeat foo 5 "trylater [echo foo]"', "0 ['foo']") timeFastForward(5) self.assertResponse(' ', "1 ['foo']") timeFastForward(5) self.assertResponse(' ', "2 ['foo']") finally: self.irc.removeCallback('TryLater') def testRepeatDisallowsIntegerNames(self): self.assertError('scheduler repeat 1234 1234 "echo NoIntegerNames"') def testRepeatDisallowsDuplicateNames(self): self.assertNotError('scheduler repeat foo 5 "echo foo"') self.assertError('scheduler repeat foo 5 "echo another foo fails"') def testSinglePersistence(self): self.assertRegexp( 'scheduler add 10 echo testSingle', '^The operation succeeded') self.assertNotError('unload Scheduler') schedule.schedule.reset() timeFastForward(20) self.assertNoResponse(' ', timeout=1) self.assertNotError('load Scheduler') self.assertResponse(' ', 'testSingle') def testRepeatPersistence(self): self.assertRegexp( 'scheduler repeat repeater 20 echo testRepeat', 'testRepeat') self.assertNotError('unload Scheduler') schedule.schedule.reset() timeFastForward(30) self.assertNoResponse(' ', timeout=1) self.assertNotError('load Scheduler') self.assertNoResponse(' ', timeout=1) # T+30 to T+31 timeFastForward(5) self.assertNoResponse(' ', timeout=1) # T+36 to T+37 timeFastForward(5) self.assertResponse(' ', 'testRepeat', timeout=1) # T+42 timeFastForward(15) self.assertNoResponse(' ', timeout=1) # T+57 to T+58 timeFastForward(5) self.assertResponse(' ', 'testRepeat', timeout=1) # T+64 self.assertNotError('unload Scheduler') schedule.schedule.reset() timeFastForward(20) self.assertNoResponse(' ', timeout=1) self.assertNotError('load Scheduler') self.assertNoResponse(' ', timeout=1) # T+85 to T+86 timeFastForward(10) self.assertNoResponse(' ', timeout=1) # T+95 to T+96 timeFastForward(10) self.assertResponse(' ', 'testRepeat', timeout=1) # T+106 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/SedRegex/0000755000175000017500000000000014535072535015311 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/SedRegex/__init__.py0000644000175000017500000000522514535072470017424 0ustar00valval### # Copyright (c) 2015, Michael Daniel Telatynski # Copyright (c) 2015-2020, James Lu # Copyright (c) 2020-2021, 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. ### """ History replacer using sed-style expressions. """ import supybot import supybot.world as world __version__ = supybot.version.version __author__ = supybot.Author("Michael Daniel Telatynski", "t3chguy", "postmaster@webdevguru.co.uk") __contributors__ = {supybot.authors.jlu: ["options bolding the replacement text", "misc. bug fixes and enhancements"], supybot.Author('nyuszika7h', 'nyuszika7h', 'nyuszika7h@openmailbox.org'): ["_unpack_sed method within plugin.py"] } __maintainer__ = supybot.authors.limnoria_core __url__ = 'https://github.com/progval/Limnoria/tree/master/plugins/SedRegex' from . import config from . import plugin from . import constants from importlib import reload reload(config) reload(plugin) reload(constants) if world.testing: from . import test Class = plugin.Class configure = config.configure # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/SedRegex/config.py0000644000175000017500000001040414535072470017125 0ustar00valval### # Copyright (c) 2015, Michael Daniel Telatynski # Copyright (c) 2015-2019, James Lu # Copyright (c) 2020-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('SedRegex') except: _ = lambda x: x def configure(advanced): from supybot.questions import expect, anything, something, yn, output conf.registerPlugin('SedRegex', True) if advanced: output("""The SedRegex plugin allows you to make Perl/sed-style regex replacements to your chat history.""") SedRegex = conf.registerPlugin('SedRegex') conf.registerChannelValue(SedRegex, 'displayErrors', registry.Boolean(True, _("""Should errors be displayed?"""))) conf.registerChannelValue(SedRegex, 'boldReplacementText', registry.Boolean(True, _("""Should the replacement text be bolded?"""))) conf.registerChannelValue(SedRegex, 'enable', registry.Boolean(False, _("""Should Perl/sed-style regex replacing work in this channel?"""))) conf.registerChannelValue(SedRegex, 'ignoreRegex', registry.Boolean(True, _("""Should Perl/sed regex replacing ignore messages which look like valid regex?"""))) conf.registerChannelValue(SedRegex, 'format', registry.String(_('$nick meant to say: $replacement'), _("""Sets the format string for a message edited by the original author. Required fields: $nick (nick of the author), $replacement (edited message)"""))) conf.registerChannelValue(SedRegex.format, 'other', registry.String(_('$otherNick thinks $nick meant to say: $replacement'), _(""" Sets the format string for a message edited by another author. Required fields: $nick (nick of the original author), $otherNick (nick of the editor), $replacement (edited message)"""))) conf.registerGlobalValue(SedRegex, 'processTimeout', registry.PositiveFloat(0.5, _("""Sets the timeout when processing a single regexp. The default should be adequate unless you have a busy or low-powered system that cannot process regexps quickly enough. However, you will not want to set this value too high as that would make your bot vulnerable to ReDoS attacks."""))) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/SedRegex/constants.py0000755000175000017500000000155214535072470017703 0ustar00valval#!/usr/bin/env python3 import re TAG_SEEN = 'SedRegex.seen' TAG_IS_REGEX = 'SedRegex.isRegex' SED_REGEX = re.compile( # This part matches an optional nick followed by ":" or ",", used to direct replacement # at a particular user. r"^(?:(?P.+?)[:,] )?" # Match and save the delimiter (any one symbol) as a named group r"s(?P[^\w\s])" # Match the pattern to replace, which can be any string up to the first instance of the delimiter r"(?P(?:(?!(?P=delim)).)*)(?P=delim)" # Ditto with the replacement r"(?P(?:(?!(?P=delim)).)*)" # Optional final delimiter plus flags at the end r"(?:(?P=delim)(?P[a-z]*))?" ) if __name__ == '__main__': print("This is the full regex used by the plugin; paste it into your favourite regex tester " "for debugging:") print(SED_REGEX) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/SedRegex/plugin.py0000644000175000017500000002443314535072470017165 0ustar00valval### # Copyright (c) 2015, Michael Daniel Telatynski # Copyright (c) 2015-2020, James Lu # Copyright (c) 2020-2021, 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.commands import * from supybot.commands import ProcessTimeoutError import supybot.plugins as plugins import supybot.ircmsgs as ircmsgs import supybot.callbacks as callbacks import supybot.ircutils as ircutils import supybot.ircdb as ircdb import supybot.utils as utils import re import sys try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization('SedRegex') except ImportError: _ = lambda x: x from .constants import SED_REGEX, TAG_SEEN, TAG_IS_REGEX # Replace newlines and friends with things like literal "\n" (backslash and "n") axe_spaces = utils.str.MultipleReplacer({'\n': '\\n', '\t': '\\t', '\r': '\\r'}) class SearchNotFoundError(Exception): pass class SedRegex(callbacks.PluginRegexp): """ Enable SedRegex on the desired channels: ``config channel #yourchannel plugins.sedregex.enable True`` After enabling SedRegex, typing a regex in the form ``s/text/replacement/`` will make the bot announce replacements. :: 20:24 helli world 20:24 s/i/o/ 20:24 jlu5 meant to say: hello world You can also do ``othernick: s/text/replacement/`` to only replace messages from a certain user. Supybot ignores are respected by the plugin, and messages from ignored users will only be considered if their nick is explicitly given. Regex flags ^^^^^^^^^^^ The following regex flags (i.e. the ``g`` in ``s/abc/def/g``, etc.) are supported: - ``i``: case insensitive replacement - ``g``: replace all occurences of the original text - ``s``: *(custom flag specific to this plugin)* replace only messages from the caller """ threaded = True public = True unaddressedRegexps = ['replacer'] flags = 0 # Make callback matching case sensitive @staticmethod def _unpack_sed(expr): if '\0' in expr: raise ValueError('Expression can\'t contain NUL') delim = expr[1] escaped_expr = '' for (i, c) in enumerate(expr): if c == delim and i > 0: if expr[i - 1] == '\\': escaped_expr = escaped_expr[:-1] + '\0' continue escaped_expr += c match = SED_REGEX.search(escaped_expr) if not match: return groups = match.groupdict() pattern = groups['pattern'].replace('\0', delim) replacement = groups['replacement'].replace('\0', delim) if groups['flags']: raw_flags = set(groups['flags']) else: raw_flags = set() flags = 0 count = 1 for flag in raw_flags: if flag == 'g': count = 0 if flag == 'i': flags |= re.IGNORECASE pattern = re.compile(pattern, flags) return (pattern, replacement, count, raw_flags) # Tag all messages that SedRegex has seen before. This slightly optimizes the ignoreRegex # feature as all messages tagged with SedRegex.seen but not SedRegex.isRegex is NOT a regexp. # If we didn't have this tag, we'd have to run a regexp match on each message in the history # to check if it's a regexp, as there could've been regexp-like messages sent before # SedRegex was enabled. def doNotice(self, irc, msg): if self.registryValue('enable', msg.channel, irc.network): msg.tag(TAG_SEEN) def doPrivmsg(self, irc, msg): # callbacks.PluginRegexp works by defining doPrivmsg(), we don't want to overwrite # its behaviour super().doPrivmsg(irc, msg) self.doNotice(irc, msg) # SedRegex main routine. This is called automatically by callbacks.PluginRegexp on every # message that matches the SED_REGEX expression defined in constants.py # The actual regexp is passed into PluginRegexp by setting __doc__ equal to the regexp. def replacer(self, irc, msg, regex): if not self.registryValue('enable', msg.channel, irc.network): return self.log.debug("SedRegex: running on %s/%s for %s", irc.network, msg.channel, regex) iterable = reversed(irc.state.history) msg.tag(TAG_IS_REGEX) try: (pattern, replacement, count, flags) = self._unpack_sed(msg.args[1]) except Exception as e: self.log.warning(_("SedRegex parser error: %s"), e, exc_info=True) if self.registryValue('displayErrors', msg.channel, irc.network): irc.error('%s.%s: %s' % (e.__class__.__module__, e.__class__.__name__, e)) return next(iterable) if 's' in flags: # Special 's' flag lets the bot only look at self messages target = msg.nick else: target = regex.group('nick') if not ircutils.isNick(str(target)): return regex_timeout = self.registryValue('processTimeout') try: message = process(self._replacer_process, irc, msg, target, pattern, replacement, count, iterable, timeout=regex_timeout, pn=self.name(), cn='replacer') except ProcessTimeoutError: irc.error(_("Search timed out.")) except SearchNotFoundError: irc.error(_("Search not found in the last %i IRC messages on this network.") % len(irc.state.history)) except Exception as e: self.log.warning(_("SedRegex replacer error: %s"), e, exc_info=True) if self.registryValue('displayErrors', msg.channel, irc.network): irc.error('%s.%s: %s' % (e.__class__.__module__, e.__class__.__name__, e)) else: irc.reply(message, prefixNick=False) replacer.__doc__ = SED_REGEX.pattern def _replacer_process(self, irc, msg, target, pattern, replacement, count, messages): for m in messages: if m.command in ('PRIVMSG', 'NOTICE') and \ ircutils.strEqual(m.args[0], msg.args[0]) and m.tagged('receivedBy') == irc: if target and m.nick != target: continue # Don't snarf ignored users' messages unless specifically # told to. if ircdb.checkIgnored(m.prefix) and not target: continue # When running substitutions, ignore the "* nick" part of any actions. action = ircmsgs.isAction(m) if action: text = ircmsgs.unAction(m) else: text = m.args[1] # Test messages sent before SedRegex was activated. Mark them all as seen # so we only need to do this check once per message. if not m.tagged(TAG_SEEN): m.tag(TAG_SEEN) if SED_REGEX.match(m.args[1]): m.tag(TAG_IS_REGEX) # Ignore messages containing a regexp if ignoreRegex is on. if self.registryValue('ignoreRegex', msg.channel, irc.network) and m.tagged(TAG_IS_REGEX): self.log.debug("Skipping message %s because it is tagged as isRegex", m.args[1]) continue try: replace_result = pattern.search(text) if replace_result: if self.registryValue('boldReplacementText', msg.channel, irc.network): replacement = ircutils.bold(replacement) subst = pattern.sub(replacement, text, count) if action: # If the message was an ACTION, prepend the nick back. subst = '* %s %s' % (m.nick, subst) subst = axe_spaces(subst) if m.nick == msg.nick: fmt = self.registryValue('format', msg.channel, irc.network) env = {'replacement': subst} else: fmt = self.registryValue('format.other', msg.channel, irc.network) env = {'otherNick': msg.nick, 'replacement': subst} return ircutils.standardSubstitute(irc, m, fmt, env) except Exception as e: self.log.warning(_("SedRegex error: %s"), e, exc_info=True) raise self.log.debug(_("SedRegex: Search %r not found in the last %i messages of %s."), msg.args[1], len(irc.state.history), msg.args[0]) raise SearchNotFoundError() Class = SedRegex # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/SedRegex/test.py0000644000175000017500000003153314535072470016645 0ustar00valval### # Copyright (c) 2017-2020, James Lu # Copyright (c) 2020-2021, 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 __future__ import print_function import unittest from supybot.test import * class SedRegexTestCase(ChannelPluginTestCase): other = "blah!blah@someone.else" other2 = "ghost!ghost@spooky" plugins = ('SedRegex', 'Utilities') config = {'plugins.sedregex.enable': True, 'plugins.sedregex.boldReplacementText': False} # getMsg() stalls if no message is ever sent (i.e. if the plugin fails to respond to a request) # We should limit the timeout to prevent the tests from taking forever. timeout = 1 def testSimpleReplace(self): self.feedMsg('Abcd abcdefgh') self.feedMsg('s/abcd/test/') # Run an empty command so that messages from the previous trigger are caught. m = self.getMsg(' ') self.assertIn('Abcd testefgh', str(m)) def testNoMatch(self): self.feedMsg('hello world') self.feedMsg('s/goodbye//') m = self.getMsg(' ') self.assertIn('Search not found', str(m)) self.feedMsg('s/Hello/hi/') # wrong case m = self.getMsg(' ') self.assertIn('Search not found', str(m)) def testCaseInsensitiveReplace(self): self.feedMsg('Aliens Are Invading, Help!') self.feedMsg('s/a/e/i') m = self.getMsg(' ') self.assertIn('eliens', str(m)) def testIgnoreRegexWithBadCase(self): self.feedMsg('aliens are invading, help!') self.assertSnarfNoResponse('S/aliens/monsters/') def testGlobalReplace(self): self.feedMsg('AAaa aaAa a b') self.feedMsg('s/a/e/g') m = self.getMsg(' ') self.assertIn('AAee eeAe e b', str(m)) def testGlobalCaseInsensitiveReplace(self): self.feedMsg('Abba') self.feedMsg('s/a/e/gi') m = self.getMsg(' ') self.assertIn('ebbe', str(m)) def testOnlySelfReplace(self): self.feedMsg('evil machines') self.feedMsg('evil tacocats', frm=self.__class__.other) self.feedMsg('s/evil/kind/s') m = self.getMsg(' ') self.assertIn('kind machines', str(m)) def testAllFlagsReplace(self): self.feedMsg('Terrible, terrible crimes') self.feedMsg('Terrible, terrible TV shows', frm=self.__class__.other) self.feedMsg('s/terr/horr/sgi') m = self.getMsg(' ') self.assertIn('horrible, horrible crimes', str(m)) def testOtherPersonReplace(self): self.feedMsg('yeah, right', frm=self.__class__.other) self.feedMsg('s/right/left/', frm=self.__class__.other2) m = self.getMsg(' ') # Note: using the bot prefix for the s/right/left/ part causes the first nick in "X thinks Y" # to be empty? It works fine in runtime though... self.assertIn('%s thinks %s meant to say' % (ircutils.nickFromHostmask(self.__class__.other2), ircutils.nickFromHostmask(self.__class__.other)), str(m)) def testExplicitOtherReplace(self): self.feedMsg('ouch', frm=self.__class__.other2) self.feedMsg('poof', frm=self.__class__.other) self.feedMsg('wow!') # This should work regardless of whether we use "nick," or "nick:" as prefix self.feedMsg('%s: s/^/p/' % ircutils.nickFromHostmask(self.__class__.other2)) m = self.getMsg(' ') self.assertIn('pouch', str(m)) self.feedMsg('%s, s/^/c/' % ircutils.nickFromHostmask(self.__class__.other2)) m = self.getMsg(' ') self.assertIn('couch', str(m)) @unittest.skipUnless(sys.version_info[0] >= 3, 'Test fails on Python 2.') def testBoldReplacement(self): with conf.supybot.plugins.sedregex.boldReplacementText.context(True): self.feedMsg('hahahaha', frm=self.__class__.other) # One replacement self.feedMsg('s/h/H/', frm=self.__class__.other2) m = self.getMsg(' ') self.assertIn('\x02H\x02aha', str(m)) # Replace all instances self.feedMsg('s/h/H/g', frm=self.__class__.other2) m = self.getMsg(' ') self.assertIn('\x02H\x02a\x02H\x02a', str(m)) # One whole word self.feedMsg('sweet dreams are made of this', frm=self.__class__.other) self.feedMsg('s/this/cheese/', frm=self.__class__.other2) m = self.getMsg(' ') self.assertIn('of \x02cheese\x02', str(m)) def testNonSlashSeparator(self): self.feedMsg('we are all decelopers on this blessed day') self.feedMsg('s.c.v.') m = self.getMsg(' ') self.assertIn('developers', str(m)) self.feedMsg('4 / 2 = 8') self.feedMsg('s@/@*@') m = self.getMsg(' ') self.assertIn('4 * 2 = 8', str(m)) def testWeirdSeparatorsFail(self): self.feedMsg("can't touch this", frm=self.__class__.other) # Only symbols are allowed as separators self.feedMsg('blah: s a b ') self.feedMsg('blah: sdadbd') m = self.getMsg('echo dummy message') # XXX: this is a total hack... for msg in self.irc.state.history: self.assertNotIn("cbn't", str(msg)) def testActionReplace(self): self.feedMsg("\x01ACTION sleeps\x01") self.feedMsg('s/sleeps/wakes/') m = self.getMsg(' ') self.assertIn('meant to say: * %s wakes' % self.nick, str(m)) def testOtherPersonActionReplace(self): self.feedMsg("\x01ACTION sleeps\x01", frm=self.__class__.other) self.feedMsg('s/sleeps/wakes/') m = self.getMsg(' ') n = ircutils.nickFromHostmask(self.__class__.other) self.assertIn('thinks %s meant to say: * %s wakes' % (n, n), str(m)) # https://github.com/jlu5/SupyPlugins/commit/e19abe049888667c3d0a4eb4a2c3ae88b8bea511 # We want to make sure the bot treats channel names case-insensitively, if some client # writes to it using a differente case. def testCaseNormalizationInRead(self): assert self.channel != self.channel.title() # In case Limnoria's defaults change self.feedMsg("what a strange bug", to=self.channel.title()) self.feedMsg('s/strange/hilarious/', to=self.channel) m = self.getMsg(' ') self.assertIn('what a hilarious bug', str(m)) def testCaseNormalizationInReplace(self): assert self.channel != self.channel.title() # In case Limnoria's defaults change self.feedMsg("Segmentation fault", to=self.channel) self.feedMsg('s/$/ (core dumped)/', to=self.channel.title()) m = self.getMsg(' ') self.assertIn('Segmentation fault (core dumped)', str(m)) @unittest.skipIf(world.disableMultiprocessing, "Test requires multiprocessing to be enabled") def testReDoSTimeout(self): # From https://snyk.io/blog/redos-and-catastrophic-backtracking/ for idx in range(500): self.feedMsg("ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX") self.feedMsg(r"s/A(B|C+)+D/this should abort/") m = self.getMsg(' ', timeout=1) self.assertIn('timed out', str(m)) def testMissingTrailingSeparator(self): # Allow the plugin to work if you miss the trailing / self.feedMsg('hello world') self.feedMsg('s/world/everyone') m = self.getMsg(' ') self.assertIn('hello everyone', str(m)) # Make sure it works if there's a space in the replacement self.feedMsg('hello world') self.feedMsg('s@world@how are you') m = self.getMsg(' ') self.assertIn('hello how are you', str(m)) # Ditto with a space in the original text self.feedMsg("foo bar @ baz") self.feedMsg('s/bar @/and') m = self.getMsg(' ') self.assertIn('foo and baz', str(m)) def testIgnoreTextAfterTrailingSeparator(self): # https://github.com/jlu5/SupyPlugins/issues/59 self.feedMsg('see you ltaer') self.feedMsg('s/ltaer/later/ this text will be ignored') m = self.getMsg(' ') self.assertIn('see you later', str(m)) self.feedMsg('s/LTAER/later, bye/i ') m = self.getMsg(' ') self.assertIn('see you later, bye', str(m)) def testIgnoreRegexOnMessagesBeforeEnable(self): # Before 2020-10-12 SedRegex used a single msg.tag() to track and ignore messages parsed as a regexp. # However, a common complaint is that this doesn't catch regexps sent before SedRegex was loaded/enabled... with conf.supybot.plugins.sedregex.enable.context(False): self.feedMsg('foo') self.feedMsg('barbell') self.feedMsg('s/foo/bar/') self.feedMsg('abcdef') self.feedMsg('s/bar/door/') m = self.getMsg(' ') # The INCORRECT response would be "s/foo/door/" self.assertIn('doorbell', str(m)) def testSeparatorPresentInNick(self): # Check that replacement doesn't break if the target's nick contains the sed separator. frm = 'hello|world!~hello@clk-12345678.example.com' with conf.supybot.protocols.irc.strictRfc.context(False): self.feedMsg('the quick brown fox jumps over the lazy hog', frm=frm) # self replace self.feedMsg('s|hog|dog', frm=frm) m = self.getMsg(' ') self.assertIn('the lazy dog', str(m)) # other replace self.feedMsg('%s: s| hog| dog' % ircutils.nickFromHostmask(frm), frm=self.__class__.other2) m = self.getMsg(' ') self.assertIn('the lazy dog', str(m)) def testSlashInNicks(self): # Slash in nicks should be accepted when strictRfc is off frm = 'nick/othernet!hello@othernet.internal' with conf.supybot.protocols.irc.strictRfc.context(False): self.feedMsg('hello world', frm=frm) self.feedMsg('abc 123', frm=frm) # self replace self.feedMsg('s/world/everyone/', frm=frm) m = self.getMsg(' ') self.assertIn('hello everyone', str(m)) # other replace self.feedMsg('%s: s/123/321/' % ircutils.nickFromHostmask(frm), frm=self.__class__.other2) m = self.getMsg(' ') self.assertIn('abc 321', str(m)) # When strictRfc is on, nicks for explicit reference are checked but not # the sender's own nick with conf.supybot.protocols.irc.strictRfc.context(True): self.assertSnarfNoResponse('%s: s/123/321/' % ircutils.nickFromHostmask(frm), frm=self.__class__.other2) def testFmtString(self): fmt = "<$nick>: $replacement" with conf.supybot.plugins.sedregex.format.context(fmt): self.feedMsg('frog') self.feedMsg('s/frog/frogged/') m = self.getMsg(' ') self.assertIn('<%s>: frogged' % self.nick, str(m)) def testFmtStringOtherPerson(self): fmt = "(edited by $otherNick) <$nick>: $replacement" with conf.supybot.plugins.sedregex.format.other.context(fmt): self.feedMsg('frog', frm=self.__class__.other) self.feedMsg('s/frog/frogged/', frm=self.__class__.other2) m = self.getMsg(' ') self.assertIn('(edited by %s) <%s>: frogged' % (ircutils.nickFromHostmask(self.__class__.other2), ircutils.nickFromHostmask(self.__class__.other)), str(m)) # TODO: test ignores # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Seen/0000755000175000017500000000000014535072535014475 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Seen/__init__.py0000644000175000017500000000511514535072470016606 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Keeps track of the last time a user was seen on a channel and what they last said. It also allows you to see what you missed since you parted the channel. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Seen/config.py0000644000175000017500000000564314535072470016322 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Seen') 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('Seen', True) Seen = conf.registerPlugin('Seen') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Seen, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue(Seen, 'minimumNonWildcard', registry.NonNegativeInteger(2, _("""The minimum non-wildcard characters required to perform a 'seen' request. Of course, it only applies if there is a wildcard in the request."""))) conf.registerChannelValue(Seen, 'showLastMessage', registry.Boolean(True, _("""Determines whether the last message will be displayed with @seen. Useful for keeping messages from a channel private."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Seen/locales/0000755000175000017500000000000014535072535016117 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Seen/locales/de.po0000644000175000017500000001322314535072470017046 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:33+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" #: config.py:50 msgid "" "The minimum non-wildcard characters\n" " required to perform a 'seen' request. Of course, it only applies if " "there\n" " is a wildcard in the request." msgstr "" #: config.py:54 msgid "" "Determines whether the last message will\n" " be displayed with @seen. Useful for keeping messages from a channel\n" " private." msgstr "" #: plugin.py:98 msgid "" "This plugin allows you to see when and what someone last said and\n" " what you missed since you left a channel." msgstr "" #: plugin.py:190 msgid "Not enough non-wildcard characters." msgstr "" #: plugin.py:198 plugin.py:306 #, fuzzy msgid "%s was last seen in %s %s ago" msgstr "%s wurde zuletzt in %s vor %s gesehen: %s" #: plugin.py:204 plugin.py:283 plugin.py:310 msgid "%s: %s" msgstr "" #: plugin.py:210 msgid "%s (%s ago)" msgstr "%s (vor %s)" #: plugin.py:212 msgid "%s could be %L" msgstr "%s könnte %L sein" #: plugin.py:212 msgid "or" msgstr "oder" #: plugin.py:214 msgid "I haven't seen anyone matching %s." msgstr "Ich habe niemanden gesehen der auf %s passt." #: plugin.py:216 plugin.py:313 msgid "I have not seen %s." msgstr "Ich habe %s nicht gesehn." #: plugin.py:223 #, fuzzy msgid "You must be in %s to use this command." msgstr "Du musst in %s sein, um diesen Befehl zu benutzen." #: plugin.py:225 #, fuzzy msgid "%s must be in %s to use this command." msgstr "Du musst in %s sein, um diesen Befehl zu benutzen." #: plugin.py:231 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. is only necessary if the message isn't sent on " "the\n" " channel itself. may contain * as a wildcard.\n" " " msgstr "" "[] \n" "\n" "Gibt an wann zum letzten Mal gesehen wurde und was dieser zuletzt " "sagte. ist nur nötig, falls die Nachricht nicht im Kanal selbst " "gesendet wurde. kann * als Platzhalter beinhalten." #: plugin.py:238 plugin.py:256 msgid "You've found me!" msgstr "" #: plugin.py:246 msgid "" "[] [--user ] []\n" "\n" " Returns the last time was seen and what was last seen\n" " doing. This includes any form of activity, instead of just " "PRIVMSGs.\n" " If isn't specified, returns the last activity seen in\n" " . If --user is specified, looks up name in the user " "database\n" " and returns the last time user was active in . " "is\n" " only necessary if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [--user ] []\n" "\n" "Gibt zurück wann zuletzt gesehen wurde und was dieser Tat. Dies " "beinhaltet jede Art von Aktivität, anstatt nur PRIVMSGs. Falls nicht " "angegen wurde, wird die letzte gesehene Aktivität im ausgegeben. " "Falls --user angegeben wird, wird in der Benutzerdatenbank nachgeschaut und " "die Zeit ausgegeben als der Benutzer zuletzte Aktiv war im . " "ist nur nötig, falls die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:280 #, fuzzy msgid "Someone was last seen in %s %s ago" msgstr "Jemand wurde zuletzt in %s gesehen, vor %s: %s" #: plugin.py:286 msgid "I have never seen anyone." msgstr "Ich habe noch niemals jemanden gesehen." #: plugin.py:290 msgid "" "[]\n" "\n" " Returns the last thing said in . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Gibt das zuletzt gesagte im aus. ist nur nötig, falls die " "Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:317 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. This looks up in the user seen database, which " "means\n" " that it could be any nick recognized as user that was seen.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Gibt an wann zum letzten mal gesehen wurde und was zuletzt " "sagte. Der wird in der Gesehen-Datenbank nachgeschaut, dies bedeutet, " "dass es jeder Nick sein kann der vom Benutzer gesehen wurde. " "ist nur nötig, falls die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:331 #, fuzzy msgid "" "[] []\n" "\n" " Returns the messages since last left the channel.\n" " If is not given, it defaults to the nickname of the person\n" " calling the command.\n" " " msgstr "" "[] \n" "\n" "Gibt die Nachrichten zurück, die seit dem verlassen von im Kanal " "gesendet wurden." #: plugin.py:363 msgid "I couldn't find in my history of %s messages where %r last left %s" msgstr "" "Ich konnte in meiner Vergangenheit von %s Nachrichten nichts finden, wo %r " "zuletzt %s verlassen hat." #: plugin.py:372 msgid "Either %s didn't leave, or no messages were sent while %s was gone." msgstr "" "Entweder ist %s nicht gegangen, oder keine Nachrichten wurde gesendet " "während %s weg war." #, fuzzy #~ msgid "I am not in %s." #~ msgstr "Ich habe %s nicht gesehn." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Seen/locales/fi.po0000644000175000017500000001545214535072470017062 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Seen plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 13:06+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 "" "The minimum non-wildcard characters\n" " required to perform a 'seen' request. Of course, it only applies if " "there\n" " is a wildcard in the request." msgstr "" "Vähimmäismäärä ei-jokerimerkkejä, joita vaaditaan 'seen'-pyynnön " "tekemiseen.\n" " Vaikuttaa tietysti vain, jos pyynnössä on jokerimerkki." #: config.py:54 msgid "" "Determines whether the last message will\n" " be displayed with @seen. Useful for keeping messages from a channel\n" " private." msgstr "" "Määrittää näytetäänkö viimeisin viesti @seen-komennolla. Hyödyllinen kanavan " "viestien\n" " yksityisenä pitämiseen." #: plugin.py:98 msgid "" "This plugin allows you to see when and what someone last said and\n" " what you missed since you left a channel." msgstr "" "Tämä plugin sallii jonkun viimeisen sanan näkemisen ja sen mitä sinulta jäi " "näkemättä edellisen\n" "kanavalta poistumisesi jälkeen." #: plugin.py:190 #, fuzzy msgid "Not enough non-wildcard characters." msgstr "Ei-jokerimerkki merkkejä ei ole annettu riittävästi." #: plugin.py:198 plugin.py:306 #, fuzzy msgid "%s was last seen in %s %s ago" msgstr "%s nähtiin viimeksi kanavalla %s %s sitten: %s" #: plugin.py:204 plugin.py:283 plugin.py:310 msgid "%s: %s" msgstr "%s: %s" #: plugin.py:210 msgid "%s (%s ago)" msgstr "%s (%s sitten)" #: plugin.py:212 msgid "%s could be %L" msgstr "%s voisi olla %L" #: plugin.py:212 msgid "or" msgstr "tai" #: plugin.py:214 msgid "I haven't seen anyone matching %s." msgstr "En ole nähnyt kenenkään täsmäävän %s." #: plugin.py:216 plugin.py:313 msgid "I have not seen %s." msgstr "En ole nähnyt %s:ää." #: plugin.py:223 #, fuzzy msgid "You must be in %s to use this command." msgstr "Käyttäjän %s täytyy olla kanavalla %s käyttääkseen tätä komentoa." #: plugin.py:225 msgid "%s must be in %s to use this command." msgstr "Käyttäjän %s täytyy olla kanavalla %s käyttääkseen tätä komentoa." #: plugin.py:231 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. is only necessary if the message isn't sent on " "the\n" " channel itself. may contain * as a wildcard.\n" " " msgstr "" "[] \n" "\n" " Palauttaa viimeisen kerran, kun nähtiin ja mitä " " nähtiin sanovan viimeksi.\n" " on vaadittu vain jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:238 plugin.py:256 msgid "You've found me!" msgstr "Löysit minut!" #: plugin.py:246 msgid "" "[] [--user ] []\n" "\n" " Returns the last time was seen and what was last seen\n" " doing. This includes any form of activity, instead of just " "PRIVMSGs.\n" " If isn't specified, returns the last activity seen in\n" " . If --user is specified, looks up name in the user " "database\n" " and returns the last time user was active in . " "is\n" " only necessary if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [--user ] []\n" "\n" " Palauttaa viimeisen kerran, jolloin nähtiin viimeeksi " "ja mitä nähtiin viimeeksi\n" " tekemässä. Tämä sisältää kaikenlaisen aktiivisuuden, sen sijaan, " "että sisältäisi vain PRIVMSGitä.\n" " Jos ei ole määritetty, palauttaa viimeisen " "aktiivisuuden, joka tapahtui\n" " . Jos --user on määritetty, etsii nimeä " "käyttäjätietokannasta\n" " ja palauttaa viimeisen kerran, kun käyttäjä oli aktiivinen " ". on\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:280 #, fuzzy msgid "Someone was last seen in %s %s ago" msgstr "Joku nähtiin viimeeksi kanavalla %s %s sitten: %s" #: plugin.py:286 msgid "I have never seen anyone." msgstr "Minä en ole nähnyt ketään." #: plugin.py:290 msgid "" "[]\n" "\n" " Returns the last thing said in . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Palauttaa viimeisen asian, joka sanottiin . on " "vaadittu vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:317 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. This looks up in the user seen database, which " "means\n" " that it could be any nick recognized as user that was seen.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Palauttaa viimeisen kerran, kun nähtiin ja mitä " "nähtiin viimeksi\n" " sanomassa. Tämä etsii käyttäjän näkemistietokannasta, mikä " "tarkoittaa,\n" " että se voi olla mikä tahansa nimimerkki. joka on tunnistettu " "käyttäjä joka on nähty.\n" " on vaadittu vain, jos viestiä ei lähetetä kanavalla\n" " itsellään.\n" " " #: plugin.py:331 msgid "" "[] []\n" "\n" " Returns the messages since last left the channel.\n" " If is not given, it defaults to the nickname of the person\n" " calling the command.\n" " " msgstr "" "[] \n" "\n" " Palauttaa sen jälkeiset viestit, kun viimeksi poistui " "kanavalta.\n" "Ellei anneta, se on oletuksena komentoa pyytävän henkilön " "nimimerkki. " #: plugin.py:363 msgid "I couldn't find in my history of %s messages where %r last left %s" msgstr "" "En voinut löytää %s viestin historiasta milloin %r viimeksi lähti kanavalta " "%s" #: plugin.py:372 msgid "Either %s didn't leave, or no messages were sent while %s was gone." msgstr "" "Joko %s ei lähtenyt, tai yhtään viestiä ei lähetetty silloin, kun %s oli " "poissa." #~ msgid "I am not in %s." #~ msgstr "En ole nähnyt %s:ää." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Seen/locales/fr.po0000644000175000017500000001413314535072470017066 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:50 msgid "" "The minimum non-wildcard characters\n" " required to perform a 'seen' request. Of course, it only applies if " "there\n" " is a wildcard in the request." msgstr "" "Le nombre minimum de caractères non-joker nécessaires pour effectuer une " "requête \"seen\". Bien sûr, cela ne s'applique que s'il y a des jokers dans " "la requête." #: config.py:54 msgid "" "Determines whether the last message will\n" " be displayed with @seen. Useful for keeping messages from a channel\n" " private." msgstr "" "Détermine si le dernier message sera affiché avec @seen. Utile pour garder " "les messages d’un salon privés." #: plugin.py:98 msgid "" "This plugin allows you to see when and what someone last said and\n" " what you missed since you left a channel." msgstr "" #: plugin.py:190 msgid "Not enough non-wildcard characters." msgstr "Pas assez de caractères non-joker." #: plugin.py:198 plugin.py:306 msgid "%s was last seen in %s %s ago" msgstr "%s a été vu-e pour la dernière fois sur %s il y a %s" #: plugin.py:204 plugin.py:283 plugin.py:310 msgid "%s: %s" msgstr "%s : %s" #: plugin.py:210 msgid "%s (%s ago)" msgstr "%s (il y a %s)" #: plugin.py:212 msgid "%s could be %L" msgstr "%s doit être %L" #: plugin.py:212 msgid "or" msgstr "ou" #: plugin.py:214 msgid "I haven't seen anyone matching %s." msgstr "Je n'ai vu personne correspondant à %s." #: plugin.py:216 plugin.py:313 msgid "I have not seen %s." msgstr "Je n'ai pas vu %s." #: plugin.py:223 #, fuzzy msgid "You must be in %s to use this command." msgstr "%s doit être dans %s pour utiliser cette commande." #: plugin.py:225 msgid "%s must be in %s to use this command." msgstr "%s doit être dans %s pour utiliser cette commande." #: plugin.py:231 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. is only necessary if the message isn't sent on " "the\n" " channel itself. may contain * as a wildcard.\n" " " msgstr "" "[] \n" "\n" "Retourne la dernière fois que le a été vu-e et la dernière fois que " " a parlé. n'est nécessaire que si le message n'est pas envoyé " "sur le canal lui-même. peut contenir le joker *." #: plugin.py:238 plugin.py:256 msgid "You've found me!" msgstr "" #: plugin.py:246 msgid "" "[] [--user ] []\n" "\n" " Returns the last time was seen and what was last seen\n" " doing. This includes any form of activity, instead of just " "PRIVMSGs.\n" " If isn't specified, returns the last activity seen in\n" " . If --user is specified, looks up name in the user " "database\n" " and returns the last time user was active in . " "is\n" " only necessary if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [--user ] []\n" "\n" "Retourne la dernière fois que le a été vu-e et ce quand iel a fait " "quelque chose pour la première fois. Cela inclue toute forme d'activité, et " "pas uniquement envoyer des messages. Si le n'est pas donné, retourne " "la dernière activité vue sur le . Si --user est spécifié, recherche " "le nom dans la base de données des utilisateurs la dernière fois que celui " "en question a été vu-e actif-ve sur le n'est nécessaire que " "si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:280 msgid "Someone was last seen in %s %s ago" msgstr "Quelqu'un a été vu-e pour la dernière fois sur %s il y a %s" #: plugin.py:286 msgid "I have never seen anyone." msgstr "Je n'ai jamais vu personne." #: plugin.py:290 msgid "" "[]\n" "\n" " Returns the last thing said in . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Retourne la dernière chose dite sur le . n'est nécessaire que " "si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:317 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. This looks up in the user seen database, which " "means\n" " that it could be any nick recognized as user that was seen.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" "Retourne la dernière fois que a été vu-e et la dernière fois que " "a été vu parler. Cela recherche dans la base de données des " "utilisateurs vus, ce qui signifie que si le nick n'était pas reconnu comme " "l'utilisateur , il n'est pas considéré comme vu. n'est " "nécessaire que si le message n'est pas envoyé sur le canal lui-même." #: plugin.py:331 msgid "" "[] []\n" "\n" " Returns the messages since last left the channel.\n" " If is not given, it defaults to the nickname of the person\n" " calling the command.\n" " " msgstr "" "[] \n" "\n" "Retourne les messages depuis que est parti-e du canal. " "correspond par défaut au nick de la personne appelant la commande." #: plugin.py:363 msgid "I couldn't find in my history of %s messages where %r last left %s" msgstr "" "Je ne peux pas trouver dans mon historique de %s messages, où %r a quitté il " "y a %s" #: plugin.py:372 msgid "Either %s didn't leave, or no messages were sent while %s was gone." msgstr "" "Soit %s n'est jamais parti-e, soit aucun message n'a été envoyé depuis " "qu'iel est parti-e." #~ msgid "I am not in %s." #~ msgstr "Je ne suis pas dans %s." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Seen/locales/it.po0000644000175000017500000001326414535072470017077 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-16 00: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:50 msgid "" "The minimum non-wildcard characters\n" " required to perform a 'seen' request. Of course, it only applies if " "there\n" " is a wildcard in the request." msgstr "" #: config.py:54 msgid "" "Determines whether the last message will\n" " be displayed with @seen. Useful for keeping messages from a channel\n" " private." msgstr "" #: plugin.py:98 msgid "" "This plugin allows you to see when and what someone last said and\n" " what you missed since you left a channel." msgstr "" #: plugin.py:190 msgid "Not enough non-wildcard characters." msgstr "" #: plugin.py:198 plugin.py:306 #, fuzzy msgid "%s was last seen in %s %s ago" msgstr "%s è stato visto per l'ultima volta in %s %s fa: %s" #: plugin.py:204 plugin.py:283 plugin.py:310 msgid "%s: %s" msgstr "" #: plugin.py:210 msgid "%s (%s ago)" msgstr "%s (%s fa)" #: plugin.py:212 msgid "%s could be %L" msgstr "%s potrebbe essere %L" #: plugin.py:212 msgid "or" msgstr "oppure" #: plugin.py:214 msgid "I haven't seen anyone matching %s." msgstr "Non ho visto nessuno che corrisponda a %s." #: plugin.py:216 plugin.py:313 msgid "I have not seen %s." msgstr "Non ho visto %s." #: plugin.py:223 #, fuzzy msgid "You must be in %s to use this command." msgstr "Per usare questo comando %s deve essere in %s." #: plugin.py:225 msgid "%s must be in %s to use this command." msgstr "Per usare questo comando %s deve essere in %s." #: plugin.py:231 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. is only necessary if the message isn't sent on " "the\n" " channel itself. may contain * as a wildcard.\n" " " msgstr "" "[] \n" "\n" " Riporta l'ultima volta che è stato visto e cosa stava " "dicendo.\n" " è necessario solo se il messaggio non viene inviato nel " "canale\n" " stesso. può contenere * come wildcard.\n" " " #: plugin.py:238 plugin.py:256 msgid "You've found me!" msgstr "" #: plugin.py:246 msgid "" "[] [--user ] []\n" "\n" " Returns the last time was seen and what was last seen\n" " doing. This includes any form of activity, instead of just " "PRIVMSGs.\n" " If isn't specified, returns the last activity seen in\n" " . If --user is specified, looks up name in the user " "database\n" " and returns the last time user was active in . " "is\n" " only necessary if the message isn't sent on the channel itself.\n" " " msgstr "" "[] [--user ] []\n" "\n" " Riporta l'ultima volta che è stato visto e cosa stava " "facendo.\n" " Include qualsiasi forma di attività, non solo l'invio di messaggi.\n" " Se non è specificato, riporta l'ultima attività vista in " ".\n" " Se --user è specificato, cerca il nome nel database degli utenti e " "riporta\n" " l'ultima volta che l'utente era attivo in . è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:280 #, fuzzy msgid "Someone was last seen in %s %s ago" msgstr "Qualcuno è stato visto per l'ultima volta in %s %s fa: %s" #: plugin.py:286 msgid "I have never seen anyone." msgstr "Non ho mai visto nessuno." #: plugin.py:290 msgid "" "[]\n" "\n" " Returns the last thing said in . is only " "necessary\n" " if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Riporta l'ultima cosa detta in . è necessario\n" " solo se il messaggio non viene inviato nel canale stesso.\n" " " #: plugin.py:317 msgid "" "[] \n" "\n" " Returns the last time was seen and what was last seen\n" " saying. This looks up in the user seen database, which " "means\n" " that it could be any nick recognized as user that was seen.\n" " is only necessary if the message isn't sent in the " "channel\n" " itself.\n" " " msgstr "" "[] \n" "\n" " Riporta l'ultima volta che è stato visto e cosa stava " "dicendo.\n" " Cerca nel database degli utenti visti, ovvero qualsiasi nick " "che\n" " venga riconosciuto con il dell'utente che è stato visto.\n" " è necessario solo se il messaggio non viene inviato nel " "canale stesso.\n" " " #: plugin.py:331 #, fuzzy msgid "" "[] []\n" "\n" " Returns the messages since last left the channel.\n" " If is not given, it defaults to the nickname of the person\n" " calling the command.\n" " " msgstr "" "[] \n" "\n" " Riporta i messaggi da quando ha lasciato il canale l'ultima " "volta.\n" " " #: plugin.py:363 msgid "I couldn't find in my history of %s messages where %r last left %s" msgstr "" "Non trovo nella cronologia dei messaggi di %s dove %r ha lasciato %s " "l'ultima volta." #: plugin.py:372 msgid "Either %s didn't leave, or no messages were sent while %s was gone." msgstr "%s non è uscito o non ha inviato alcun messaggio quando se n'é andato." #~ msgid "I am not in %s." #~ msgstr "Non sono in %s." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Seen/plugin.py0000644000175000017500000004053714535072477016363 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2011, 2013, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import sys import time import supybot.log as log import supybot.conf as conf import supybot.utils as utils import supybot.world as world import supybot.ircdb as ircdb from supybot.commands import * import supybot.irclib as irclib import supybot.utils.minisix as minisix 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, internationalizeDocstring _ = PluginInternationalization('Seen') class IrcStringAndIntDict(utils.InsensitivePreservingDict): def key(self, x): if isinstance(x, int): return x else: return ircutils.toLower(x) class SeenDB(plugins.ChannelUserDB): IdDict = IrcStringAndIntDict def serialize(self, v): return list(v) def deserialize(self, channel, id, L): (seen, saying) = L return (float(seen), saying) def update(self, channel, nickOrId, saying): seen = time.time() self[channel, nickOrId] = (seen, saying) self[channel, ''] = (seen, saying) def seenWildcard(self, channel, nick): nicks = ircutils.IrcSet() nickRe = re.compile('^%s$' % '.*'.join(nick.split('*')), re.I) for (searchChan, searchNick) in self.keys(): #print 'chan: %s ... nick: %s' % (searchChan, searchNick) if isinstance(searchNick, int): # We need to skip the reponses that are keyed by id as they # apparently duplicate the responses for the same person that # are keyed by nick-string continue if ircutils.strEqual(searchChan, channel): if nickRe.search(searchNick) is not None: nicks.add(searchNick) L = [[nick, self.seen(channel, nick)] for nick in nicks] def negativeTime(x): return -x[1][0] utils.sortBy(negativeTime, L) return L def seen(self, channel, nickOrId): return self[channel, nickOrId] filename = conf.supybot.directories.data.dirize('Seen.db') anyfilename = conf.supybot.directories.data.dirize('Seen.any.db') class Seen(callbacks.Plugin): """This plugin allows you to see when and what someone last said and what you missed since you left a channel.""" noIgnore = True def __init__(self, irc): self.__parent = super(Seen, self) self.__parent.__init__(irc) self.db = SeenDB(filename) self.anydb = SeenDB(anyfilename) self.lastmsg = {} world.flushers.append(self.db.flush) world.flushers.append(self.anydb.flush) def die(self): if self.db.flush in world.flushers: world.flushers.remove(self.db.flush) else: self.log.debug('Odd, no flush in flushers: %r', world.flushers) self.db.close() if self.anydb.flush in world.flushers: world.flushers.remove(self.anydb.flush) else: self.log.debug('Odd, no flush in flushers: %r', world.flushers) self.anydb.close() self.__parent.die() def __call__(self, irc, msg): self.__parent.__call__(irc, msg) def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return if msg.channel: channel = msg.channel said = ircmsgs.prettyPrint(msg) self.db.update(channel, msg.nick, said) self.anydb.update(channel, msg.nick, said) try: id = ircdb.users.getUserId(msg.prefix) self.db.update(channel, id, said) self.anydb.update(channel, id, said) except KeyError: pass # Not in the database. def doPart(self, irc, msg): channel = msg.args[0] said = ircmsgs.prettyPrint(msg) self.anydb.update(channel, msg.nick, said) try: id = ircdb.users.getUserId(msg.prefix) self.anydb.update(channel, id, said) except KeyError: pass # Not in the database. doJoin = doPart doKick = doPart def doQuit(self, irc, msg): said = ircmsgs.prettyPrint(msg) try: id = ircdb.users.getUserId(msg.prefix) except KeyError: id = None # Not in the database. for channel in msg.tagged('channels'): self.anydb.update(channel, msg.nick, said) if id is not None: self.anydb.update(channel, id, said) doNick = doQuit def doMode(self, irc, msg): # Filter out messages from network Services if msg.nick: try: id = ircdb.users.getUserId(msg.prefix) except KeyError: id = None # Not in the database. channel = msg.args[0] said = ircmsgs.prettyPrint(msg) self.anydb.update(channel, msg.nick, said) if id is not None: self.anydb.update(channel, id, said) doTopic = doMode def _seen(self, irc, channel, name, any=False): if any: db = self.anydb else: db = self.db try: results = [] if '*' in name: if (len(name.replace('*', '')) < self.registryValue('minimumNonWildcard', channel, irc.network)): irc.error(_('Not enough non-wildcard characters.'), Raise=True) results = db.seenWildcard(channel, name) else: results = [[name, db.seen(channel, name)]] if len(results) == 1: (nick, info) = results[0] (when, said) = info if nick in irc.state.channels[channel].users: reply = format(_('%s was last seen in %s %s ago, and is in the channel now'), nick, channel, utils.timeElapsed(time.time()-when)) else: reply = format(_('%s was last seen in %s %s ago'), nick, channel, utils.timeElapsed(time.time()-when)) if self.registryValue('showLastMessage', channel, irc.network): if minisix.PY2: said = said.decode('utf8') reply = _('%s: %s') % (reply, said) irc.reply(reply) elif len(results) > 1: L = [] for (nick, info) in results: (when, said) = info if nick in irc.state.channels[channel].users: L.append(format(_('%s (%s ago, and is in the channel now)'), nick, utils.timeElapsed(time.time()-when))) else: L.append(format(_('%s (%s ago)'), nick, utils.timeElapsed(time.time()-when))) irc.reply(format(_('%s could be %L'), name, (L, _('or')))) else: irc.reply(format(_('I haven\'t seen anyone matching %s.'), name)) except KeyError: if name in irc.state.channels[channel].users: irc.reply(format(_("%s is in the channel right now."), name)) else: irc.reply(format(_('I have not seen %s.'), name)) def _checkChannelPresence(self, irc, channel, target, you): if channel not in irc.state.channels: irc.error(_("I'm not in %s." % channel), Raise=True) if target not in irc.state.channels[channel].users: if you: msg = format(_('You must be in %s to use this command.'), channel) else: msg = format(_('%s must be in %s to use this command.'), target, channel) irc.error(msg, Raise=True) @internationalizeDocstring def seen(self, irc, msg, args, channel, name): """[] Returns the last time was seen and what was last seen saying. is only necessary if the message isn't sent on the channel itself. may contain * as a wildcard. """ if name and ircutils.strEqual(name, irc.nick): irc.reply(_("You've found me!")) return self._checkChannelPresence(irc, channel, msg.nick, True) self._seen(irc, channel, name) seen = wrap(seen, ['channel', 'something']) @internationalizeDocstring def any(self, irc, msg, args, channel, optlist, name): """[] [--user ] [] Returns the last time was seen and what was last seen doing. This includes any form of activity, instead of just PRIVMSGs. If isn't specified, returns the last activity seen in . If --user is specified, looks up name in the user database and returns the last time user was active in . is only necessary if the message isn't sent on the channel itself. """ if name and ircutils.strEqual(name, irc.nick): irc.reply(_("You've found me!")) return self._checkChannelPresence(irc, channel, msg.nick, True) if name and optlist: raise callbacks.ArgumentError elif name: self._seen(irc, channel, name, any=True) elif optlist: for (option, arg) in optlist: if option == 'user': user = arg self._user(irc, channel, user, any=True) else: self._last(irc, channel, any=True) any = wrap(any, ['channel', getopts({'user': 'otherUser'}), additional('something')]) def _last(self, irc, channel, any=False): if any: db = self.anydb else: db = self.db try: (when, said) = db.seen(channel, '') pattern = r'<(.*?)>' match = re.search(pattern, said) if not match: irc.error(format(_('I couldn\'t parse the nick of the speaker of the last line.')), Raise=True) nick = match.group(1) reply = format(_('Last seen in %s was %s, %s ago'), channel, nick, utils.timeElapsed(time.time()-when)) if self.registryValue('showLastMessage', channel, irc.network): reply = _('%s: %s') % (reply, said) irc.reply(reply) except KeyError: irc.reply(_('I have never seen anyone.')) @internationalizeDocstring def last(self, irc, msg, args, channel): """[] Returns the last thing said in . is only necessary if the message isn't sent in the channel itself. """ self._checkChannelPresence(irc, channel, msg.nick, True) self._last(irc, channel) last = wrap(last, ['channel']) def _user(self, irc, channel, user, any=False): if any: db = self.anydb else: db = self.db try: (when, said) = db.seen(channel, user.id) if user.name in irc.state.channels[channel].users: reply = format(_('%s was last seen in %s %s ago and is in the channel now'), user.name, channel, utils.timeElapsed(time.time()-when)) else: reply = format(_('%s was last seen in %s %s ago'), user.name, channel, utils.timeElapsed(time.time()-when)) if self.registryValue('showLastMessage', channel, irc.network): reply = _('%s: %s') % (reply, said) irc.reply(reply) except KeyError: if user.name in irc.state.channels[channel].users: irc.reply(format(_("%s is in the channel right now."), user.name)) else: irc.reply(format(_('I have not seen %s.'), user.name)) @internationalizeDocstring def user(self, irc, msg, args, channel, user): """[] Returns the last time was seen and what was last seen saying. This looks up in the user seen database, which means that it could be any nick recognized as user that was seen. is only necessary if the message isn't sent in the channel itself. """ self._checkChannelPresence(irc, channel, msg.nick, True) self._user(irc, channel, user) user = wrap(user, ['channel', 'otherUser']) @internationalizeDocstring def since(self, irc, msg, args, channel, nick): """[] [] Returns the messages since last left the channel. If is not given, it defaults to the nickname of the person calling the command. """ if nick is None: nick = msg.nick you = True else: you = False self._checkChannelPresence(irc, channel, nick, you) if nick is None: nick = msg.nick end = None # By default, up until the most recent message. for (i, m) in utils.seq.renumerate(irc.state.history): if end is None and m.command == 'JOIN' and \ ircutils.strEqual(m.args[0], channel) and \ ircutils.strEqual(m.nick, nick): end = i if m.command == 'PART' and \ ircutils.strEqual(m.nick, nick) and \ ircutils.strEqual(m.args[0], channel): break elif m.command == 'QUIT' and ircutils.strEqual(m.nick, nick): # XXX We assume the person was in-channel at this point. break elif m.command == 'KICK' and \ ircutils.strEqual(m.args[1], nick) and \ ircutils.strEqual(m.args[0], channel): break else: # I never use this; it only kicks in when the for loop exited normally. irc.error(format(_('I couldn\'t find in my history of %s messages ' 'where %r last left %s'), len(irc.state.history), nick, channel)) return msgs = [m for m in irc.state.history[i:end] if m.command == 'PRIVMSG' and ircutils.strEqual(m.args[0], channel)] if msgs: irc.reply(format('%L', list(map(ircmsgs.prettyPrint, msgs)))) else: irc.reply(format(_('Either %s didn\'t leave, ' 'or no messages were sent while %s was gone.'), nick, nick)) since = wrap(since, ['channel', additional('nick')]) Class = Seen # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Seen/test.py0000644000175000017500000001441614535072477016041 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2013, James McCoy # Copyright (c) 2010-2021, 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 * import supybot.ircdb as ircdb class ChannelDBTestCase(ChannelPluginTestCase): plugins = ('Seen', 'User') def setUp(self): ChannelPluginTestCase.setUp(self) self.prefix = 'foo!bar@baz' self.nick = 'foo' self.wildcardTest = ['f*', '*oo', '*foo*', 'f*o*o'] self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar', prefix=self.prefix)) _ = self.irc.takeMsg() chancap = ircdb.makeChannelCapability(self.channel, 'op') ircdb.users.getUser(self.nick).addCapability(chancap) def testNoKeyErrorEscapeFromSeen(self): self.irc.feedMsg(ircmsgs.join(self.channel, self.irc.nick, prefix=self.prefix)) self.assertRegexp('seen asldfkjasdlfkj', '^I have not seen') self.assertNotRegexp('seen asldfkjasdlfkj', 'KeyError') def testAny(self): self.irc.feedMsg(ircmsgs.join(self.channel, self.irc.nick, prefix=self.prefix)) self.assertRegexp('seen any', '%s <%s> has joined' % (self.nick, self.prefix)) self.irc.feedMsg(ircmsgs.mode(self.channel, args=('+o', self.nick), prefix=self.prefix)) self.assertRegexp('seen any %s' % self.nick, '^%s was last seen.*:' % self.nick) with conf.supybot.plugins.seen.showLastMessage.context(False): self.assertRegexp('seen any %s' % self.nick, '^%s was last seen[^:]*' % self.nick) self.assertNotError('config plugins.Seen.minimumNonWildcard 0') orig = conf.supybot.protocols.irc.strictRfc() try: for state in (True, False): conf.supybot.protocols.irc.strictRfc.setValue(state) for wildcard in self.wildcardTest: self.assertRegexp('seen any %s' % wildcard, '^%s was last seen' % self.nick) self.assertRegexp('seen any bar*', '^I haven\'t seen anyone matching') finally: conf.supybot.protocols.irc.strictRfc.setValue(orig) def testSeen(self): self.irc.feedMsg(ircmsgs.join(self.channel, self.irc.nick, prefix=self.prefix)) self.assertNotError('seen last') self.assertNotError('list') self.assertNotError('config plugins.Seen.minimumNonWildcard 2') self.assertRegexp('seen user %s' % self.nick, '^%s was last seen' % self.nick) self.assertError('seen *') self.assertNotError('seen %s' % self.nick) self.assertNotError('config plugins.Seen.minimumNonWildcard 0') orig = conf.supybot.protocols.irc.strictRfc() try: for state in (True, False): conf.supybot.protocols.irc.strictRfc.setValue(state) for wildcard in self.wildcardTest: self.assertRegexp('seen %s' % wildcard, '^%s was last seen' % self.nick) self.assertRegexp('seen bar*', '^I haven\'t seen anyone matching') finally: conf.supybot.protocols.irc.strictRfc.setValue(orig) def testSeenNickInChannel(self): # Test case: 'seen' with a nick (user in channel) self.irc.feedMsg(ircmsgs.join(self.channel, self.irc.nick, prefix=self.prefix)) self.assertRegexp('seen %s' % self.nick, 'is in the channel right now') m = self.assertNotError('seen %s' % self.nick.upper()) self.assertIn(self.nick.upper(), m.args[1]) def testSeenUserInChannel(self): # Test case: 'seen' with a user (user in channel) self.irc.feedMsg(ircmsgs.join(self.channel, self.irc.nick, prefix=self.prefix)) self.assertRegexp('seen user %s' % self.nick, 'is in the channel right now') def testSeenNickNotInChannel(self): # Test case: 'seen' with a nick (user not in channel) testnick = "user123" self.irc.feedMsg(ircmsgs.join(self.channel, testnick, "user123!baz")) self.irc.feedMsg(ircmsgs.part(self.channel, prefix="user123!baz")) self.assertNotRegexp("seen %s" % testnick, "is in the channel right now") def testSeenNoUser(self): self.irc.feedMsg(ircmsgs.join(self.channel, self.irc.nick, prefix=self.prefix)) self.assertNotRegexp('seen user alsdkfjalsdfkj', 'KeyError') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Services/0000755000175000017500000000000014535072535015366 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/__init__.py0000644000175000017500000000505714535072470017504 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Services: Handles management of nicks with NickServ, and ops with ChanServ; to (re)gain access to its own nick and channels. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/config.py0000644000175000017500000001303214535072470017202 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.ircutils as ircutils import supybot.registry as registry from supybot.i18n import PluginInternationalization _ = PluginInternationalization('Services') def registerNick(nick, password=''): p = conf.supybot.plugins.Services.Nickserv.get('password') h = _('Determines what password the bot will use with NickServ when ' \ 'identifying as %s.') % nick v = conf.registerNetworkValue(p, nick, registry.String(password, h, private=True)) if password: v.setValue(password) def configure(advanced): from supybot.questions import something conf.registerPlugin('Services', True) nick = something(_('What is your registered nick?')) password = something(_('What is your password for that nick?')) chanserv = something(_('What is your ChanServ named?'), default='ChanServ') nickserv = something(_('What is your NickServ named?'), default='NickServ') conf.supybot.plugins.Services.nicks.setValue([nick]) conf.supybot.plugins.Services.NickServ.setValue(nickserv) registerNick(nick, password) conf.supybot.plugins.Services.ChanServ.setValue(chanserv) class ValidNickOrEmptyString(registry.String): def setValue(self, v): if v and not ircutils.isNick(v): raise registry.InvalidRegistryValue('Value must be a valid nick or the empty string.') registry.String.setValue(self, v) class ValidNickSet(conf.ValidNicks): List = ircutils.IrcSet sorted = True Services = conf.registerPlugin('Services') conf.registerNetworkValue(Services, 'nicks', ValidNickSet([], _("""Space-separated list of nicks the bot will use with services."""))) class Networks(registry.SpaceSeparatedSetOfStrings): List = ircutils.IrcSet conf.registerGlobalValue(Services, 'disabledNetworks', Networks(_('QuakeNet').split(), _("""Determines what networks this plugin will be disabled on."""))) conf.registerNetworkValue(Services, 'noJoinsUntilIdentified', registry.Boolean(False, _("""Determines whether the bot will not join any channels until it is identified. This may be useful, for instances, if you have a vhost that isn't set until you're identified, or if you're joining +r channels that won't allow you to join unless you identify."""))) conf.registerNetworkValue(Services, 'ghostDelay', registry.NonNegativeInteger(60, _("""Determines how many seconds the bot will wait between successive GHOST attempts. Set this to 0 to disable GHOST."""))) conf.registerNetworkValue(Services, 'ghostCommand', registry.String("GHOST", _("""Determines the NickServ command to use for GHOST. If the network you're using runs Anope, set this to "RECOVER". If the network you're using runs Atheme, set this to "GHOST" or "REGAIN"."""))) conf.registerNetworkValue(Services, 'NickServ', ValidNickOrEmptyString('NickServ', _("""Determines what nick the 'NickServ' service has."""))) conf.registerGroup(Services.NickServ, 'password') conf.registerNetworkValue(Services, 'ChanServ', ValidNickOrEmptyString('ChanServ', _("""Determines what nick the 'ChanServ' service has."""))) conf.registerChannelValue(Services.ChanServ, 'password', registry.String('', _("""Determines what password the bot will use with ChanServ."""), private=True)) conf.registerChannelValue(Services.ChanServ, 'op', registry.Boolean(False, _("""Determines whether the bot will request to get opped by the ChanServ when it joins the channel."""))) conf.registerChannelValue(Services.ChanServ, 'halfop', registry.Boolean(False, _("""Determines whether the bot will request to get half-opped by the ChanServ when it joins the channel."""))) conf.registerChannelValue(Services.ChanServ, 'voice', registry.Boolean(False, _("""Determines whether the bot will request to get voiced by the ChanServ when it joins the channel."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Services/locales/0000755000175000017500000000000014535072535017010 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/locales/de.po0000644000175000017500000002631214535072470017742 0ustar00valval# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-11-13 18:58+0100\n" "Last-Translator: Florian Besser \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" #: config.py:40 msgid "" "Determines what password the bot will use with NickServ when identifying as " "%s." msgstr "" "Legt fest welches Passwort der Bot verwenden wird um sich als %s bei " "NickServ zu identifizieren." #: config.py:50 msgid "What is your registered nick?" msgstr "Wie ist der registrierte Nick?" #: config.py:51 msgid "What is your password for that nick?" msgstr "Was ist dein Passwort für den Nick?" #: config.py:52 msgid "What is your ChanServ named?" msgstr "Wie heißt der ChanServ?" #: config.py:53 msgid "What is your NickServ named?" msgstr "Wie heißt der NickServ?" #: config.py:70 msgid "" "Determines what nicks the bot will use with\n" " services." msgstr "Legt fest der was für einen Nick der Bot mit den Services nutzt." #: config.py:77 msgid "" "Determines what networks this plugin\n" " will be disabled on." msgstr "Legt fest auf welchen Netzwerken dieses Plugin deaktiviert sein soll." #: config.py:77 msgid "QuakeNet" msgstr "QuakeNet" #: config.py:81 msgid "" "Determines whether the bot will not join any\n" " channels until it is identified. This may be useful, for instances, if\n" " you have a vhost that isn't set until you're identified, or if you're\n" " joining +r channels that won't allow you to join unless you identify." msgstr "" "Legt fest ob der Bot Kanäle betreten soll, solange er noch nicht " "identifiziert ist. Das könnte nützlich sein falls du einen vHost hast der " "erst nach dem Identifizeren gesetzt wird, oder du +R Kanaäle betreten willt, " "die es nicht erlauben sie zu betreten solange du nicht identifiziert bist." #: config.py:86 #, fuzzy msgid "" "Determines how many seconds the bot will\n" " wait between successive GHOST attempts. Set this to 0 to disable GHOST." msgstr "legt fest wie viele Sekunden der Bot zwischen GHOST versuchen wartet." #: config.py:89 msgid "" "Determines what nick the 'NickServ' service\n" " has." msgstr "Legt fest welchen Nick der 'NickServ' Service hat." #: config.py:93 msgid "" "Determines what nick the 'ChanServ' service\n" " has." msgstr "Legt fest welchen Nick der 'ChanServ' Service hat." #: config.py:96 msgid "" "Determines what password the bot will use with\n" " ChanServ." msgstr "Legt fest welches Passwort der Bot für den ChanServ nutzt." #: config.py:99 msgid "" "Determines whether the bot will request to get\n" " opped by the ChanServ when it joins the channel." msgstr "" "Legt fest ob der Bot Op von ChanServ erfragen soll, sobald er einen Kanal " "betritt." #: config.py:102 msgid "" "Determines whether the bot will request to get\n" " half-opped by the ChanServ when it joins the channel." msgstr "" "Legt fest ob der Bot halb-Op von ChanServ erfragen soll, sobald er einen " "Kanal betritt." #: config.py:105 msgid "" "Determines whether the bot will request to get\n" " voiced by the ChanServ when it joins the channel." msgstr "" "Legt fest ob der Bot Voice von ChanServ erfragen soll, sobald er einen Kanal " "betritt." #: plugin.py:54 msgid "" "This plugin handles dealing with Services on networks that provide them.\n" " Basically, you should use the \"password\" command to tell the bot a " "nick to\n" " identify with and what password to use to identify with that nick. You " "can\n" " use the password command multiple times if your bot has multiple nicks\n" " registered. Also, be sure to configure the NickServ and ChanServ\n" " configuration variables to match the NickServ and ChanServ nicks on " "your\n" " network. Other commands such as identify, op, etc. should not be\n" " necessary if the bot is properly configured." msgstr "" "Die Plugin kümmert sich um die Services, die das Netzwerk anbietet. Du " "solltest den \"password\" Befehl nutzen um den Bot zu sagen mit welchem Nick " "er sich identifizieren soll und welches Passwort er verwenden soll um sich " "zu identifizieren. Du kanns den Passwort Befehl mehrere Male benutzen, " "falls dein Bot mehrere Nicks registriert hat. Stelle außerdem sicher,dass " "die NickServ und ChanServ Variablen auf die NickServ und ChanServ Nicks im " "Netzwerk zutreffen. Andere Befehle, wie identify, op etc., sollten nicht " "nötig sein falls der Bot richtig konfiguriert ist." #: plugin.py:441 msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to send the " "%s command." msgstr "" "Du musst supybot.plugins.Services.ChanServ setzen, damit es mir möglich ist " "den %s Befehl auszuführen." #: plugin.py:447 msgid "" "[]\n" "\n" " Attempts to get opped by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Versucht Op durch ChanServ im zu bekommen. ist nur " "notwendig, falls die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:453 msgid "I'm already opped in %s." msgstr "Ich habe schon Op in %s." #: plugin.py:460 msgid "" "[]\n" "\n" " Attempts to get voiced by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Versucht Voice durch ChanServ im zu bekommen. ist nur " "notwendig, falls die Nachricht nicht im Kanal selbst gesendet wurde." #: plugin.py:466 msgid "I'm already voiced in %s." msgstr "Ich habe schon Voice in %s." #: plugin.py:483 msgid "" "[]\n" "\n" " Attempts to get unbanned by ChanServ in . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Versucht Op durch ChanServ im zu bekommen. ist nur " "notwendig, falls die Nachricht nicht im Kanal selbst gesendet wurde, " "wahrscheinlich wird der Befehl sowieso nicht im Kanal selbst gesendet, wenn " "du den Befehl braucht." #: plugin.py:504 msgid "" "[]\n" "\n" " Attempts to get invited by ChanServ to . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Versucht Op durch ChanServ im zu bekommen. ist nur " "notwendig, falls die Nachricht nicht im Kanal selbst gesendet wurde, " "wahrscheinlich wird der Befehl sowieso nicht im Kanal selbst gesendet, wenn " "du den Befehl braucht." #: plugin.py:526 msgid "" "takes no arguments\n" "\n" " Identifies with NickServ using the current nick.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Identifiziert mit dem NickServ mit dem momentanen Nick." #: plugin.py:535 msgid "I don't have a configured password for my current nick." msgstr "Du hast kein Passwort für meinen momentanen Nick konfiguriert." #: plugin.py:538 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to do " "identify." msgstr "" "Du musst supybot.plugins.Services.NickServ setzen, damit es mich möglich ist " "mich zu identifizieren." #: plugin.py:544 msgid "" "[]\n" "\n" " Ghosts the bot's given nick and takes it. If no nick is given,\n" " ghosts the bot's configured nick and takes it.\n" " " msgstr "" "[]\n" "\n" "'Ghost' den Bot Nick und nimmt ihn sich. Falls kein Nick angegeben wirde, " "wird 'ghost' für den konfigurierten Nick gesendet." #: plugin.py:553 msgid "I cowardly refuse to ghost myself." msgstr "Ich verweigere es auf mich selbst 'ghost' anzuwenden." #: plugin.py:558 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to ghost a " "nick." msgstr "" "Du musst supybot.plugins.Services.NickServ setzen, erst dann ist es mir " "möglich 'ghost' auf einen Nick anzuwenden." #: plugin.py:563 msgid "" "\n" "\n" " Sends the to NickServ. For example, to register to NickServ\n" " on Atheme, use: @nickserv REGISTER ." msgstr "" #: plugin.py:572 #, fuzzy msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to message " "NickServ" msgstr "" "Du musst supybot.plugins.Services.NickServ setzen, erst dann ist es mir " "möglich 'ghost' auf einen Nick anzuwenden." #: plugin.py:577 msgid "" "\n" "\n" " Sends the to ChanServ. For example, to register a channel\n" " on Atheme, use: @chanserv REGISTER <#channel>." msgstr "" #: plugin.py:586 #, fuzzy msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to message " "ChanServ" msgstr "" "Du musst supybot.plugins.Services.ChanServ setzen, damit es mir möglich ist " "den %s Befehl auszuführen." #: plugin.py:593 msgid "" " []\n" "\n" " Sets the NickServ password for to . If " "is\n" " not given, removes from the configured nicks.\n" " " msgstr "" " []\\n\n" "Setzt das NickServ Passwort für auf . Falls " "nicht angegeben wurde, wird der aus der Liste der konfigurierten " "Nicks entfernt." #: plugin.py:605 msgid "That nick was not configured with a password." msgstr "Für diesen Nick wurde kein Passwort konfiguriert." #: plugin.py:618 msgid "" "takes no arguments\n" "\n" " Returns the nicks that this plugin is configured to identify and " "ghost\n" " with.\n" " " msgstr "" "hat keine Argumente \n" "\n" "Gibt die Nicks aus, für die dieses Plugin konfiguriert wurde um sich zu " "identifizieren und 'ghost' zu benutzen." #: plugin.py:628 msgid "I'm not currently configured for any nicks." msgstr "Ich habe zur Zeit keine Nicks konfiguriert." #: plugin.py:635 msgid "Experimental IRC extensions are not enabled for this bot." msgstr "" #: plugin.py:641 msgid "This network does not support draft/account-registration." msgstr "" #: plugin.py:647 msgid "This network does not support labeled-response." msgstr "" #: plugin.py:653 msgid "This bot is already authenticated on the network." msgstr "" #: plugin.py:658 msgid "" "[] []\n" "\n" " Uses the experimental REGISTER command to create an account for the " "bot\n" " on the , using the and the if provided.\n" " Some networks may require the email.\n" " You may need to use the 'services verify' command afterward to " "confirm\n" " your email address." msgstr "" #: plugin.py:672 msgid "This network requires an email address to register." msgstr "" #: plugin.py:686 msgid "" "[] \n" "\n" " If the requires a verification code, you need to call " "this\n" " command with the code the server gave you to finish the\n" " registration." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/locales/fi.po0000644000175000017500000002710314535072470017747 0ustar00valval# Services plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-08-13 23:02+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: config.py:40 msgid "" "Determines what password the bot will use with NickServ when identifying as " "%s." msgstr "" "Määrittää mitä salasanaa botti käyttää, kun tunnistautuu %s:ksi NickServillä." #: config.py:50 msgid "What is your registered nick?" msgstr "Mikä on rekisteröity nimimerkkisi?" #: config.py:51 msgid "What is your password for that nick?" msgstr "Mikä on salasanasi tuolle nimimerkille?" #: config.py:52 msgid "What is your ChanServ named?" msgstr "Minkä niminen ChanServisi on?" #: config.py:53 msgid "What is your NickServ named?" msgstr "Minkä niminen NickServisi on?" #: config.py:70 msgid "" "Determines what nicks the bot will use with\n" " services." msgstr "" "Määrittää mitä nimimerkkiä botti käyttää\n" " palvelujen kanssa." #: config.py:77 msgid "" "Determines what networks this plugin\n" " will be disabled on." msgstr "" "Määrittää missä verkoissa tämä\n" " lisäosa on poistettu käytöstä." #: config.py:77 msgid "QuakeNet" msgstr "QuakeNet" #: config.py:81 msgid "" "Determines whether the bot will not join any\n" " channels until it is identified. This may be useful, for instances, if\n" " you have a vhost that isn't set until you're identified, or if you're\n" " joining +r channels that won't allow you to join unless you identify." msgstr "" "Määrittää liittyykö botti millekään kanavalle ennen\n" " kuin se on tunnistautunut. Tämä voi olla hyödyllinen, esimerkiksi, jos\n" " sinulla on vhost, jota ei aseteta ennen kuin tunnistaudut, tai jos olet\n" " liittymässä +r kanaville, jotka eivät salli sinun liittyvän, paitsi jos " "olet tunnistautunut." #: config.py:86 #, fuzzy msgid "" "Determines how many seconds the bot will\n" " wait between successive GHOST attempts. Set this to 0 to disable GHOST." msgstr "" "Määrittää monta sekuntia botti odottaa\n" " GHOSTaus yritysten välillä." #: config.py:89 msgid "" "Determines what nick the 'NickServ' service\n" " has." msgstr "" "Määrittää mikä nimimerkki 'NickServ' palvelulla\n" " on." #: config.py:93 msgid "" "Determines what nick the 'ChanServ' service\n" " has." msgstr "" "Määrittää mikä nimimerkki 'ChanServ' palvelulla\n" " on." #: config.py:96 msgid "" "Determines what password the bot will use with\n" " ChanServ." msgstr "" "Määrittää mitä salasanaa botti käyttää\n" " ChanServin kanssa." #: config.py:99 msgid "" "Determines whether the bot will request to get\n" " opped by the ChanServ when it joins the channel." msgstr "" "Määrittää pyytääkö botti ChanServin oppaamaksi\n" " tulemista, kun se liittyy kanavalle." #: config.py:102 msgid "" "Determines whether the bot will request to get\n" " half-opped by the ChanServ when it joins the channel." msgstr "" "Määrittää pyytääkö botti tulla ChanServin puolioppaamaksi, kun\n" " se liittyy kanavalle." #: config.py:105 msgid "" "Determines whether the bot will request to get\n" " voiced by the ChanServ when it joins the channel." msgstr "" "Määrittää pyytääkö botti ChanServiltä äänen, kun\n" " se liittyy kanavalle." #: plugin.py:54 msgid "" "This plugin handles dealing with Services on networks that provide them.\n" " Basically, you should use the \"password\" command to tell the bot a " "nick to\n" " identify with and what password to use to identify with that nick. You " "can\n" " use the password command multiple times if your bot has multiple nicks\n" " registered. Also, be sure to configure the NickServ and ChanServ\n" " configuration variables to match the NickServ and ChanServ nicks on " "your\n" " network. Other commands such as identify, op, etc. should not be\n" " necessary if the bot is properly configured." msgstr "" "Tämä lisäosa hoitaa palvelut verkoissa, jotka tarjoavat niitä.\n" " Perusteellisesti, sinun pitäisi käyttää komentoa \"password\" " "kertoaksesi botille\n" " millä salasanalla ja tunnuksella se tunnistautuu. Voit käyttää\n" " salasanakomentoa monta kertaa, mikäli botillasi on monta rekisteröityä\n" " nimimerkkiä. Varmista myös, että asetusarvot täsmäävät\n" " NickServin ja ChanServin nimimerkkeihin verkossasi.\n" " Muiden komentojen, kuten identify, op, jne. ei\n" " pitäisi olla vaadittuja, jos botti on määritetty hyvin." #: plugin.py:441 msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to send the " "%s command." msgstr "" "Sinun täytyy asettaa supybot.plugins.Services.ChanServ ennen kuin pystyn " "lähettämään komennon %s." #: plugin.py:447 msgid "" "[]\n" "\n" " Attempts to get opped by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Yrittää päästä ChanServin oppaamaksi . on " "vaadittu vain,\n" " jos viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:453 msgid "I'm already opped in %s." msgstr "Minut on jo opattu kanavalla %s." #: plugin.py:460 msgid "" "[]\n" "\n" " Attempts to get voiced by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Yrittää saada ChanServiltä äänen . on vaadittu " "vain, jos\n" " viestiä ei lähetetä kanavalla itsellään.\n" " " #: plugin.py:466 msgid "I'm already voiced in %s." msgstr "Minulla on jo ääni kanavalla %s." #: plugin.py:483 msgid "" "[]\n" "\n" " Attempts to get unbanned by ChanServ in . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Yrittää poistaa porttikiellon käyttämällä ChanServiä. " " on vaadittu vain, jos\n" " viestiä ei lähetetä kanavalla itsellään, mutta luultavast jos " "tarvitset tätä komentoa, et ole kanavallai\n" " itsellään.\n" " " #: plugin.py:504 msgid "" "[]\n" "\n" " Attempts to get invited by ChanServ to . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Yrittää saada ChanServiltä kutsun . on vaadittu " "vain,\n" " jos viestiä ei lähetetä kanavalla itsellään, mutta luultavasti\n" " jos tarvitset tätä komentoa, et lähetä sitä kanavalla\n" " itsellään.\n" " " #: plugin.py:526 msgid "" "takes no arguments\n" "\n" " Identifies with NickServ using the current nick.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Tunnistautuu NickServille käyttämällä nykyistä nimimerkkiä.\n" " " #: plugin.py:535 msgid "I don't have a configured password for my current nick." msgstr "Minulla ei ole määritettyä salasanaa nimimerkilleni." #: plugin.py:538 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to do " "identify." msgstr "" "Sinun täytyy asettaa supybot.plugins.Services.NickServ ennen kuin pystyn " "tunnistautumaan." #: plugin.py:544 msgid "" "[]\n" "\n" " Ghosts the bot's given nick and takes it. If no nick is given,\n" " ghosts the bot's configured nick and takes it.\n" " " msgstr "" "[]\n" "\n" " Ghostaa botin annetun nimimerkin ja ottaa sen. Jos nimimerkkiä ei " "ole annettu,\n" " ghostaa botin määritetyn nimimerkin ja ottaa sen.\n" " " #: plugin.py:553 msgid "I cowardly refuse to ghost myself." msgstr "Minä pelkurimaisesti kieltäydyn ghostaamasta itseäni." #: plugin.py:558 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to ghost a " "nick." msgstr "" "Sinun täytyy asettaa supybot.plugins.Services.NickServ ennen kuin pystyn " "ghostaamaan nimimerkin." #: plugin.py:563 msgid "" "\n" "\n" " Sends the to NickServ. For example, to register to NickServ\n" " on Atheme, use: @nickserv REGISTER ." msgstr "" #: plugin.py:572 #, fuzzy msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to message " "NickServ" msgstr "" "Sinun täytyy asettaa supybot.plugins.Services.NickServ ennen kuin pystyn " "ghostaamaan nimimerkin." #: plugin.py:577 msgid "" "\n" "\n" " Sends the to ChanServ. For example, to register a channel\n" " on Atheme, use: @chanserv REGISTER <#channel>." msgstr "" #: plugin.py:586 #, fuzzy msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to message " "ChanServ" msgstr "" "Sinun täytyy asettaa supybot.plugins.Services.ChanServ ennen kuin pystyn " "lähettämään komennon %s." #: plugin.py:593 msgid "" " []\n" "\n" " Sets the NickServ password for to . If " "is\n" " not given, removes from the configured nicks.\n" " " msgstr "" " []\n" "\n" " Asettaa NickServin salasanan . Jos " " ei\n" " ole annettu, poistaa määritetyistä nimimerkeistä.\n" " " #: plugin.py:605 msgid "That nick was not configured with a password." msgstr "Tuota nimimerkkiä ei ole määritetty salasanan kanssa." #: plugin.py:618 msgid "" "takes no arguments\n" "\n" " Returns the nicks that this plugin is configured to identify and " "ghost\n" " with.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa nimimerkin, jolla tämä lisäosa on määritetty " "tunnistautumaan ja\n" " ghostaamaan.\n" " " #: plugin.py:628 msgid "I'm not currently configured for any nicks." msgstr "Minulle ei ole tällä hetkellä määritetty yhtään nimimerkkiä." #: plugin.py:635 msgid "Experimental IRC extensions are not enabled for this bot." msgstr "" #: plugin.py:641 msgid "This network does not support draft/account-registration." msgstr "" #: plugin.py:647 msgid "This network does not support labeled-response." msgstr "" #: plugin.py:653 msgid "This bot is already authenticated on the network." msgstr "" #: plugin.py:658 msgid "" "[] []\n" "\n" " Uses the experimental REGISTER command to create an account for the " "bot\n" " on the , using the and the if provided.\n" " Some networks may require the email.\n" " You may need to use the 'services verify' command afterward to " "confirm\n" " your email address." msgstr "" #: plugin.py:672 msgid "This network requires an email address to register." msgstr "" #: plugin.py:686 msgid "" "[] \n" "\n" " If the requires a verification code, you need to call " "this\n" " command with the code the server gave you to finish the\n" " registration." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/locales/fr.po0000644000175000017500000002603614535072470017764 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:40 msgid "" "Determines what password the bot will use with NickServ when identifying as " "%s." msgstr "" "Détermine quel mot de passe le bot utilisera pour s'identifier à NickServ " "lors de l'identification à %s." #: config.py:50 msgid "What is your registered nick?" msgstr "Quel est votre nick enregistré ?" #: config.py:51 msgid "What is your password for that nick?" msgstr "Quel est votre mot de passe pour ce nick ?" #: config.py:52 msgid "What is your ChanServ named?" msgstr "Comment est nommé ChanServ ?" #: config.py:53 msgid "What is your NickServ named?" msgstr "Comment est nommé NickServ ?" #: config.py:70 msgid "" "Determines what nicks the bot will use with\n" " services." msgstr "Détermine quels nicks le bot utilisera avec les services." #: config.py:77 msgid "" "Determines what networks this plugin\n" " will be disabled on." msgstr "Détermine sur quels réseaux ce plugin sera désactivé." #: config.py:77 msgid "QuakeNet" msgstr "QuakeNet" #: config.py:81 msgid "" "Determines whether the bot will not join any\n" " channels until it is identified. This may be useful, for instances, if\n" " you have a vhost that isn't set until you're identified, or if you're\n" " joining +r channels that won't allow you to join unless you identify." msgstr "" "Détermine si le bot peut rejoindre un ou des canal(aux) avant d'être " "identifié. Ce peut être utilise, par exemple si vous utilisez une vhost qui " "n'est activée qu'une fois que vous êtes identifié, ou encore si vous devez " "rejoindre des canaux en +r (=des canaux que l'on ne peut joindre sans être " "identifié)" #: config.py:86 #, fuzzy msgid "" "Determines how many seconds the bot will\n" " wait between successive GHOST attempts. Set this to 0 to disable GHOST." msgstr "" "Détermine le nombre de secondes entre deux tentatives successives de GHOST." #: config.py:89 msgid "" "Determines what nick the 'NickServ' service\n" " has." msgstr "Détermine quel nick NickServ a." #: config.py:93 msgid "" "Determines what nick the 'ChanServ' service\n" " has." msgstr "Détermine quel nick ChanServ a." #: config.py:96 msgid "" "Determines what password the bot will use with\n" " ChanServ." msgstr "Détermine quel mot de passe le bot utilisera avec ChanServ." #: config.py:99 msgid "" "Determines whether the bot will request to get\n" " opped by the ChanServ when it joins the channel." msgstr "" "Détermine si le bot demandera à être oppé par ChanServ lorsqu'il rejoint le " "canal." #: config.py:102 msgid "" "Determines whether the bot will request to get\n" " half-opped by the ChanServ when it joins the channel." msgstr "" "Détermine si le bot demandera à être oppé par NickServ lorsqu'il rejoint le " "canal." #: config.py:105 msgid "" "Determines whether the bot will request to get\n" " voiced by the ChanServ when it joins the channel." msgstr "" "Détermine si le bot demandera à être voicé par ChanServ lorsqu'il rejoint le " "canal." #: plugin.py:54 msgid "" "This plugin handles dealing with Services on networks that provide them.\n" " Basically, you should use the \"password\" command to tell the bot a " "nick to\n" " identify with and what password to use to identify with that nick. You " "can\n" " use the password command multiple times if your bot has multiple nicks\n" " registered. Also, be sure to configure the NickServ and ChanServ\n" " configuration variables to match the NickServ and ChanServ nicks on " "your\n" " network. Other commands such as identify, op, etc. should not be\n" " necessary if the bot is properly configured." msgstr "" "Ce plugin gère les services sur les réseaux qui en fournissent. En général, " "vous pouvez utiliser la commande 'password' pour dire au bot de prendre un " "nick et de s'identifier avec le mot de passe. Vous pouvez utiliser la " "commande 'password' autant de fois que le bot a de nicks enregistrés. Aussi, " "assurez-vous de configurer les variables NickServ et ChanServ pour " "correspondre à leurs nicks respectifs sur le réseau en question. D'autres " "commandes, comme 'identify', 'op', ... ne sont pas nécessaires quand le bot " "est configuré correctement." #: plugin.py:441 msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to send the " "%s command." msgstr "" "vous devez définir supybot.plugins.Services.ChanServ avant que je ne puisse " "envoyer la commande %s" #: plugin.py:447 msgid "" "[]\n" "\n" " Attempts to get opped by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" "Demande à être opé par ChanServ sur le . n'est nécessaire que " "si la commande n'est pas envoyée sur le canal lui-même." #: plugin.py:453 msgid "I'm already opped in %s." msgstr "Je suis déjà opé sur %s." #: plugin.py:460 msgid "" "[]\n" "\n" " Attempts to get voiced by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Demande à être voicé par ChanServ sur le . n'est nécessaire " "que si la commande n'est pas envoyée sur le canal lui-même." #: plugin.py:466 msgid "I'm already voiced in %s." msgstr "Je suis déjà voicé sur %s." #: plugin.py:483 msgid "" "[]\n" "\n" " Attempts to get unbanned by ChanServ in . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Cherche à être débanni par ChanServ sur le . n'est nécessaire " "que si la commande n'est pas envoyée sur le canal lui-même, mais, vous " "n'avez probablement pas besoin d'utiliser cette commande sur le canal en " "question." #: plugin.py:504 msgid "" "[]\n" "\n" " Attempts to get invited by ChanServ to . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" "Cherche à être invité par ChanServ sur le . n'est nécessaire " "que si la commande n'est pas envoyée sur le canal lui-même, mais, vous " "n'avez probablement pas besoin d'utiliser cette commande sur le canal en " "question." #: plugin.py:526 msgid "" "takes no arguments\n" "\n" " Identifies with NickServ using the current nick.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "S'identifie auprès de NickServ, en utilisant le nick actuel." #: plugin.py:535 msgid "I don't have a configured password for my current nick." msgstr "Je n'ai pas de mot de passe configuré pour mon nick actuel." #: plugin.py:538 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to do " "identify." msgstr "" "Vous devez définir supybot.plugins.Services.NickServ avant que je ne puisse " "m'identifier" #: plugin.py:544 msgid "" "[]\n" "\n" " Ghosts the bot's given nick and takes it. If no nick is given,\n" " ghosts the bot's configured nick and takes it.\n" " " msgstr "" "[]\n" "\n" "Ghost le nick donné du bot et le prend. Si aucun nick n'est donné, utilise " "celui configuré." #: plugin.py:553 msgid "I cowardly refuse to ghost myself." msgstr "Je suis trop couard pour me ghoster moi-même." #: plugin.py:558 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to ghost a " "nick." msgstr "" "Vous devez définir supybot.plugins.Services.NickServ avant que je ne puisse " "ghoster un nick." #: plugin.py:563 msgid "" "\n" "\n" " Sends the to NickServ. For example, to register to NickServ\n" " on Atheme, use: @nickserv REGISTER ." msgstr "" #: plugin.py:572 #, fuzzy msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to message " "NickServ" msgstr "" "Vous devez définir supybot.plugins.Services.NickServ avant que je ne puisse " "ghoster un nick." #: plugin.py:577 msgid "" "\n" "\n" " Sends the to ChanServ. For example, to register a channel\n" " on Atheme, use: @chanserv REGISTER <#channel>." msgstr "" #: plugin.py:586 #, fuzzy msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to message " "ChanServ" msgstr "" "vous devez définir supybot.plugins.Services.ChanServ avant que je ne puisse " "envoyer la commande %s" #: plugin.py:593 msgid "" " []\n" "\n" " Sets the NickServ password for to . If " "is\n" " not given, removes from the configured nicks.\n" " " msgstr "" " []\n" "\n" "Défini le NickServ pour le . Si le n'est " "pas donné, supprime de la liste des nis, configurés." #: plugin.py:605 msgid "That nick was not configured with a password." msgstr "Ce nick n'est pas configuré avec un mot de passe." #: plugin.py:618 msgid "" "takes no arguments\n" "\n" " Returns the nicks that this plugin is configured to identify and " "ghost\n" " with.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne les nicks pour lesquels ce plugin est configuré pour s'identifier " "et ghoster." #: plugin.py:628 msgid "I'm not currently configured for any nicks." msgstr "Je ne suis actuellement configuré pour aucun nick." #: plugin.py:635 msgid "Experimental IRC extensions are not enabled for this bot." msgstr "" #: plugin.py:641 msgid "This network does not support draft/account-registration." msgstr "" #: plugin.py:647 msgid "This network does not support labeled-response." msgstr "" #: plugin.py:653 msgid "This bot is already authenticated on the network." msgstr "" #: plugin.py:658 msgid "" "[] []\n" "\n" " Uses the experimental REGISTER command to create an account for the " "bot\n" " on the , using the and the if provided.\n" " Some networks may require the email.\n" " You may need to use the 'services verify' command afterward to " "confirm\n" " your email address." msgstr "" #: plugin.py:672 msgid "This network requires an email address to register." msgstr "" #: plugin.py:686 msgid "" "[] \n" "\n" " If the requires a verification code, you need to call " "this\n" " command with the code the server gave you to finish the\n" " registration." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/locales/it.po0000644000175000017500000002640514535072470017771 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-10 12:57+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:40 msgid "" "Determines what password the bot will use with NickServ when identifying as " "%s." msgstr "" "Determina quale password il bot userà con NickServ per identificarsi come %s." #: config.py:50 msgid "What is your registered nick?" msgstr "Qual è il tuo nick registrato?" #: config.py:51 msgid "What is your password for that nick?" msgstr "Qual è la password per questo nick?" #: config.py:52 msgid "What is your ChanServ named?" msgstr "Come si chiama ChanServ?" #: config.py:53 msgid "What is your NickServ named?" msgstr "Come si chiama NickServ?" #: config.py:70 msgid "" "Determines what nicks the bot will use with\n" " services." msgstr "Determina quali nick il bot userà con i servizi." #: config.py:77 msgid "" "Determines what networks this plugin\n" " will be disabled on." msgstr "Determina su quali reti sarà disattivato questo plugin." #: config.py:77 msgid "QuakeNet" msgstr "QuakeNet" #: config.py:81 msgid "" "Determines whether the bot will not join any\n" " channels until it is identified. This may be useful, for instances, if\n" " you have a vhost that isn't set until you're identified, or if you're\n" " joining +r channels that won't allow you to join unless you identify." msgstr "" "Determina se il bot non entrerà in alcun canale finché non è identificato.\n" " Può essere utile, ad esempio, se si utilizza un vhost che non viene\n" " impostato fino a che non ci si identifica oppure se si necessita di\n" " entrare in un canale con un mode +r (su Freenode, +j su Azzurra) che\n" " non permette il join a utenti non identificati." #: config.py:86 #, fuzzy msgid "" "Determines how many seconds the bot will\n" " wait between successive GHOST attempts. Set this to 0 to disable GHOST." msgstr "" "Determina quanti secondi il bot aspetterà tra un tentativo di GHOST e " "l'altro." #: config.py:89 msgid "" "Determines what nick the 'NickServ' service\n" " has." msgstr "Determina quale nick ha il servizio \"NickServ\"." #: config.py:93 msgid "" "Determines what nick the 'ChanServ' service\n" " has." msgstr "Determina quale nick ha il servizio \"ChanServ\"." #: config.py:96 msgid "" "Determines what password the bot will use with\n" " ChanServ." msgstr "Determina quale password il bot userà con ChanServ." #: config.py:99 msgid "" "Determines whether the bot will request to get\n" " opped by the ChanServ when it joins the channel." msgstr "" "Determina se il bot richiederà lo stato di op da ChanServ quando entra nel " "canale." #: config.py:102 msgid "" "Determines whether the bot will request to get\n" " half-opped by the ChanServ when it joins the channel." msgstr "" "Determina se il bot richiederà lo stato di halfop da ChanServ quando entra " "nel canale." #: config.py:105 msgid "" "Determines whether the bot will request to get\n" " voiced by the ChanServ when it joins the channel." msgstr "" "Determina se il bot richiederà il voice da ChanServ quando entra nel canale." #: plugin.py:54 msgid "" "This plugin handles dealing with Services on networks that provide them.\n" " Basically, you should use the \"password\" command to tell the bot a " "nick to\n" " identify with and what password to use to identify with that nick. You " "can\n" " use the password command multiple times if your bot has multiple nicks\n" " registered. Also, be sure to configure the NickServ and ChanServ\n" " configuration variables to match the NickServ and ChanServ nicks on " "your\n" " network. Other commands such as identify, op, etc. should not be\n" " necessary if the bot is properly configured." msgstr "" "Questo plugin gestisce i servizi su reti che li forniscono. " "Fondamentalmente\n" " va usato il comando \"password\" per fornire al bot un nick con il " "quale\n" " identificarsi e quale password usare; se il bot ha più nick registrati, " "è\n" " possibile usare tale comando più volte. Assicurati di impostare le " "variabili\n" " di configurazione NickServ e ChanServ in modo che corrispondano ai " "rispettivi\n" " nick sulla rete in questione. Se il bot è ben configurato, gli altri " "comandi\n" " come \"identify\", \"op\", ecc. non dovrebbero essere necessari." #: plugin.py:441 msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to send the " "%s command." msgstr "" "È necessario impostare la variabile supybot.plugins.Services.ChanServ " "affinché possa inviare il comando %s." #: plugin.py:447 msgid "" "[]\n" "\n" " Attempts to get opped by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Tenta di ricevere lo stato di op da ChanServ in . \n" " è necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:453 msgid "I'm already opped in %s." msgstr "Sono già op in %s." #: plugin.py:460 msgid "" "[]\n" "\n" " Attempts to get voiced by ChanServ in . is only\n" " necessary if the message isn't sent in the channel itself.\n" " " msgstr "" "[]\n" "\n" " Tenta di ricevere il voice da ChanServ in . è\n" " necessario solo se il messaggio non viene inviato nel canale " "stesso.\n" " " #: plugin.py:466 msgid "I'm already voiced in %s." msgstr "Ho già il voice in %s." #: plugin.py:483 msgid "" "[]\n" "\n" " Attempts to get unbanned by ChanServ in . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Tenta di farsi rimuovere il ban da ChanServ in . è\n" " necessario solo se il messaggio non viene inviato nel canale stesso, " "ma è\n" " probabile che se serve questo comando non lo si stia inviando in " "canale.\n" " " #: plugin.py:504 msgid "" "[]\n" "\n" " Attempts to get invited by ChanServ to . is " "only\n" " necessary if the message isn't sent in the channel itself, but " "chances\n" " are, if you need this command, you're not sending it in the channel\n" " itself.\n" " " msgstr "" "[]\n" "\n" " Tenta di essere invitato da ChanServ in . è " "necessario\n" " solo se il messaggio non viene inviato nel canale stesso, ma è " "probabile\n" " che se serve questo comando non lo si stia inviando in canale.\n" " " #: plugin.py:526 msgid "" "takes no arguments\n" "\n" " Identifies with NickServ using the current nick.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Si identifica con NickServ utilizzando l'attuale nick in uso.\n" " " #: plugin.py:535 msgid "I don't have a configured password for my current nick." msgstr "Non ho una password configurata per il mio attuale nick." #: plugin.py:538 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to do " "identify." msgstr "" "È necessario impostare la variabile supybot.plugins.Services.NickServ " "affinché possa identificarmi." #: plugin.py:544 msgid "" "[]\n" "\n" " Ghosts the bot's given nick and takes it. If no nick is given,\n" " ghosts the bot's configured nick and takes it.\n" " " msgstr "" "[]\n" "\n" " Termina una connessione \"fantasma\" (ghost) e riprende il nick del " "bot.\n" " Se non è specificato alcun nick, riprende quello configurato.\n" " " #: plugin.py:553 msgid "I cowardly refuse to ghost myself." msgstr "" "Codardamente mi rifiuto di terminare una connessione \"fantasma\" (ghost)." #: plugin.py:558 msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to ghost a " "nick." msgstr "" "È necessario impostare la variabile supybot.plugins.Services.NickServ " "affinché possa riprendermi il nick (ghost)." #: plugin.py:563 msgid "" "\n" "\n" " Sends the to NickServ. For example, to register to NickServ\n" " on Atheme, use: @nickserv REGISTER ." msgstr "" #: plugin.py:572 #, fuzzy msgid "" "You must set supybot.plugins.Services.NickServ before I'm able to message " "NickServ" msgstr "" "È necessario impostare la variabile supybot.plugins.Services.NickServ " "affinché possa riprendermi il nick (ghost)." #: plugin.py:577 msgid "" "\n" "\n" " Sends the to ChanServ. For example, to register a channel\n" " on Atheme, use: @chanserv REGISTER <#channel>." msgstr "" #: plugin.py:586 #, fuzzy msgid "" "You must set supybot.plugins.Services.ChanServ before I'm able to message " "ChanServ" msgstr "" "È necessario impostare la variabile supybot.plugins.Services.ChanServ " "affinché possa inviare il comando %s." #: plugin.py:593 msgid "" " []\n" "\n" " Sets the NickServ password for to . If " "is\n" " not given, removes from the configured nicks.\n" " " msgstr "" " []\n" "\n" " Imposta la password di NickServ per . Se \n" " non è specificata, rimuove dai nick configurati.\n" " " #: plugin.py:605 msgid "That nick was not configured with a password." msgstr "Questo nick non è stato configurato con una password." #: plugin.py:618 msgid "" "takes no arguments\n" "\n" " Returns the nicks that this plugin is configured to identify and " "ghost\n" " with.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Restituisce i nick per i quali questo plugin è configurato per " "l'identificazione e il ghost.\n" " " #: plugin.py:628 msgid "I'm not currently configured for any nicks." msgstr "Al momento non sono configurato per alcun nick." #: plugin.py:635 msgid "Experimental IRC extensions are not enabled for this bot." msgstr "" #: plugin.py:641 msgid "This network does not support draft/account-registration." msgstr "" #: plugin.py:647 msgid "This network does not support labeled-response." msgstr "" #: plugin.py:653 msgid "This bot is already authenticated on the network." msgstr "" #: plugin.py:658 msgid "" "[] []\n" "\n" " Uses the experimental REGISTER command to create an account for the " "bot\n" " on the , using the and the if provided.\n" " Some networks may require the email.\n" " You may need to use the 'services verify' command afterward to " "confirm\n" " your email address." msgstr "" #: plugin.py:672 msgid "This network requires an email address to register." msgstr "" #: plugin.py:686 msgid "" "[] \n" "\n" " If the requires a verification code, you need to call " "this\n" " command with the code the server gave you to finish the\n" " registration." msgstr "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Services/plugin.py0000644000175000017500000010471114535072477017247 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import time from . import config import supybot.conf as conf import supybot.utils as utils from supybot.commands import * import supybot.irclib as irclib import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Services') class State: def __init__(self): self.channels = [] self.sentGhost = None self.identified = False self.waitingJoins = [] class Services(callbacks.Plugin): """This plugin handles dealing with Services on networks that provide them. Basically, you should use the "password" command to tell the bot a nick to identify with and what password to use to identify with that nick. You can use the password command multiple times if your bot has multiple nicks registered. Also, be sure to configure the NickServ and ChanServ configuration variables to match the NickServ and ChanServ nicks on your network. Other commands such as identify, op, etc. should not be necessary if the bot is properly configured.""" # 10 minutes ought to be more than enough for the server to reply. # Holds the (irc, nick) of the caller so we can notify them when the # command either succeeds or fails. _register = utils.structures.ExpiringDict(600) def __init__(self, irc): self.__parent = super(Services, self) self.__parent.__init__(irc) network = irc.network if irc else None for nick in self.registryValue('nicks', network=network): config.registerNick(nick) self.reset() def reset(self): self.state = {} def _getState(self, irc): return self.state.setdefault(irc.network, State()) def disabled(self, irc): disabled = self.registryValue('disabledNetworks') if irc.network in disabled or \ irc.state.supported.get('NETWORK', '') in disabled: return True return False def outFilter(self, irc, msg): state = self._getState(irc) if msg.command == 'JOIN' and not self.disabled(irc): if not state.identified: if self.registryValue('noJoinsUntilIdentified', network=irc.network): self.log.info('Holding JOIN to %s @ %s until identified.', msg.channel, irc.network) state.waitingJoins.append(msg) return None return msg def _getNick(self, network): network_nick = conf.supybot.networks.get(network).nick() if network_nick == '': return conf.supybot.nick() else: return network_nick def _getNickServPassword(self, nick, network): # This should later be nick-specific. assert nick in self.registryValue('nicks', network=network) return self.registryValue('NickServ.password.%s' % nick, network=network) def _setNickServPassword(self, nick, password, network): # This also should be nick-specific. assert nick in self.registryValue('nicks', network=network) self.setRegistryValue('NickServ.password.%s' % nick, password, network=network) def _doIdentify(self, irc, nick=None): if self.disabled(irc): return if nick is None: nick = self._getNick(irc.network) if nick not in self.registryValue('nicks', network=irc.network): return nickserv = self.registryValue('NickServ', network=irc.network) password = self._getNickServPassword(nick, irc.network) if not nickserv: self.log.warning('Tried to identify without a NickServ set.') return if not password: self.log.warning('Tried to identify without a password set.') return assert ircutils.strEqual(irc.nick, nick), \ 'Identifying with not normal nick.' self.log.info('Sending identify (current nick: %s)', irc.nick) identify = 'IDENTIFY %s' % password # It's important that this next statement is irc.sendMsg, not # irc.queueMsg. We want this message to get through before any # JOIN messages also being sent on 376. irc.sendMsg(ircmsgs.privmsg(nickserv, identify)) def _doGhost(self, irc, nick=None): if self.disabled(irc): return state = self._getState(irc) if nick is None: nick = self._getNick(irc.network) if nick not in self.registryValue('nicks', network=irc.network): return nickserv = self.registryValue('NickServ', network=irc.network) password = self._getNickServPassword(nick, irc.network) ghostDelay = self.registryValue('ghostDelay', network=irc.network) if not ghostDelay: return if not nickserv: self.log.warning('Tried to ghost without a NickServ set.') return if not password: self.log.warning('Tried to ghost without a password set.') return if state.sentGhost and time.time() < (state.sentGhost + ghostDelay): self.log.warning('Refusing to send GHOST more than once every ' '%s seconds.' % ghostDelay) else: self.log.info('Sending ghost (current nick: %s; ghosting: %s)', irc.nick, nick) ghostCommand = self.registryValue('ghostCommand', network=irc.network) ghost = '%s %s %s' % (ghostCommand, nick, password) # Ditto about the sendMsg (see _doIdentify). irc.sendMsg(ircmsgs.privmsg(nickserv, ghost)) state.sentGhost = time.time() def __call__(self, irc, msg): self.__parent.__call__(irc, msg) if self.disabled(irc): return state = self._getState(irc) nick = self._getNick(irc.network) if nick not in self.registryValue('nicks', network=irc.network): return nickserv = self.registryValue('NickServ', network=irc.network) password = self._getNickServPassword(nick, irc.network) ghostDelay = self.registryValue('ghostDelay', network=irc.network) if not ghostDelay: return if nick and nickserv and password and \ not ircutils.strEqual(nick, irc.nick): if irc.afterConnect and (state.sentGhost is None or (state.sentGhost + ghostDelay) < time.time()): if nick in irc.state.nicksToHostmasks: self._doGhost(irc) else: irc.sendMsg(ircmsgs.nick(nick)) # 433 is handled elsewhere. def do001(self, irc, msg): # New connection, make sure sentGhost is None. state = self._getState(irc) state.sentGhost = None def do376(self, irc, msg): if self.disabled(irc): return nick = self._getNick(irc.network) if nick not in self.registryValue('nicks', network=irc.network): return nickserv = self.registryValue('NickServ', network=irc.network) if not nickserv: self.log.warning('NickServ is unset, cannot identify.') return password = self._getNickServPassword(nick, irc.network) if not password: self.log.warning('Password for %s is unset, cannot identify.',nick) return if not nick: self.log.warning('Cannot identify without a nick being set. ' 'Set supybot.plugins.Services.nick.') return if ircutils.strEqual(irc.nick, nick): self._doIdentify(irc) else: self._doGhost(irc) do422 = do377 = do376 def do433(self, irc, msg): if self.disabled(irc): return nick = self._getNick(irc.network) if nick not in self.registryValue('nicks', network=irc.network): return if nick and irc.afterConnect: password = self._getNickServPassword(nick, irc.network) if not password: return self._doGhost(irc) def do515(self, irc, msg): # Can't join this channel, it's +r (we must be identified). state = self._getState(irc) state.channels.append(msg.args[1]) def doNick(self, irc, msg): nick = self._getNick(irc.network) if ircutils.strEqual(msg.args[0], irc.nick) and \ ircutils.strEqual(irc.nick, nick): self._doIdentify(irc) elif ircutils.strEqual(msg.nick, nick): irc.sendMsg(ircmsgs.nick(nick)) def _ghosted(self, irc, s): nick = self._getNick(irc.network) lowered = s.lower() return bool('killed' in lowered and (nick in s or 'ghost' in lowered)) def doNotice(self, irc, msg): if irc.afterConnect: nickserv = self.registryValue('NickServ', network=irc.network) chanserv = self.registryValue('ChanServ', network=irc.network) if nickserv and ircutils.strEqual(msg.nick, nickserv): self.doNickservNotice(irc, msg) elif chanserv and ircutils.strEqual(msg.nick, chanserv): self.doChanservNotice(irc, msg) _chanRe = re.compile('\x02(#.*?)\x02') def doChanservNotice(self, irc, msg): if self.disabled(irc): return s = msg.args[1].lower() channel = None m = self._chanRe.search(s) networkGroup = conf.supybot.networks.get(irc.network) on = 'on %s' % irc.network if m is not None: channel = m.group(1) if 'all bans' in s or 'unbanned from' in s or \ ('unbanned %s' % irc.nick.lower()) in \ ircutils.stripFormatting(s): # All bans removed (old freenode?) # You have been unbanned from (oftc, anope) # "Unbanned \x02someuser\x02 from \x02#channel\x02 (\x02N\x02 # ban(s) removed)" (atheme 7.x) irc.sendMsg(networkGroup.channels.join(channel)) elif 'isn\'t registered' in s: self.log.warning('Received "%s isn\'t registered" from ChanServ %s', channel, on) elif 'this channel has been registered' in s: self.log.debug('Got "Registered channel" from ChanServ %s.', on) elif 'already opped' in s: # This shouldn't happen, Services.op should refuse to run if # we already have ops. self.log.debug('Got "Already opped" from ChanServ %s.', on) elif 'access level' in s and 'is required' in s: self.log.warning('Got "Access level required" from ChanServ %s.', on) elif 'inviting' in s: self.log.debug('Got "Inviting to channel" from ChanServ %s.', on) elif s.startswith('['): chanTypes = irc.state.supported['CHANTYPES'] if re.match(r'^\[[%s]' % re.escape(chanTypes), s): self.log.debug('Got entrymsg from ChanServ %s.', on) elif irc.isChannel(msg.args[0]): # Atheme uses channel-wide notices for alerting channel access # changes if the FANTASY or VERBOSE setting is on; we can suppress # these 'unexpected notice' warnings since they're not really # important. pass else: self.log.warning('Got unexpected notice from ChanServ %s: %r.', on, msg) def doNickservNotice(self, irc, msg): if self.disabled(irc): return state = self._getState(irc) nick = self._getNick(irc.network) s = ircutils.stripFormatting(msg.args[1].lower()) on = 'on %s' % irc.network networkGroup = conf.supybot.networks.get(irc.network) if 'incorrect' in s or 'denied' in s: log = 'Received "Password Incorrect" from NickServ %s. ' \ 'Resetting password to empty.' % on self.log.warning(log) state.sentGhost = time.time() self._setNickServPassword(nick, '', irc.network) elif self._ghosted(irc, s): self.log.info('Received "GHOST succeeded" from NickServ %s.', on) state.sentGhost = None state.identified = False irc.queueMsg(ircmsgs.nick(nick)) elif 'is not registered' in s: self.log.info('Received "Nick not registered" from NickServ %s.', on) elif 'currently' in s and 'isn\'t' in s or 'is not' in s: # The nick isn't online, let's change our nick to it. state.sentGhost = None irc.queueMsg(ircmsgs.nick(nick)) elif ('owned by someone else' in s) or \ ('nickname is registered and protected' in s) or \ ('nick belongs to another user' in s): # freenode, arstechnica, chatjunkies # oftc, zirc.org # sorcery self.log.info('Received "Registered nick" from NickServ %s.', on) elif '/msg' in s and 'id' in s and 'password' in s: # Usage info for identify command; ignore. self.log.debug('Got usage info for identify command %s.', on) elif ('please choose a different nick' in s): # oftc, part 3 # This is a catch-all for redundant messages from nickserv. pass elif ('now recognized' in s) or \ ('already identified' in s) or \ ('already logged in' in s) or \ ('successfully identified' in s) or \ ('password accepted' in s) or \ ('now identified' in s): # freenode, oftc, arstechnica, zirc, .... # sorcery self.log.info('Received "Password accepted" from NickServ %s.', on) state.identified = True for channel in irc.state.channels.keys(): self.checkPrivileges(irc, channel) for channel in state.channels: irc.queueMsg(networkGroup.channels.join(channel)) waitingJoins = state.waitingJoins state.waitingJoins = [] for join in waitingJoins: irc.sendMsg(join) elif 'not yet authenticated' in s: # zirc.org has this, it requires an auth code. email = s.split()[-1] self.log.warning('Received "Nick not yet authenticated" from ' 'NickServ %s. Check email at %s and send the ' 'auth command to NickServ.', on, email) else: self.log.info('Received notice from NickServ %s: %q.', on, ircutils.stripFormatting(msg.args[1])) def do903(self, irc, msg): # RPL_SASLSUCCESS if self.disabled(irc): return state = self._getState(irc) state.identified = True for channel in irc.state.channels.keys(): self.checkPrivileges(irc, channel) if irc.state.fsm in [irclib.IrcStateFsm.States.CONNECTED, irclib.IrcStateFsm.States.CONNECTED_SASL]: for channel in state.channels: irc.queueMsg(networkGroup.channels.join(channel)) waitingJoins = state.waitingJoins state.waitingJoins = [] for join in waitingJoins: irc.sendMsg(join) do907 = do903 # ERR_SASLALREADY, just to be sure we didn't miss it def do901(self, irc, msg): # RPL_LOGGEDOUT if self.disabled(irc): return state = self._getState(irc) state.identified = False def checkPrivileges(self, irc, channel): if self.disabled(irc): return chanserv = self.registryValue('ChanServ', network=irc.network) on = 'on %s' % irc.network if chanserv and self.registryValue('ChanServ.op', channel, irc.network): if irc.nick not in irc.state.channels[channel].ops: self.log.info('Requesting op from %s in %s %s.', chanserv, channel, on) irc.sendMsg(ircmsgs.privmsg(chanserv, 'op %s' % channel)) if chanserv and self.registryValue('ChanServ.halfop', channel, irc.network): if irc.nick not in irc.state.channels[channel].halfops: self.log.info('Requesting halfop from %s in %s %s.', chanserv, channel, on) irc.sendMsg(ircmsgs.privmsg(chanserv, 'halfop %s' % channel)) if chanserv and self.registryValue('ChanServ.voice', channel, irc.network): if irc.nick not in irc.state.channels[channel].voices: self.log.info('Requesting voice from %s in %s %s.', chanserv, channel, on) irc.sendMsg(ircmsgs.privmsg(chanserv, 'voice %s' % channel)) def doMode(self, irc, msg): if self.disabled(irc): return chanserv = self.registryValue('ChanServ', network=irc.network) on = 'on %s' % irc.network if ircutils.strEqual(msg.nick, chanserv): channel = msg.args[0] if len(msg.args) == 3: if ircutils.strEqual(msg.args[2], irc.nick): mode = msg.args[1] info = self.log.info if mode == '+o': info('Received op from ChanServ in %s %s.', channel, on) elif mode == '+h': info('Received halfop from ChanServ in %s %s.', channel, on) elif mode == '+v': info('Received voice from ChanServ in %s %s.', channel, on) def do366(self, irc, msg): # End of /NAMES list; finished joining a channel state = self._getState(irc) if state.identified: channel = msg.args[1] # nick is msg.args[0]. self.checkPrivileges(irc, channel) def callCommand(self, command, irc, msg, *args, **kwargs): if self.disabled(irc): irc.error('Services plugin is disabled on this network', Raise=True) self.__parent.callCommand(command, irc, msg, *args, **kwargs) def _chanservCommand(self, irc, channel, command, log=False): chanserv = self.registryValue('ChanServ', network=irc.network) if chanserv: msg = ircmsgs.privmsg(chanserv, ' '.join([command, channel])) irc.sendMsg(msg) else: if log: self.log.warning('Unable to send %s command to ChanServ, ' 'you must set ' 'supybot.plugins.Services.ChanServ before ' 'I can send commands to ChanServ.', command) else: irc.error(_('You must set supybot.plugins.Services.ChanServ ' 'before I\'m able to send the %s command.') % command, Raise=True) @internationalizeDocstring def op(self, irc, msg, args, channel): """[] Attempts to get opped by ChanServ in . is only necessary if the message isn't sent in the channel itself. """ if irc.nick in irc.state.channels[channel].ops: irc.error(format(_('I\'m already opped in %s.'), channel)) else: self._chanservCommand(irc, channel, 'op') op = wrap(op, [('checkChannelCapability', 'op'), 'inChannel']) @internationalizeDocstring def voice(self, irc, msg, args, channel): """[] Attempts to get voiced by ChanServ in . is only necessary if the message isn't sent in the channel itself. """ if irc.nick in irc.state.channels[channel].voices: irc.error(format(_('I\'m already voiced in %s.'), channel)) else: self._chanservCommand(irc, channel, 'voice') voice = wrap(voice, [('checkChannelCapability', 'op'), 'inChannel']) def do474(self, irc, msg): if self.disabled(irc): return channel = msg.args[1] on = 'on %s' % irc.network self.log.info('Banned from %s, attempting ChanServ unban %s.', channel, on) self._chanservCommand(irc, channel, 'unban', log=True) # Success log in doChanservNotice. @internationalizeDocstring def unban(self, irc, msg, args, channel): """[] Attempts to get unbanned by ChanServ in . is only necessary if the message isn't sent in the channel itself, but chances are, if you need this command, you're not sending it in the channel itself. """ self._chanservCommand(irc, channel, 'unban') irc.replySuccess() unban = wrap(unban, [('checkChannelCapability', 'op')]) def do473(self, irc, msg): if self.disabled(irc): return channel = msg.args[1] on = 'on %s' % irc.network self.log.info('%s is +i, attempting ChanServ invite %s.', channel, on) self._chanservCommand(irc, channel, 'invite', log=True) @internationalizeDocstring def invite(self, irc, msg, args, channel): """[] Attempts to get invited by ChanServ to . is only necessary if the message isn't sent in the channel itself, but chances are, if you need this command, you're not sending it in the channel itself. """ self._chanservCommand(irc, channel, 'invite') irc.replySuccess() invite = wrap(invite, [('checkChannelCapability', 'op'), 'inChannel']) def doInvite(self, irc, msg): if ircutils.strEqual( msg.nick, self.registryValue('ChanServ', network=irc.network)): channel = msg.args[1] on = 'on %s' % irc.network networkGroup = conf.supybot.networks.get(irc.network) self.log.info('Joining %s, invited by ChanServ %s.', channel, on) irc.queueMsg(networkGroup.channels.join(channel)) @internationalizeDocstring def identify(self, irc, msg, args): """takes no arguments Identifies with NickServ using the current nick. """ if self.registryValue('NickServ', network=irc.network): if irc.nick in self.registryValue('nicks', network=irc.network): self._doIdentify(irc, irc.nick) irc.replySuccess() else: irc.error(_('I don\'t have a configured password for ' 'my current nick.')) else: irc.error(_('You must set supybot.plugins.Services.NickServ before ' 'I\'m able to do identify.')) identify = wrap(identify, [('checkCapability', 'admin')]) @internationalizeDocstring def ghost(self, irc, msg, args, nick): """[] Ghosts the bot's given nick and takes it. If no nick is given, ghosts the bot's configured nick and takes it. """ if self.registryValue('NickServ', network=irc.network): if not nick: nick = self._getNick(irc.network) if ircutils.strEqual(nick, irc.nick): irc.error(_('I cowardly refuse to ghost myself.')) else: self._doGhost(irc, nick=nick) irc.replySuccess() else: irc.error(_('You must set supybot.plugins.Services.NickServ before ' 'I\'m able to ghost a nick.')) ghost = wrap(ghost, [('checkCapability', 'admin'), additional('nick')]) def nickserv(self, irc, msg, args, text): """ Sends the to NickServ. For example, to register to NickServ on Atheme, use: @nickserv REGISTER .""" nickserv = self.registryValue('NickServ', network=irc.network) if nickserv: irc.replySuccess() irc.queueMsg(ircmsgs.privmsg(nickserv, text)) else: irc.error(_('You must set supybot.plugins.Services.NickServ before ' 'I\'m able to message NickServ')) nickserv = wrap(nickserv, ['owner', 'text']) def chanserv(self, irc, msg, args, text): """ Sends the to ChanServ. For example, to register a channel on Atheme, use: @chanserv REGISTER <#channel>.""" chanserv = self.registryValue('ChanServ', network=irc.network) if chanserv: irc.replySuccess() irc.queueMsg(ircmsgs.privmsg(chanserv, text)) else: irc.error(_('You must set supybot.plugins.Services.ChanServ before ' 'I\'m able to message ChanServ')) chanserv = wrap(chanserv, ['owner', 'text']) @internationalizeDocstring def password(self, irc, msg, args, nick, password): """ [] Sets the NickServ password for to . If is not given, removes from the configured nicks. """ if not password: try: v = self.registryValue('nicks', network=irc.network).copy() v.remove(nick) self.setRegistryValue('nicks', value=v, network=irc.network) irc.replySuccess() except KeyError: irc.error(_('That nick was not configured with a password.')) return else: v = self.registryValue('nicks', network=irc.network).copy() v.add(nick) self.setRegistryValue('nicks', value=v, network=irc.network) config.registerNick(nick, password) irc.replySuccess() password = wrap(password, [('checkCapability', 'admin'), 'private', 'nick', 'text']) @internationalizeDocstring def nicks(self, irc, msg, args): """takes no arguments Returns the nicks that this plugin is configured to identify and ghost with. """ L = list(self.registryValue('nicks', network=irc.network)) if L: utils.sortBy(ircutils.toLower, L) irc.reply(format('%L', L)) else: irc.reply(_('I\'m not currently configured for any nicks.')) nicks = wrap(nicks, [('checkCapability', 'admin')]) def _checkCanRegister(self, irc, otherIrc): if not conf.supybot.protocols.irc.experimentalExtensions(): irc.error( _("Experimental IRC extensions are not enabled for this bot."), Raise=True ) if "draft/account-registration" not in otherIrc.state.capabilities_ls: irc.error( _("This network does not support draft/account-registration."), Raise=True ) if "labeled-response" not in otherIrc.state.capabilities_ls: irc.error( _("This network does not support labeled-response."), Raise=True ) if otherIrc.sasl_authenticated: irc.error( _("This bot is already authenticated on the network."), Raise=True ) def register(self, irc, msg, args, otherIrc, password, email): """[] [] Uses the experimental REGISTER command to create an account for the bot on the , using the and the if provided. Some networks may require the email. You may need to use the 'services verify' command afterward to confirm your email address.""" # Using this early draft specification: # https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495 self._checkCanRegister(irc, otherIrc) cap_values = (otherIrc.state.capabilities_ls["draft/account-registration"] or "").split(",") if "email-required" in cap_values and email is None: irc.error( _("This network requires an email address to register."), Raise=True ) label = ircutils.makeLabel() self._register[label] = (irc, msg.nick) otherIrc.queueMsg(ircmsgs.IrcMsg( server_tags={"label": label}, command="REGISTER", args=["*", email or "*", password], )) register = wrap(register, ["owner", "private", "networkIrc", "something", optional("email")]) def verify(self, irc, msg, args, otherIrc, account, code): """[] If the requires a verification code, you need to call this command with the code the server gave you to finish the registration.""" self._checkCanRegister(irc, otherIrc) label = ircutils.makeLabel() self._register[label] = (irc, msg.nick) otherIrc.queueMsg(ircmsgs.IrcMsg( server_tags={"label": label}, command="VERIFY", args=[account, code] )) verify = wrap(verify, [ "owner", "private", "networkIrc", "somethingWithoutSpaces", "something" ]) def _replyToRegister(self, irc, msg, command, reply): if not conf.supybot.protocols.irc.experimentalExtensions(): self.log.warning( "Got unexpected '%s' on %s, this should not " "happen unless supybot.protocols.irc.experimentalExtensions " "is enabled", command, irc.network ) return label = msg.server_tags.get("label") if not label and "batch" in msg.server_tags: for batch in irc.state.getParentBatches(msg): label = batch.messages[0].server_tags.get("label") if label: break if not label: self.log.error( "Got '%s' on %s, but it is missing a label. " "This is a bug, please report it.", command, irc.network ) return if label not in self._register: self.log.warning( "Got '%s' on %s, but I don't remember using " "REGISTER/VERIFY. " "This may be caused by high latency from the server.", command, irc.network ) return (initialIrc, initialNick) = self._register[label] initialIrc.reply(reply) def doFailRegister(self, irc, msg): self._replyToRegister( irc, msg, "FAIL %s" % msg.args[0], format( "Failed to register on %s; the server said: %s (%s)", irc.network, msg.args[1], msg.args[-1] ) ) doFailVerify = doFailRegister # This should not be called, but you never know. def doWarnRegister(self, irc, msg): self._replyToRegister( irc, msg, "WARN %s" % msg.args[0], format( "Registration warning from %s: %s (%s)", irc.network, msg.args[1], msg.args[-1] ) ) doWarnVerify = doWarnRegister # This should not be called, but you never know. def doNoteRegister(self, irc, msg): self._replyToRegister( irc, msg, "NOTE %s" % msg.args[0], format( "Registration note from %s: %s (%s)", irc.network, msg.args[1], msg.args[-1] ) ) doNoteVerify = doNoteRegister def doRegister(self, irc, msg): (subcommand, account, message) = msg.args if subcommand == "SUCCESS": self._replyToRegister( irc, msg, "REGISTER SUCCESS", format( "Registration of account %s on %s succeeded: %s", account, irc.network, message ) ) elif subcommand == "VERIFICATION_REQUIRED": self._replyToRegister( irc, msg, "REGISTER VERIFICATION_REQUIRED", format( "Registration of %s on %s requires verification to complete: %s", account, irc.network, message ) ) else: self._replyToRegister( irc, msg, "REGISTER %s" % subcommand, format( "Unknown reply while registering %s on %s: %s %s", account, irc.network, subcommand, message ) ) def doVerify(self, irc, msg): (subcommand, account, message) = msg.args if subcommand == "SUCCESS": self._replyToRegister( irc, msg, "VERIFY SUCCESS", format( "Verification of account %s on %s succeeded: %s", account, irc.network, message ) ) else: self._replyToRegister( irc, msg, "VERIFY %s" % subcommand, format( "Unknown reply while registering %s on %s: %s %s", account, irc.network, subcommand, message ) ) Class = Services # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Services/test.py0000644000175000017500000003414414535072470016723 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import supybot.conf as conf from supybot.ircmsgs import IrcMsg from copy import copy class ServicesTestCase(PluginTestCase): plugins = ('Services', 'Config') config = { 'plugins.Services.NickServ': 'NickServ', 'plugins.Services.ChanServ': 'ChanServ', } def testPasswordAndIdentify(self): try: self.assertNotError('services password foo bar') self.assertError('services identify') # Don't have a password. finally: self.assertNotError('services password foo ""') try: self.assertNotError('services password %s baz' % self.nick) m = self.assertNotError('services identify') self.assertEqual(m.args[0], 'NickServ') self.assertEqual(m.args[1].lower(), 'identify baz') self.assertNotError('services password %s biff' % self.nick) m = self.assertNotError('services identify') self.assertEqual(m.args[0], 'NickServ') self.assertEqual(m.args[1].lower(), 'identify biff') finally: self.assertNotError('services password %s ""' % self.nick) def testPasswordConfig(self): self.assertNotError('config plugins.Services.nicks ""') self.assertNotError('config network plugins.Services.nicks ""') try: self.assertNotError('services password %s bar' % self.nick) self.assertResponse( 'config plugins.Services.nicks', 'Global: ; test: %s' % self.nick) self.assertResponse( 'config plugins.Services.nickserv.password.%s' % self.nick, 'Global: bar; test: bar') self.assertNotError( 'config network plugins.Services.nickserv.password.%s bar2' % self.nick) self.assertResponse( 'config plugins.Services.nickserv.password.%s' % self.nick, 'Global: bar; test: bar2') self.assertResponse( 'config plugins.Services.nickserv.password.%s' % self.nick, 'Global: bar; test: bar2') m = self.assertNotError('services identify') self.assertEqual(m.args[0], 'NickServ') self.assertEqual(m.args[1].lower(), 'identify bar2') finally: self.assertNotError('services password %s ""' % self.nick) def testNickserv(self): self.assertNotError('nickserv foo bar') m = self.irc.takeMsg() self.assertEqual(m.command, 'PRIVMSG', m) self.assertEqual(m.args, ('NickServ', 'foo bar'), m) def testChanserv(self): self.assertNotError('chanserv foo bar') m = self.irc.takeMsg() self.assertEqual(m.command, 'PRIVMSG', m) self.assertEqual(m.args, ('ChanServ', 'foo bar'), m) def testRegisterNoExperimentalExtensions(self): self.assertRegexp( "register p4ssw0rd", "error: Experimental IRC extensions") self.irc.feedMsg(IrcMsg( command="FAIL", args=["REGISTER", "BLAH", "message"])) self.assertIsNone(self.irc.takeMsg()) self.irc.feedMsg(IrcMsg( command="REGISTER", args=["SUCCESS", "account", "msg"])) self.assertIsNone(self.irc.takeMsg()) self.irc.feedMsg(IrcMsg( command="REGISTER", args=["VERIFICATION_REQUIRED", "account", "msg"])) self.assertIsNone(self.irc.takeMsg()) class JoinsBeforeIdentifiedTestCase(PluginTestCase): plugins = ('Services',) config = { 'plugins.Services.noJoinsUntilIdentified': False, } def testSingleNetwork(self): queuedJoin = ircmsgs.join('#test', prefix=self.prefix) self.irc.queueMsg(queuedJoin) self.assertEqual(self.irc.takeMsg(), queuedJoin, 'Join request did not go through.') class NoJoinsUntilIdentifiedTestCase(PluginTestCase): plugins = ('Services',) config = { 'plugins.Services.noJoinsUntilIdentified': True, } def _identify(self, irc): irc.feedMsg(IrcMsg(command='376', args=(self.nick,))) msg = irc.takeMsg() self.assertEqual(msg.command, 'PRIVMSG') self.assertEqual(msg.args[0], 'NickServ') irc.feedMsg(ircmsgs.notice(self.nick, 'now identified', 'NickServ')) def testSingleNetwork(self): try: self.assertNotError('services password %s secret' % self.nick) queuedJoin = ircmsgs.join('#test', prefix=self.prefix) self.irc.queueMsg(queuedJoin) self.assertIsNone(self.irc.takeMsg(), 'Join request went through before identification.') self._identify(self.irc) self.assertEqual(self.irc.takeMsg(), queuedJoin, 'Join request did not go through after identification.') finally: self.assertNotError('services password %s ""' % self.nick) def testMultipleNetworks(self): try: net1 = copy(self) net1.irc = getTestIrc('testnet1') net1.assertNotError('services password %s secret' % self.nick) net2 = copy(self) net2.irc = getTestIrc('testnet2') net2.assertNotError('services password %s secret' % self.nick) queuedJoin1 = ircmsgs.join('#testchan1', prefix=self.prefix) net1.irc.queueMsg(queuedJoin1) self.assertIsNone(net1.irc.takeMsg(), 'Join request 1 went through before identification.') self._identify(net1.irc) self.assertEqual(net1.irc.takeMsg(), queuedJoin1, 'Join request 1 did not go through after identification.') queuedJoin2 = ircmsgs.join('#testchan2', prefix=self.prefix) net2.irc.queueMsg(queuedJoin2) self.assertIsNone(net2.irc.takeMsg(), 'Join request 2 went through before identification.') self._identify(net2.irc) self.assertEqual(net2.irc.takeMsg(), queuedJoin2, 'Join request 2 did not go through after identification.') finally: net1.assertNotError('services password %s ""' % self.nick) net2.assertNotError('services password %s ""' % self.nick) class ExperimentalServicesTestCase(PluginTestCase): plugins = ["Services"] timeout = 0.1 def setUp(self): super().setUp() conf.supybot.protocols.irc.experimentalExtensions.setValue(True) self._initialCaps = self.irc.state.capabilities_ls.copy() self.irc.state.capabilities_ls["draft/account-registration"] = None self.irc.state.capabilities_ls["labeled-response"] = None def tearDown(self): self.irc.state.capabilities_ls = self._initialCaps conf.supybot.protocols.irc.experimentalExtensions.setValue(False) super().tearDown() def testRegisterSupportError(self): old_caps = self.irc.state.capabilities_ls.copy() try: del self.irc.state.capabilities_ls["labeled-response"] self.assertRegexp( "register p4ssw0rd", "error: This network does not support labeled-response.") del self.irc.state.capabilities_ls["draft/account-registration"] self.assertRegexp( "register p4ssw0rd", "error: This network does not support draft/account-registration.") finally: self.irc.state.capabilities_ls = old_caps def testRegisterRequireEmail(self): old_caps = self.irc.state.capabilities_ls.copy() try: self.irc.state.capabilities_ls["draft/account-registration"] = "email-required" self.assertRegexp( "register p4ssw0rd", "error: This network requires an email address to register.") finally: self.irc.state.capabilities_ls = old_caps def testRegisterSuccess(self): m = self.getMsg("register p4ssw0rd") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"])) self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="REGISTER", args=["SUCCESS", "accountname", "welcome!"] )) self.assertResponse( "", "Registration of account accountname on test succeeded: welcome!") def testRegisterSuccessBatch(self): # oragono replies with a batch m = self.getMsg("register p4ssw0rd") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"])) batch_name = "Services_testRegisterSuccessBatch" self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="BATCH", args=["+" + batch_name, "labeled-response"] )) self.irc.feedMsg(IrcMsg( server_tags={"batch": batch_name}, command="REGISTER", args=["SUCCESS", "accountname", "welcome!"] )) self.irc.feedMsg(IrcMsg( server_tags={"batch": batch_name}, command="NOTICE", args=[self.irc.nick, "Registration succeeded blah blah blah"] )) self.irc.feedMsg(IrcMsg( command="BATCH", args=["-" + batch_name], )) self.assertResponse( "", "Registration of account accountname on test succeeded: welcome!") def testRegisterSuccessEmail(self): m = self.getMsg("register p4ssw0rd foo@example.org") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg( command="REGISTER", args=["*", "foo@example.org", "p4ssw0rd"])) self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="REGISTER", args=["SUCCESS", "accountname", "welcome!"] )) self.assertResponse( "", "Registration of account accountname on test succeeded: welcome!") def testRegisterVerify(self): m = self.getMsg("register p4ssw0rd") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"])) self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="REGISTER", args=["VERIFICATION_REQUIRED", "accountname", "check your emails"] )) self.assertResponse( "", "Registration of accountname on test requires verification " "to complete: check your emails") m = self.getMsg("verify accountname c0de") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg( command="VERIFY", args=["accountname", "c0de"])) self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="VERIFY", args=["SUCCESS", "accountname", "welcome!"] )) self.assertResponse( "", "Verification of account accountname on test succeeded: welcome!") def testRegisterVerifyBatch(self): m = self.getMsg("register p4ssw0rd") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg(command="REGISTER", args=["*", "*", "p4ssw0rd"])) self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="REGISTER", args=["VERIFICATION_REQUIRED", "accountname", "check your emails"] )) self.assertResponse( "", "Registration of accountname on test requires verification " "to complete: check your emails") m = self.getMsg("verify accountname c0de") label = m.server_tags.pop("label") self.assertEqual(m, IrcMsg( command="VERIFY", args=["accountname", "c0de"])) batch_name = "Services_testVerifySuccessBatch" self.irc.feedMsg(IrcMsg( server_tags={"label": label}, command="BATCH", args=["+" + batch_name, "labeled-response"] )) self.irc.feedMsg(IrcMsg( server_tags={"batch": batch_name}, command="VERIFY", args=["SUCCESS", "accountname", "welcome!"] )) self.irc.feedMsg(IrcMsg( server_tags={"batch": batch_name}, command="NOTICE", args=[self.irc.nick, "Verification succeeded blah blah blah"] )) self.irc.feedMsg(IrcMsg( command="BATCH", args=["-" + batch_name], )) self.assertResponse( "", "Verification of account accountname on test succeeded: welcome!") # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/ShrinkUrl/0000755000175000017500000000000014535072535015524 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/__init__.py0000644000175000017500000000475614535072470017647 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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. ### """ Shrinks URLs using various URL shortening services, like tinyurl. """ import supybot import supybot.world as world __version__ = "%%VERSION%%" __author__ = supybot.authors.jemfinch __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {supybot.authors.jamessan: ['xrl.us support', 'x0.no support']} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/config.py0000644000175000017500000001165314535072470017347 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('ShrinkUrl') def configure(advanced): from supybot.questions import output, expect, anything, something, yn conf.registerPlugin('ShrinkUrl', True) if yn(_("""This plugin offers a snarfer that will go retrieve a shorter version of long URLs that are sent to the channel. Would you like this snarfer to be enabled?"""), default=False): conf.supybot.plugins.ShrinkUrl.shrinkSnarfer.setValue(True) class ShrinkService(registry.OnlySomeStrings): """Valid values include 'tiny', 'ur1', and 'x0'.""" validStrings = ('tiny', 'ur1', 'x0') class ShrinkCycle(registry.SpaceSeparatedListOfStrings): """Valid values include 'ln', 'tiny', 'ur1', and 'x0'.""" Value = ShrinkService def __init__(self, *args, **kwargs): super(ShrinkCycle, self).__init__(*args, **kwargs) self.lastIndex = -1 def setValue(self, v): super(self.__class__, self).setValue(v) self.lastIndex = -1 def getService(self): L = self() if L: self.lastIndex = (self.lastIndex + 1) % len(L) return L[self.lastIndex] raise ValueError('No services have been configured for rotation. ' \ 'See conf.supybot.plugins.ShrinkUrl.serviceRotation.') ShrinkUrl = conf.registerPlugin('ShrinkUrl') conf.registerChannelValue(ShrinkUrl, 'shrinkSnarfer', registry.Boolean(False, _("""Determines whether the shrink snarfer is enabled. This snarfer will watch for URLs in the channel, and if they're sufficiently long (as determined by supybot.plugins.ShrinkUrl.minimumLength) it will post a smaller URL from the service as denoted in supybot.plugins.ShrinkUrl.default."""))) conf.registerChannelValue(ShrinkUrl.shrinkSnarfer, 'showDomain', registry.Boolean(True, _("""Determines whether the snarfer will show the domain of the URL being snarfed along with the shrunken URL."""))) conf.registerChannelValue(ShrinkUrl, 'minimumLength', registry.PositiveInteger(48, _("""The minimum length a URL must be before the bot will shrink it."""))) conf.registerChannelValue(ShrinkUrl, 'nonSnarfingRegexp', registry.Regexp(None, _("""Determines what URLs are to be snarfed; URLs matching the regexp given will not be snarfed. Give the empty string if you have no URLs that you'd like to exclude from being snarfed."""))) conf.registerChannelValue(ShrinkUrl, 'outFilter', registry.Boolean(False, _("""Determines whether the bot will shrink the URLs of outgoing messages if those URLs are longer than supybot.plugins.ShrinkUrl.minimumLength."""))) conf.registerChannelValue(ShrinkUrl, 'default', ShrinkService('x0', _("""Determines what website the bot will use when shrinking a URL."""))) conf.registerGlobalValue(ShrinkUrl, 'bold', registry.Boolean(True, _("""Determines whether this plugin will bold certain portions of its replies."""))) conf.registerChannelValue(ShrinkUrl, 'serviceRotation', ShrinkCycle([], _("""If set to a non-empty value, specifies the list of services to rotate through for the shrinkSnarfer and outFilter."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/ShrinkUrl/locales/0000755000175000017500000000000014535072535017146 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/locales/fi.po0000644000175000017500000001353714535072470020113 0ustar00valval# ShrinkUrl plugin in Limnoria. # Copyright (C) 2011, 2012 Limnoria # Mikaela Suomalainen , 2011, 2012. # msgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2015-01-24 19:31+0100\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: \n" "X-Generator: Poedit 1.6.10\n" #: config.py:40 msgid "" "This plugin offers a snarfer that will go retrieve a shorter\n" " version of long URLs that are sent to the channel. Would you\n" " like this snarfer to be enabled?" msgstr "" "Tämä lisäosa tarjoaa kaappaajan,joka palauttaa lyhyemmän\n" " version pitkistä URL-osoitteista, jotka lähetetään kanavalle. Haluaisitko\n" " tämän kaappaajan olevan käytössä?" #: config.py:46 #, fuzzy msgid "Valid values include 'tiny', 'ur1', and 'x0'." msgstr "Kelvolliset arvot ovat 'ln', 'tiny', 'goo', 'ur1' ja 'x0' ." #: config.py:50 #, fuzzy msgid "Valid values include 'ln', 'tiny', 'ur1', and 'x0'." msgstr "Kelvolliset arvot ovat 'ln', 'tiny', 'goo', 'ur1' ja 'x0' ." #: config.py:71 #, fuzzy msgid "" "Determines whether the\n" " shrink snarfer is enabled. This snarfer will watch for URLs in the\n" " channel, and if they're sufficiently long (as determined by\n" " supybot.plugins.ShrinkUrl.minimumLength) it will post a\n" " smaller URL from the service as denoted in\n" " supybot.plugins.ShrinkUrl.default." msgstr "" "Määrittää onko kutistuskaappain käytössä.\n" " Tämä kaappaaja vahtii URL-osoitteita kanavalla\n" " ja jos ne ovat tarpeeksi pitkiä (määritetty asetusarvolla\n" " supybot.plugins.ShrinkUrl.minimumLength), se lähettää lyhyemmän URL-" "osoitteen\n" " joko sivustolta ln-s.net tai tinyurl.com, riippuen, minkä asetuksen\n" " supybot.plugins.ShrinkUrl.default määrittää." #: config.py:78 msgid "" "Determines whether the snarfer will show the\n" " domain of the URL being snarfed along with the shrunken URL." msgstr "" "Määrittää näyttääkö kaappaaja kaapatun URL-osoitteen domainin lyhennetyn\n" " URL-osoitteen kanssa." #: config.py:81 msgid "" "The minimum length a URL must be before\n" " the bot will shrink it." msgstr "" "Vähimmäispituus joka URL-osoitteen täytyy olla, ennen kuin\n" " botti kutistaa sen." #: config.py:84 msgid "" "Determines what URLs are to be snarfed; URLs\n" " matching the regexp given will not be snarfed. Give the empty string " "if\n" " you have no URLs that you'd like to exclude from being snarfed." msgstr "" "Määrittää mitä URL-osoitteita ei kaapata; URL-osoitteet, jotka\n" " täsmäävät annettuun säännölliseen lausekkeeseen jätetään kaappaamatta. " "Anna tyhjä merkkiketju, jos\n" " et halua estää mitään URL-osoitetta tulemasta kaapatuksi." #: config.py:88 msgid "" "Determines whether the bot will shrink the\n" " URLs of outgoing messages if those URLs are longer than\n" " supybot.plugins.ShrinkUrl.minimumLength." msgstr "" "Määrittää lyhentääkö botti ulosmenevien viestien\n" " URL-osoitteet, jos ne ovat pidempiä kuin\n" " supybot.plugins.ShrinkUrl.minimumLength." #: config.py:92 msgid "" "Determines what website the bot will use when\n" " shrinking a URL." msgstr "" "Määrittää mitä verkkosivua botti käyttää lyhentäessään\n" " URL-osoitetta." #: config.py:95 msgid "" "Determines whether this plugin will bold\n" " certain portions of its replies." msgstr "" "Määrittää korostaako botti tietyt osat\n" " vastauksissaan." #: config.py:98 msgid "" "If set to a non-empty value, specifies the list of\n" " services to rotate through for the shrinkSnarfer and outFilter." msgstr "" "Jos tämä on asetettu muuksi, kuin tyhjäksi arvoksi, määrittää listan\n" " palveluista, joita käytetään kutistuskaappaajalle ja ulostulon " "suodattimelle." #: plugin.py:101 msgid "" "This plugin features commands to shorten URLs through different services,\n" " like tinyurl." msgstr "" "Tämä plugin sisältää komennot URL-osoitteiden lyhentämiseen eri palveluilla, " "kuten esimerkiksi\n" " tinyurl:illa." #: plugin.py:205 msgid "" "\n" "\n" " Returns a TinyURL.com version of \n" " " msgstr "" "\n" "\n" " Palauttaa TinyURL.com palvelun lyhentämän version .\n" " " #: plugin.py:235 msgid "" "\n" "\n" " Returns an ur1 version of .\n" " " msgstr "" "\n" " Palauttaa ur1-version .\n" " " #: plugin.py:262 msgid "" "\n" "\n" " Returns an x0.no version of .\n" " " msgstr "" "\n" "\n" " Palauttaa x0.no palvelun lyhentämän version .\n" " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an ln-s.net version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa ln-s.net version .\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an goo.gl version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa goo.gl-palvelun lyhentämän version ." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an expanded version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ " Palauttaa laajennetun version .\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an xrl.us version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa xrl.us palvelun lyhentämän version .\n" #~ " " #~ msgid "Valid values include 'ln', 'tiny', 'xrl', 'goo', and 'x0'." #~ msgstr "Kelvolliset arvot ovat 'ln', 'tiny', 'xrl', 'goo' ja 'x0' ." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/locales/fr.po0000644000175000017500000001213514535072470020115 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2015-01-24 19:32+0100\n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:40 msgid "" "This plugin offers a snarfer that will go retrieve a shorter\n" " version of long URLs that are sent to the channel. Would you\n" " like this snarfer to be enabled?" msgstr "" "Ce plugin offre un snarfer qui récupère de longues URLs envoyées sur un " "canal pour en envoyer une version plus courte. Voulez-vous activer ce " "snarfer ?" #: config.py:46 #, fuzzy msgid "Valid values include 'tiny', 'ur1', and 'x0'." msgstr "Les valeurs valides incluent 'ln', 'tiny', 'goo', 'ur1', et 'x0'." #: config.py:50 #, fuzzy msgid "Valid values include 'ln', 'tiny', 'ur1', and 'x0'." msgstr "Les valeurs valides incluent 'ln', 'tiny', 'goo', 'ur1', et 'x0'." #: config.py:71 #, fuzzy msgid "" "Determines whether the\n" " shrink snarfer is enabled. This snarfer will watch for URLs in the\n" " channel, and if they're sufficiently long (as determined by\n" " supybot.plugins.ShrinkUrl.minimumLength) it will post a\n" " smaller URL from the service as denoted in\n" " supybot.plugins.ShrinkUrl.default." msgstr "" "Détermine si le snarfer d'URL est activé. Ce remplaceur sera à l'écoute de " "toutes les URLs sur lle canal, et, si elle sont suffisamment longues " "(déterminé par supybot.plugins.ShrinkUrl.minimumLength) le bot postera une " "URL raccourcie avec ln-s.net ou tinyurl.com, comme défini par supybot." "plugins.ShrinkUrl.default." #: config.py:78 msgid "" "Determines whether the snarfer will show the\n" " domain of the URL being snarfed along with the shrunken URL." msgstr "" "Détermine si le snarfer affichera le domaine de l'URL snarfée avec l'URL " "raccourcie." #: config.py:81 msgid "" "The minimum length a URL must be before\n" " the bot will shrink it." msgstr "La taille minimum d'une URL pour que le bot la raccourcice." #: config.py:84 msgid "" "Determines what URLs are to be snarfed; URLs\n" " matching the regexp given will not be snarfed. Give the empty string " "if\n" " you have no URLs that you'd like to exclude from being snarfed." msgstr "" "Détermine quelles URLs seront snarfées ; les URLs correspondant à " "l'expression régulière ne seront par snarfées. Donnez une chaîne vide si il " "n'y a pas d'URL que vous voulez exclure." #: config.py:88 msgid "" "Determines whether the bot will shrink the\n" " URLs of outgoing messages if those URLs are longer than\n" " supybot.plugins.ShrinkUrl.minimumLength." msgstr "" "Détermine si le bot raccourcira les URLs des messages sortant si ces URLs " "sont plus longues que supybot.plugins.ShrinkUrl.minimumLength." #: config.py:92 msgid "" "Determines what website the bot will use when\n" " shrinking a URL." msgstr "Détermine quel site web le bot utilisera pour raccourcir une URL" #: config.py:95 msgid "" "Determines whether this plugin will bold\n" " certain portions of its replies." msgstr "" "Détermine si ce plugin mettra en gras certaines portions de ses réponses." #: config.py:98 msgid "" "If set to a non-empty value, specifies the list of\n" " services to rotate through for the shrinkSnarfer and outFilter." msgstr "" "Si définit à une valeur non vide, définit la liste des services à faire " "tourner pour shrinkSnarfer et outFilter." #: plugin.py:101 msgid "" "This plugin features commands to shorten URLs through different services,\n" " like tinyurl." msgstr "" #: plugin.py:205 msgid "" "\n" "\n" " Returns a TinyURL.com version of \n" " " msgstr "" "\n" "\n" "Retourne une version de TinyURL.com de l'." #: plugin.py:235 msgid "" "\n" "\n" " Returns an ur1 version of .\n" " " msgstr "" "\n" "\n" "Retourne une version de ur1 de l'." #: plugin.py:262 msgid "" "\n" "\n" " Returns an x0.no version of .\n" " " msgstr "" "\n" "\n" "Retourne une version de x0.no de l'." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an ln-s.net version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne une version de ln-s.net de l'." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an goo.gl version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne une version de goo.gl de l'." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an expanded version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne la version large de l'." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an xrl.us version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne une version de xrl.us de l'." #~ msgid "Valid values include 'ln', 'tiny', 'xrl', 'goo', and 'x0'." #~ msgstr "Les valeurs valides incluent 'ln', 'tiny', 'xrl', 'goo', et 'x0'." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/locales/it.po0000644000175000017500000001217314535072470020124 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2015-01-24 11:45+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:40 msgid "" "This plugin offers a snarfer that will go retrieve a shorter\n" " version of long URLs that are sent to the channel. Would you\n" " like this snarfer to be enabled?" msgstr "" "Questo plugin offre un cattura URL che riporterà una versione accorciata\n" " di quelli lunghi inviati al canale. Lo si vuole abilitare?" #: config.py:46 #, fuzzy msgid "Valid values include 'tiny', 'ur1', and 'x0'." msgstr "I valori validi comprendono \"ln\", \"tiny\", \"goo\" e \"x0\"." #: config.py:50 #, fuzzy msgid "Valid values include 'ln', 'tiny', 'ur1', and 'x0'." msgstr "I valori validi comprendono \"ln\", \"tiny\", \"goo\" e \"x0\"." #: config.py:71 #, fuzzy msgid "" "Determines whether the\n" " shrink snarfer is enabled. This snarfer will watch for URLs in the\n" " channel, and if they're sufficiently long (as determined by\n" " supybot.plugins.ShrinkUrl.minimumLength) it will post a\n" " smaller URL from the service as denoted in\n" " supybot.plugins.ShrinkUrl.default." msgstr "" "Determina se l'accorcia URL è abilitato. Questo controllerà gli URL che " "passano\n" " in canale e se sono sufficientemente lunghi (determinato da\n" " supybot.plugins.ShrinkUrl.minimumLength) il bot ne invierà uno più " "corto\n" " tramite ln-s.net o tinyurl.com, come definito in supybot.plugins." "ShrinkUrl.default." #: config.py:78 msgid "" "Determines whether the snarfer will show the\n" " domain of the URL being snarfed along with the shrunken URL." msgstr "" "Determina se l'accorcia URL mostrerà il dominio dell'URL originale assieme a " "quello accorciato." #: config.py:81 msgid "" "The minimum length a URL must be before\n" " the bot will shrink it." msgstr "" "La lunghezza minima che un URL deve avere affinché il bot decida di " "accorciarlo." #: config.py:84 msgid "" "Determines what URLs are to be snarfed; URLs\n" " matching the regexp given will not be snarfed. Give the empty string " "if\n" " you have no URLs that you'd like to exclude from being snarfed." msgstr "" "Determina quali URL debbano essere intercettati; quelli che corrispondono " "alla\n" " regexp fornita non verranno coinvolti. Se non si vuole escludere alcun " "URL,\n" " aggiungere una stringa vuota." #: config.py:88 msgid "" "Determines whether the bot will shrink the\n" " URLs of outgoing messages if those URLs are longer than\n" " supybot.plugins.ShrinkUrl.minimumLength." msgstr "" "Determina se il bot accorcerà gli URL dei messaggi in uscita se questi sono " "più\n" " lunghi del valore di supybot.plugins.ShrinkUrl.minimumLength." #: config.py:92 msgid "" "Determines what website the bot will use when\n" " shrinking a URL." msgstr "Determina quale sito web il bot userà per accorciare un URL." #: config.py:95 msgid "" "Determines whether this plugin will bold\n" " certain portions of its replies." msgstr "" "Determina se il plugin riporterà in grassetto alcune porzioni delle risposte." #: config.py:98 msgid "" "If set to a non-empty value, specifies the list of\n" " services to rotate through for the shrinkSnarfer and outFilter." msgstr "" "Se impostato ad un valore non vuoto, specifica l'elenco dei servizi a cui\n" " rivolgersi per le variabili shrinkSnarfer e outFilter." #: plugin.py:101 msgid "" "This plugin features commands to shorten URLs through different services,\n" " like tinyurl." msgstr "" #: plugin.py:205 msgid "" "\n" "\n" " Returns a TinyURL.com version of \n" " " msgstr "" "\n" "\n" " Restituisce una versione di TinyURL.com di \n" " " #: plugin.py:235 #, fuzzy msgid "" "\n" "\n" " Returns an ur1 version of .\n" " " msgstr "" "\n" "\n" " Restituisce una versione di xrl.us di .\n" " " #: plugin.py:262 msgid "" "\n" "\n" " Returns an x0.no version of .\n" " " msgstr "" "\n" "\n" " Restituisce una versione di x0.no di .\n" " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an ln-s.net version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce una versione di ln-s.net di .\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an goo.gl version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce una versione di goo.gl di .\n" #~ " " #, fuzzy #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an expanded version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce una versione di x0.no di .\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns an xrl.us version of .\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce una versione di xrl.us di .\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/plugin.py0000644000175000017500000002371214535072470017377 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import sys import time import json import supybot.log as log import supybot.conf as conf import supybot.utils as utils 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, internationalizeDocstring _ = PluginInternationalization('ShrinkUrl') class CdbShrunkenUrlDB(object): def __init__(self, filename): self.dbs = {} cdb = conf.supybot.databases.types.cdb services = list(conf.supybot.plugins.ShrinkUrl.default.validStrings) services.append('Expand') for service in services: dbname = filename.replace('.db', service.capitalize() + '.db') try: self.dbs[service] = cdb.connect(dbname) except OSError as e: log.error( 'ShrinkUrl: Can not open database %s: %s', dbname, e) raise KeyError("Could not open %s" % dbname) except: log.exception( 'ShrinkUrl: Can not read database %s (data corruption?)', dbname) raise KeyError("Could not open %s" % dbname) def get(self, service, url): return self.dbs[service][url] def set(self, service, url, shrunkurl): self.dbs[service][url] = shrunkurl def close(self): for service in self.dbs: self.dbs[service].close() def flush(self): for service in self.dbs: self.dbs[service].flush() ShrunkenUrlDB = plugins.DB('ShrinkUrl', {'cdb': CdbShrunkenUrlDB}) class ShrinkError(Exception): pass def retry(f): def newf(*args, **kwargs): for x in range(0, 3): try: return f(*args, **kwargs) except Exception: log.exception('Shrinking URL failed. Trying again.') time.sleep(1) return f(*args, **kwargs) return newf class ShrinkUrl(callbacks.PluginRegexp): """This plugin features commands to shorten URLs through different services, like tinyurl.""" regexps = ['shrinkSnarfer'] def __init__(self, irc): self.__parent = super(ShrinkUrl, self) self.__parent.__init__(irc) self.db = ShrunkenUrlDB() def die(self): self.db.close() def callCommand(self, command, irc, msg, *args, **kwargs): try: self.__parent.callCommand(command, irc, msg, *args, **kwargs) except utils.web.Error as e: irc.error(str(e)) def _outFilterThread(self, irc, msg): (channel, text) = msg.args network = irc.network for m in utils.web.httpUrlRe.finditer(text): url = m.group(1) if len(url) > self.registryValue('minimumLength', channel, network): try: cmd = self.registryValue('serviceRotation', channel, network, value=False) cmd = cmd.getService().capitalize() except ValueError: cmd = self.registryValue('default', channel, network) \ .capitalize() try: shortUrl = getattr(self, '_get%sUrl' % cmd)(url) text = text.replace(url, shortUrl) except (utils.web.Error, AttributeError, ShrinkError): pass newMsg = ircmsgs.privmsg(channel, text, msg=msg) newMsg.tag('shrunken') irc.queueMsg(newMsg) def outFilter(self, irc, msg): if msg.command != 'PRIVMSG': return msg if msg.channel: if not msg.shrunken: if self.registryValue('outFilter', msg.channel, irc.network): if utils.web.httpUrlRe.search(msg.args[1]): self._outFilterThread(irc, msg) return None return msg def shrinkSnarfer(self, irc, msg, match): channel = msg.channel network = irc.network if not channel: return if self.registryValue('shrinkSnarfer', channel, network): url = match.group(0) r = self.registryValue('nonSnarfingRegexp', channel, network) if r and r.search(url) is not None: self.log.debug('Matched nonSnarfingRegexp: %u', url) return minlen = self.registryValue('minimumLength', channel, network) try: cmd = self.registryValue('serviceRotation', channel, network, value=False) cmd = cmd.getService().capitalize() except ValueError: cmd = self.registryValue('default', channel, network) \ .capitalize() if len(url) >= minlen: try: shorturl = getattr(self, '_get%sUrl' % cmd)(url) except (utils.web.Error, AttributeError, ShrinkError): self.log.info('Couldn\'t get shorturl for %u', url) return if self.registryValue('shrinkSnarfer.showDomain', channel, network): domain = ' (at %s)' % utils.web.getDomain(url) else: domain = '' if self.registryValue('bold'): s = format('%u%s', ircutils.bold(shorturl), domain) else: s = format('%u%s', shorturl, domain) m = irc.reply(s, prefixNick=False) if m is not None: m.tag('shrunken') shrinkSnarfer = urlSnarfer(shrinkSnarfer) shrinkSnarfer.__doc__ = utils.web._httpUrlRe @retry def _getTinyUrl(self, url): try: return self.db.get('tiny', url) except KeyError: text = utils.web.getUrl('http://tinyurl.com/api-create.php?url=' + url) text = text.decode() if text.startswith('Error'): raise ShrinkError(text[5:]) self.db.set('tiny', url, text) return text @internationalizeDocstring def tiny(self, irc, msg, args, url): """ Returns a TinyURL.com version of """ try: tinyurl = self._getTinyUrl(url) m = irc.reply(tinyurl) if m is not None: m.tag('shrunken') except ShrinkError as e: irc.errorPossibleBug(str(e)) tiny = thread(wrap(tiny, ['httpUrl'])) _ur1Api = 'http://ur1.ca/' _ur1Regexp = re.compile(r'') @retry def _getUr1Url(self, url): try: return self.db.get('ur1ca', utils.web.urlquote(url)) except KeyError: parameters = utils.web.urlencode({'longurl': url}) response = utils.web.getUrl(self._ur1Api, data=parameters) ur1ca = self._ur1Regexp.search(response.decode()).group('url') if ur1ca: self.db.set('ur1', url, ur1ca) return ur1ca else: raise ShrinkError(response) def ur1(self, irc, msg, args, url): """ Returns an ur1 version of . """ try: ur1url = self._getUr1Url(url) m = irc.reply(ur1url) if m is not None: m.tag('shrunken') except ShrinkError as e: irc.error(str(e)) ur1 = thread(wrap(ur1, ['httpUrl'])) _x0Api = 'https://x0.no/api/?%s' @retry def _getX0Url(self, url): try: return self.db.get('x0', url) except KeyError: text = utils.web.getUrl(self._x0Api % url).decode() if text.startswith('ERROR:'): raise ShrinkError(text[6:]) self.db.set('x0', url, text) return text @internationalizeDocstring def x0(self, irc, msg, args, url): """ Returns an x0.no version of . """ try: x0url = self._getX0Url(url) m = irc.reply(x0url) if m is not None: m.tag('shrunken') except ShrinkError as e: irc.error(str(e)) x0 = thread(wrap(x0, ['httpUrl'])) Class = ShrinkUrl # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/ShrinkUrl/test.py0000644000175000017500000001224614535072470017060 0ustar00valval### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, 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 * class ShrinkUrlTestCase(ChannelPluginTestCase): plugins = ('ShrinkUrl',) config = {'supybot.snarfThrottle': 0} sfUrl ='http://sourceforge.net/p/supybot/bugs/?source=navbar' udUrl = 'http://www.urbandictionary.com/define.php?' \ 'term=all+your+base+are+belong+to+us' tests = {'tiny': [(sfUrl, r'https://tinyurl.com/b7wyvfz'), (udUrl, r'https://tinyurl.com/u479')], 'ur1': [(sfUrl, r'http://ur1.ca/ceqh8'), (udUrl, r'http://ur1.ca/9xl9k')], 'x0': [(sfUrl, r'https://x0.no/a53s'), (udUrl, r'https://x0.no/0l2k')] } if network: def testShrink(self): for (service, testdata) in self.tests.items(): for (url, shrunkurl) in testdata: self.assertRegexp('shrinkurl %s %s' % (service, url), shrunkurl) def testShrinkCycle(self): cycle = conf.supybot.plugins.ShrinkUrl.serviceRotation snarfer = conf.supybot.plugins.ShrinkUrl.shrinkSnarfer origcycle = cycle() origsnarfer = snarfer() try: self.assertNotError( 'config plugins.ShrinkUrl.serviceRotation tiny x0') self.assertError( 'config plugins.ShrinkUrl.serviceRotation tiny x1') snarfer.setValue(True) self.assertSnarfRegexp(self.udUrl, r'.*%s.* \(at' % self.tests['tiny'][1][1]) self.assertSnarfRegexp(self.udUrl, r'.*%s.* \(at' % self.tests['x0'][1][1]) finally: cycle.setValue(origcycle) snarfer.setValue(origsnarfer) def _snarf(self, service): shrink = conf.supybot.plugins.ShrinkUrl origService = shrink.default() origSnarf = shrink.shrinkSnarfer() shrink.default.setValue(service) shrink.shrinkSnarfer.setValue(True) try: for (url, shrunkurl) in self.tests[service]: teststr = r'.*%s.* \(at' % shrunkurl self.assertSnarfRegexp(url, teststr) finally: shrink.default.setValue(origService) shrink.shrinkSnarfer.setValue(origSnarf) def testTinysnarf(self): self._snarf('tiny') def testUr1snarf(self): self._snarf('ur1') def testX0snarf(self): self._snarf('x0') def testNonSnarfing(self): shrink = conf.supybot.plugins.ShrinkUrl origService = shrink.default() origSnarf = shrink.shrinkSnarfer() origLen = shrink.minimumLength() origRegexp = shrink.nonSnarfingRegexp() shrink.default.setValue('tiny') shrink.shrinkSnarfer.setValue(True) shrink.minimumLength.setValue(10) shrink.nonSnarfingRegexp.set('m/sf/') try: self.assertSnarfNoResponse('http://sf.net/', 5) self.assertSnarfRegexp('http://sourceforge.net/', r'http://tinyurl.com/7vm7.*\(at ') finally: shrink.default.setValue(origService) shrink.shrinkSnarfer.setValue(origSnarf) shrink.minimumLength.setValue(origLen) shrink.nonSnarfingRegexp.setValue(origRegexp) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Status/0000755000175000017500000000000014535072535015066 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/__init__.py0000644000175000017500000000463114535072470017201 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ A simple module to handle various informational commands querying the bot's current status and statistics, like its uptime. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/config.py0000644000175000017500000000552514535072470016712 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Status') 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('Status', True) Status = conf.registerPlugin('Status') conf.registerGroup(Status, 'cpu') conf.registerChannelValue(Status.cpu, 'children', registry.Boolean(True, _("""Determines whether the cpu command will list the time taken by children as well as the bot's process."""))) conf.registerChannelValue(Status.cpu, 'threads', registry.Boolean(False, _("""Determines whether the cpu command will provide the number of threads spawned and active."""))) conf.registerChannelValue(Status.cpu, 'memory', registry.Boolean(True, _("""Determines whether the cpu command will report the amount of memory being used by the bot."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/Status/locales/0000755000175000017500000000000014535072535016510 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/locales/de.po0000644000175000017500000001247314535072470017445 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:46+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: German\n" "X-Poedit-Country: GERMANY\n" #: config.py:47 msgid "" "Determines whether the cpu command will list\n" " the time taken by children as well as the bot's process." msgstr "" "legt fest ob der cpu Befehl die Zeit die die Kindprozesse verbrauchten, als " "auch die Zeit des Botprozesses." #: config.py:50 msgid "" "Determines whether the cpu command will\n" " provide the number of threads spawned and active." msgstr "" "Legt fest ob der cpu Befehl die Anzahl der gestarteten Threads und der " "Aktiven anzeigen soll." #: config.py:53 msgid "" "Determines whether the cpu command will report\n" " the amount of memory being used by the bot." msgstr "" "Legt fest ob der cpu Befehl den gebrauchten Speicher des Bots ausgeben soll." #: plugin.py:48 msgid "" "This plugin allows you to view different bot statistics, for example,\n" " uptime." msgstr "" #: plugin.py:75 msgid "" "takes no arguments\n" "\n" " Returns the status of the bot.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt den Status des Bots aus." #: plugin.py:83 msgid "%s as %L" msgstr "%s als %L" #: plugin.py:84 msgid "I am connected to %L." msgstr "Ich bin verbunden zu %L." #: plugin.py:86 msgid "I am currently in code profiling mode." msgstr "Momentan bin ich im quelltextanalyse Modus." #: plugin.py:92 msgid "" "takes no arguments\n" "\n" " Returns the current threads that are active.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt aus wieviele Threads momentan aktiv sind." #: plugin.py:98 msgid "I have spawned %n; %n %b still currently active: %L." msgstr "Ich habe %n erzeugt. %n %b sind jetzt noch Aktiv: %L." #: plugin.py:105 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the number of processes that have been spawned, and list of\n" " ones that are still active.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt aus wieviele Threads momentan aktiv sind." #: plugin.py:120 msgid "" "takes no arguments\n" "\n" " Returns some interesting network-related statistics.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt einige interessante Netzwerk bezogene Statistiken aus." #: plugin.py:128 msgid "an indeterminate amount of time" msgstr "eine unbestimmte Zeit" #: plugin.py:129 msgid "" "I have received %s messages for a total of %S. I have sent %s messages for " "a total of %S. I have been connected to %s for %s." msgstr "" "Ich habe %s Nachrichten empfangen, im totalen %S. Ich habe %s Nachrichten " "gesendet, im totalen %S. Ich bin mit %s verbunden, seit %s." #: plugin.py:138 msgid "" "takes no arguments\n" "\n" " Returns some interesting CPU-related statistics on the bot.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt einige interessante CPU bezogene Statistiken des Bots aus." #: plugin.py:148 msgid "" "My children have taken %.2f seconds of user time and %.2f seconds of system " "time for a total of %.2f seconds of CPU time." msgstr "" "Meine Kindsprozesse haben %.2f Sekunden an Nutzerzeit und %.2f Systemzeit " "gebraucht, im totalen %.2f Sekunden der CPU Zeit." #: plugin.py:155 msgid "" "I have taken %.2f seconds of user time and %.2f seconds of system time, for " "a total of %.2f seconds of CPU time. %s" msgstr "" "Ich habe %.2f Sekunden an benutzer Zeit und %.2f Sekunden an Systemzeit " "benötigt, in allen %.2f CPU Zeit. %s" #: plugin.py:177 msgid "Unable to run ps command." msgstr "Ich kann den Befehl ps nicht ausführen." #: plugin.py:184 msgid " I'm taking up %S of memory." msgstr " Ich verbrauche %S Speicher." #: plugin.py:187 #, fuzzy msgid " I'm taking up an unknown amount of memory." msgstr " Ich verbrauche %S Speicher." #: plugin.py:195 msgid "" "takes no arguments\n" "\n" " Returns some interesting command-related statistics.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt einige interessante befehlbezogene Statistiken aus." #: plugin.py:205 msgid "I offer a total of %n in %n. I have processed %n." msgstr "Ich biete %n in %n an. Ich habe %n bearbeitet." #: plugin.py:214 msgid "" "takes no arguments\n" "\n" " Returns a list of the commands offered by the bot.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt eine Liste der Befehle zurück, die der Bot anbietet." #: plugin.py:228 msgid "" "takes no arguments\n" "\n" " Returns the amount of time the bot has been running.\n" " " msgstr "" "hat keine Argumente\n" "\n" "Gibt aus wie lange der Bot schon läuft." #: plugin.py:232 msgid "I have been running for %s." msgstr "Ich laufe seit %s." #: plugin.py:239 msgid "" "takes no arguments\n" "\n" " Returns the server the bot is on.\n" " " msgstr "" "hat keine Arguments\n" "\n" "Gibt den Server aus mit dem der Bot verbunden ist." #: plugin.py:248 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the network the bot is on.\n" " " msgstr "" "hat keine Arguments\n" "\n" "Gibt den Server aus mit dem der Bot verbunden ist." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/locales/fi.po0000644000175000017500000001375414535072470017456 0ustar00valval# Status plugin in Limnoria # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: Status plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 11:37+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 "" "Determines whether the cpu command will list\n" " the time taken by children as well as the bot's process." msgstr "" "Määrittää luetteleeko cpu komento lasten ottaman ajan\n" " botin prosessin lisäksi." #: config.py:50 msgid "" "Determines whether the cpu command will\n" " provide the number of threads spawned and active." msgstr "" "Määrittää ilmoittaako cpu komento ilmestyneiden ja aktiiviseten ketjujen\n" " lukumäärät." #: config.py:53 msgid "" "Determines whether the cpu command will report\n" " the amount of memory being used by the bot." msgstr "" "Määrittää ilmoittaako cpu komento\n" " botin käyttämän muistin määrän." #: plugin.py:48 msgid "" "This plugin allows you to view different bot statistics, for example,\n" " uptime." msgstr "" "Tämä plugini näyttää erilaisia tilastoja botista, kuten esimerkiksi " "käynnissäolo-ajan." #: plugin.py:75 msgid "" "takes no arguments\n" "\n" " Returns the status of the bot.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa botin tilan.\n" " " #: plugin.py:83 msgid "%s as %L" msgstr "%s verkossa %L" #: plugin.py:84 msgid "I am connected to %L." msgstr "Olen yhdistänyt verkkoon %L" #: plugin.py:86 msgid "I am currently in code profiling mode." msgstr "Olen tällä hetkellä koodin profilointi tilassa." #: plugin.py:92 msgid "" "takes no arguments\n" "\n" " Returns the current threads that are active.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa botin tämänhetkiset ketjut, jotka ovat aktiivisia.\n" " " #: plugin.py:98 msgid "I have spawned %n; %n %b still currently active: %L." msgstr "Minä olen ilmestyttänyt %n; %n %b yhä aktiivinen: %L." #: plugin.py:105 msgid "" "takes no arguments\n" "\n" " Returns the number of processes that have been spawned, and list of\n" " ones that are still active.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa prosessien määrän, jotka on luotu ja listan niistä prosesseista, " "jotka ovat yhä\n" " aktiivisia..\n" " " #: plugin.py:120 msgid "" "takes no arguments\n" "\n" " Returns some interesting network-related statistics.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa joitakin mielenkiintoisia verkkoon liittyviä " "tilastotietoja.\n" " " #: plugin.py:128 msgid "an indeterminate amount of time" msgstr "määrittämätön määrä aikaa" #: plugin.py:129 msgid "" "I have received %s messages for a total of %S. I have sent %s messages for " "a total of %S. I have been connected to %s for %s." msgstr "" "Minä olen vastaanottanut %s viestiä yhteensä määrän %S edestä. Olen " "lähettänyt %s viestiä määrän %S edestä. Olen ollut yhdistettynä palvelimeen " "%s ajan %s." #: plugin.py:138 msgid "" "takes no arguments\n" "\n" " Returns some interesting CPU-related statistics on the bot.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa joitakin mielenkiintoisia suoritinkäyttöön liittyviä " "tilastotietoja botista.\n" " " #: plugin.py:148 msgid "" "My children have taken %.2f seconds of user time and %.2f seconds of system " "time for a total of %.2f seconds of CPU time." msgstr "" "Minun lapseni ovat vieneet %.2f käyttäjän aikaa ja %.2f sekuntia " "järjestelmän aikaa. Lapseni ovat ottaneet yhteensä %.2f sekuntia CPU aikaa." #: plugin.py:155 msgid "" "I have taken %.2f seconds of user time and %.2f seconds of system time, for " "a total of %.2f seconds of CPU time. %s" msgstr "" "Olen ottanut %.2f sekuntia käyttäjän aikaa %.2f sekuntia järjestelmän aikaa. " "Olen ottanut yhteensä %.2f sekuntia CPU ajasta. %s" #: plugin.py:177 msgid "Unable to run ps command." msgstr "ps komentoa ei pystytä suorittamaan." #: plugin.py:184 msgid " I'm taking up %S of memory." msgstr " Muistinkäyttöni on yhteensä %S." #: plugin.py:187 #, fuzzy msgid " I'm taking up an unknown amount of memory." msgstr " Muistinkäyttöni on yhteensä %S." #: plugin.py:195 msgid "" "takes no arguments\n" "\n" " Returns some interesting command-related statistics.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa joitakin mielenkiintoisia komentoihin liittyviä " "tilastotietoja.\n" " " #: plugin.py:205 msgid "I offer a total of %n in %n. I have processed %n." msgstr "Tarjoan yhteensä %n määrän %n sisässä. Olen käsitellyt %n." #: plugin.py:214 msgid "" "takes no arguments\n" "\n" " Returns a list of the commands offered by the bot.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa listan komennoista, jotka botti tarjoaa.\n" " " #: plugin.py:228 msgid "" "takes no arguments\n" "\n" " Returns the amount of time the bot has been running.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa ajan, jonka botti on ollut käynnissä.\n" " " #: plugin.py:232 msgid "I have been running for %s." msgstr "Olen ollut käynnissä ajan %s." #: plugin.py:239 msgid "" "takes no arguments\n" "\n" " Returns the server the bot is on.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa palvelimen, jolla botti on.\n" " " #: plugin.py:248 msgid "" "takes no arguments\n" "\n" " Returns the network the bot is on.\n" " " msgstr "" "ei ota parametrejä\n" "\n" " Palauttaa verkon, jossa botti on.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/locales/fr.po0000644000175000017500000001255114535072470017461 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:47 msgid "" "Determines whether the cpu command will list\n" " the time taken by children as well as the bot's process." msgstr "" "Détermine si la commande cpu listera le temps pris par les enfants aussi " "bien que celui du processus du bot." #: config.py:50 msgid "" "Determines whether the cpu command will\n" " provide the number of threads spawned and active." msgstr "" "Détermine si la commande cpu donnera le nombre de threads lancés et actifs." #: config.py:53 msgid "" "Determines whether the cpu command will report\n" " the amount of memory being used by the bot." msgstr "" "Détermine si la commande cpu donnera la quantité de mémoire utilisée par le " "bot." #: plugin.py:48 msgid "" "This plugin allows you to view different bot statistics, for example,\n" " uptime." msgstr "" #: plugin.py:75 msgid "" "takes no arguments\n" "\n" " Returns the status of the bot.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le status du bot." #: plugin.py:83 msgid "%s as %L" msgstr "%s en tant que %L" #: plugin.py:84 msgid "I am connected to %L." msgstr "Je suis connecté à %L" #: plugin.py:86 msgid "I am currently in code profiling mode." msgstr "Je suis actuellement en mode de profiling du code." #: plugin.py:92 msgid "" "takes no arguments\n" "\n" " Returns the current threads that are active.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne les threads actifs." #: plugin.py:98 msgid "I have spawned %n; %n %b still currently active: %L." msgstr "J'ai lancé %n ; %n %b encore actuellement en vie : %L." #: plugin.py:105 msgid "" "takes no arguments\n" "\n" " Returns the number of processes that have been spawned, and list of\n" " ones that are still active.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le nombre de processus qui ont été lancés, et la liste de ceux qui " "sont encore actifs." #: plugin.py:120 msgid "" "takes no arguments\n" "\n" " Returns some interesting network-related statistics.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne quelques statistiques intéressantes liées au réseau." #: plugin.py:128 msgid "an indeterminate amount of time" msgstr "une durée indéterminée" #: plugin.py:129 msgid "" "I have received %s messages for a total of %S. I have sent %s messages for " "a total of %S. I have been connected to %s for %s." msgstr "" "J'ai reçu %s message pour un total de %S. J'ai envoyé %s messages pour un " "total de %S. J'ai été connecté à %s pendant %s." #: plugin.py:138 msgid "" "takes no arguments\n" "\n" " Returns some interesting CPU-related statistics on the bot.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne quelques statistiques intéressantes reliées au CPU." #: plugin.py:148 msgid "" "My children have taken %.2f seconds of user time and %.2f seconds of system " "time for a total of %.2f seconds of CPU time." msgstr "" "Mes enfants ont pris %.2f secondes du temps utilisateur et %.2f secondes du " "temps système, pour un total de %.2f secondes de temps CPU." #: plugin.py:155 msgid "" "I have taken %.2f seconds of user time and %.2f seconds of system time, for " "a total of %.2f seconds of CPU time. %s" msgstr "" "J'ai pris %.2f secondes du temps utilisateur et %.2f secondes du temps " "système, pour un total de %.2f secondes de temps CPU. %s" #: plugin.py:177 msgid "Unable to run ps command." msgstr "Impossible de lancer la commande ps." #: plugin.py:184 msgid " I'm taking up %S of memory." msgstr " Je prend plus de %S de mémoire." #: plugin.py:187 #, fuzzy msgid " I'm taking up an unknown amount of memory." msgstr " Je prend plus de %S de mémoire." #: plugin.py:195 msgid "" "takes no arguments\n" "\n" " Returns some interesting command-related statistics.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne quelques statistiques intéressantes liées aux commandes." #: plugin.py:205 msgid "I offer a total of %n in %n. I have processed %n." msgstr "J'offre un total de %n dans %n plugins. J'ai géré %n." #: plugin.py:214 msgid "" "takes no arguments\n" "\n" " Returns a list of the commands offered by the bot.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne une liste des commandes offertes par le bot." #: plugin.py:228 msgid "" "takes no arguments\n" "\n" " Returns the amount of time the bot has been running.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne la durée durant laquelle le bot est lancée." #: plugin.py:232 msgid "I have been running for %s." msgstr "Je suis lancé depuis %s." #: plugin.py:239 msgid "" "takes no arguments\n" "\n" " Returns the server the bot is on.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le(s) serveur(s) sur le(s)quel(s) le bot est." #: plugin.py:248 msgid "" "takes no arguments\n" "\n" " Returns the network the bot is on.\n" " " msgstr "" "ne prend pas d'argument\n" "\n" "Retourne le(s) serveur(s) sur le(s)quel(s) le bot est." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/locales/it.po0000644000175000017500000001267314535072470017473 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-03-16 12:41+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 "" "Determines whether the cpu command will list\n" " the time taken by children as well as the bot's process." msgstr "" "Determina se il comando cpu elencherà il tempo impiegato\n" " dal processo figlio così come il processo del bot." #: config.py:50 msgid "" "Determines whether the cpu command will\n" " provide the number of threads spawned and active." msgstr "" "Determina se il comando cpu fornirà il numero di thread avviati e attivi." #: config.py:53 msgid "" "Determines whether the cpu command will report\n" " the amount of memory being used by the bot." msgstr "" "Determina se il comando cpu riporterà la quantità di memoria usata dal bot." #: plugin.py:48 msgid "" "This plugin allows you to view different bot statistics, for example,\n" " uptime." msgstr "" #: plugin.py:75 msgid "" "takes no arguments\n" "\n" " Returns the status of the bot.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta lo stato del bot.\n" " " #: plugin.py:83 msgid "%s as %L" msgstr "%s come %L" #: plugin.py:84 msgid "I am connected to %L." msgstr "Sono connesso a %L." #: plugin.py:86 msgid "I am currently in code profiling mode." msgstr "Sto analizzando i dati." #: plugin.py:92 msgid "" "takes no arguments\n" "\n" " Returns the current threads that are active.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta gli attuali thread attivi.\n" " " #: plugin.py:98 msgid "I have spawned %n; %n %b still currently active: %L." msgstr "Ho avviato %n; %n %b attualmente ancora attivi: %L." #: plugin.py:105 #, fuzzy msgid "" "takes no arguments\n" "\n" " Returns the number of processes that have been spawned, and list of\n" " ones that are still active.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta gli attuali thread attivi.\n" " " #: plugin.py:120 msgid "" "takes no arguments\n" "\n" " Returns some interesting network-related statistics.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta alcune statistiche interessanti riguardanti la rete.\n" " " #: plugin.py:128 msgid "an indeterminate amount of time" msgstr "una quantità di tempo indeterminato" #: plugin.py:129 msgid "" "I have received %s messages for a total of %S. I have sent %s messages for " "a total of %S. I have been connected to %s for %s." msgstr "" "Ho ricevuto %s messaggi per un totale di %S. Ho inviato %s messaggi per un " "totale di %S. Sono connesso a %s da %s." #: plugin.py:138 msgid "" "takes no arguments\n" "\n" " Returns some interesting CPU-related statistics on the bot.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta alcune statistiche interessanti riguardanti la CPU.\n" " " #: plugin.py:148 msgid "" "My children have taken %.2f seconds of user time and %.2f seconds of system " "time for a total of %.2f seconds of CPU time." msgstr "" "I miei processi figli hanno impiegato %.2f secondi di tempo a livello utente " "e %.2f secondi a livello di sistema per un totale di %.2f secondi di tempo " "di CPU." #: plugin.py:155 msgid "" "I have taken %.2f seconds of user time and %.2f seconds of system time, for " "a total of %.2f seconds of CPU time. %s" msgstr "" "Ho impiegato %.2f secondi di tempo a livello utente e %.2f secondi a livello " "di sistema per un totale di %.2f secondi di tempo di CPU. %s" #: plugin.py:177 msgid "Unable to run ps command." msgstr "Impossibile eseguire il comando ps." #: plugin.py:184 msgid " I'm taking up %S of memory." msgstr " Sto impiegando il %S di memoria. " #: plugin.py:187 #, fuzzy msgid " I'm taking up an unknown amount of memory." msgstr " Sto impiegando il %S di memoria. " #: plugin.py:195 msgid "" "takes no arguments\n" "\n" " Returns some interesting command-related statistics.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta alcune statistiche interessanti riguardanti i comandi.\n" " " #: plugin.py:205 msgid "I offer a total of %n in %n. I have processed %n." msgstr "Offro un totale di %n in %n. Ho elaborato %n." #: plugin.py:214 msgid "" "takes no arguments\n" "\n" " Returns a list of the commands offered by the bot.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Restituisce un elenco dei comandi offerti dal bot.\n" " " #: plugin.py:228 msgid "" "takes no arguments\n" "\n" " Returns the amount of time the bot has been running.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Riporta da quanto tempo il bot è in esecuzione.\n" " " #: plugin.py:232 msgid "I have been running for %s." msgstr "Sono in funzione da %s." #: plugin.py:239 msgid "" "takes no arguments\n" "\n" " Returns the server the bot is on.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Restituisce i server ai quali è connesso il bot.\n" " " #: plugin.py:248 msgid "" "takes no arguments\n" "\n" " Returns the network the bot is on.\n" " " msgstr "" "non necessita argomenti\n" "\n" " Restituisce la network alla quale è connesso il bot.\n" " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/plugins/Status/plugin.py0000644000175000017500000002630514535072477016751 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 sys import time import threading import multiprocessing import subprocess import supybot.conf as conf import supybot.utils as utils import supybot.world as world from supybot.commands import * import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Status') class Status(callbacks.Plugin): """This plugin allows you to view different bot statistics, for example, uptime.""" def __init__(self, irc): self.__parent = super(Status, self) self.__parent.__init__(irc) # XXX It'd be nice if these could be kept in the registry. self.sentMsgs = 0 self.recvdMsgs = 0 self.sentBytes = 0 self.recvdBytes = 0 self.connected = {} def __call__(self, irc, msg): self.recvdMsgs += 1 self.recvdBytes += len(msg) self.__parent.__call__(irc, msg) def outFilter(self, irc, msg): self.sentMsgs += 1 self.sentBytes += len(msg) return msg def do001(self, irc, msg): self.connected[irc] = time.time() @internationalizeDocstring def status(self, irc, msg, args): """takes no arguments Returns the status of the bot. """ # Initialize dictionaries nicks = {} networks = {} # Iterate through each IRC network for Irc in world.ircs: network_name = Irc.network channels = Irc.state.channels # Initialize counts for this network channel_counts = len(channels) op_counts = sum(1 for channel in channels.values() if Irc.nick in channel.ops) halfop_counts = sum(1 for channel in channels.values() if Irc.nick in channel.halfops) voice_counts = sum(1 for channel in channels.values() if Irc.nick in channel.voices) normal_counts = sum(1 for channel in channels.values() if Irc.nick in channel.users) # Store the counts in dictionaries nicks[network_name] = Irc.nick networks[network_name] = { 'Channels': channel_counts, 'Ops': op_counts, 'Half-Ops': halfop_counts, 'Voiced': voice_counts, 'Regular': normal_counts } # Prepare the response response_lines = [] for network_name, counts in networks.items(): response_lines.append( format( _('I am connected to %s as %s: Channels: %s, Ops: %s, Half-Ops: %s, Voiced: %s, Regular: %s'), network_name, nicks[network_name], counts['Channels'], counts['Ops'], counts['Half-Ops'], counts['Voiced'], counts['Regular'] ) ) if world.profiling: response_lines.append(_('I am currently in code profiling mode.')) response = format(_("%L"), response_lines) irc.reply(response) status = wrap(status) @internationalizeDocstring def threads(self, irc, msg, args): """takes no arguments Returns the current threads that are active. """ threads = [t.getName() for t in threading.enumerate()] threads.sort() s = format(_('I have spawned %n; %n %b still currently active: %L.'), (world.threadsSpawned, 'thread'), (len(threads), 'thread'), len(threads), threads) irc.reply(s) threads = wrap(threads) def processes(self, irc, msg, args): """takes no arguments Returns the number of processes that have been spawned, and list of ones that are still active. """ ps = [multiprocessing.current_process().name] ps = ps + [p.name for p in multiprocessing.active_children()] s = format('I have spawned %n; %n %b still currently active: %L.', (world.processesSpawned, 'process'), (len(ps), 'process'), len(ps), ps) irc.reply(s) processes = wrap(processes) def net(self, irc, msg, args): """takes no arguments Returns some interesting network-related statistics. """ try: elapsed = time.time() - self.connected[irc.getRealIrc()] timeElapsed = utils.timeElapsed(elapsed) except KeyError: timeElapsed = _('an indeterminate amount of time') irc.reply(format(_('I have received %s messages for a total of %S. ' 'I have sent %s messages for a total of %S. ' 'I have been connected to %s for %s.'), self.recvdMsgs, self.recvdBytes, self.sentMsgs, self.sentBytes, irc.server, timeElapsed)) net = wrap(net) @internationalizeDocstring def cpu(self, irc, msg, args): """takes no arguments Returns some interesting CPU-related statistics on the bot. """ (user, system, childUser, childSystem, elapsed) = os.times() now = time.time() target = (msg.channel, irc.network) timeRunning = now - world.startedAt if self.registryValue('cpu.children', *target) and \ user+system < timeRunning+1: # Fudge for FPU inaccuracies. children = _('My children have taken %.2f seconds of user time ' 'and %.2f seconds of system time ' 'for a total of %.2f seconds of CPU time.') % \ (childUser, childSystem, childUser+childSystem) else: children = '' activeThreads = threading.activeCount() response = _('I have taken %.2f seconds of user time and %.2f seconds ' 'of system time, for a total of %.2f seconds of CPU ' 'time. %s') % (user, system, user + system, children) if self.registryValue('cpu.threads', *target): response += format('I have spawned %n; I currently have %i still ' 'running.', (world.threadsSpawned, 'thread'), activeThreads) if self.registryValue('cpu.memory', *target): mem = None pid = os.getpid() plat = sys.platform try: if plat.startswith('linux') or plat.startswith('sunos') or \ plat.startswith('freebsd') or plat.startswith('openbsd') or \ plat.startswith('darwin'): cmd = 'ps -o rss -p %s' % pid try: inst = subprocess.Popen(cmd.split(), close_fds=True, stdin=open(os.devnull), stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: irc.error(_('Unable to run ps command.'), Raise=True) (out, foo) = inst.communicate() inst.wait() mem = int(out.splitlines()[1]) elif sys.platform.startswith('netbsd'): mem = int(os.stat('/proc/%s/mem' % pid)[7]) if mem: response += format(_(' I\'m taking up %S of memory.'), mem*1024) else: response += _(' I\'m taking up an unknown amount of memory.') except Exception: self.log.exception('Uncaught exception in cpu.memory:') irc.reply(utils.str.normalizeWhitespace(response)) cpu = wrap(cpu) @internationalizeDocstring def cmd(self, irc, msg, args): """takes no arguments Returns some interesting command-related statistics. """ commands = 0 callbacksPlugin = 0 for cb in irc.callbacks: if isinstance(cb, callbacks.Plugin): callbacksPlugin += 1 commands += len(cb.listCommands()) s = format(_('I offer a total of %n in %n. I have processed %n.'), (commands, 'command'), (callbacksPlugin, 'command-based', 'plugin'), (world.commandsProcessed, 'command')) irc.reply(s) cmd = wrap(cmd) @internationalizeDocstring def commands(self, irc, msg, args): """takes no arguments Returns a list of the commands offered by the bot. """ commands = set() for cb in irc.callbacks: if isinstance(cb, callbacks.Plugin): for command in cb.listCommands(): commands.add(command) irc.reply(format('%L', sorted(commands))) commands = wrap(commands) @internationalizeDocstring def uptime(self, irc, msg, args): """takes no arguments Returns the amount of time the bot has been running. """ response = _('I have been running for %s.') % \ utils.timeElapsed(time.time() - world.startedAt) irc.reply(response) uptime = wrap(uptime) @internationalizeDocstring def server(self, irc, msg, args): """takes no arguments Returns the server the bot is on. """ irc.reply(irc.server) server = wrap(server) @internationalizeDocstring def network(self, irc, msg, args): """takes no arguments Returns the network the bot is on. """ irc.reply(irc.network) network = wrap(network) Class = Status # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Status/test.py0000644000175000017500000000615414535072470016423 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import sys import supybot.world as world class StatusTestCase(PluginTestCase): plugins = ('Status',) def testNet(self): self.assertNotError('net') def testCpu(self): m = self.assertNotError('status cpu') self.assertNotIn('kB kB', m.args[1]) self.assertNotIn('None', m.args[1], 'None in cpu output: %r.' % m) for s in ['linux', 'freebsd', 'openbsd', 'netbsd', 'darwin']: if sys.platform.startswith(s): self.assertTrue('B' in m.args[1] or 'KB' in m.args[1] or 'MB' in m.args[1], 'No memory string on supported platform.') try: original = conf.supybot.plugins.Status.cpu.get('children')() conf.supybot.plugins.Status.cpu.get('children').setValue(False) self.assertNotRegexp('cpu', 'children') finally: conf.supybot.plugins.Status.cpu.get('children').setValue(original) def testUptime(self): self.assertNotError('uptime') def testCmd(self): self.assertNotError('cmd') def testCommands(self): self.assertNotError('commands') def testLogfilesize(self): self.feedMsg('list') self.feedMsg('list Status') self.assertNotError('upkeep') def testThreads(self): self.assertNotError('threads') def testProcesses(self): self.assertNotError('processes') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/String/0000755000175000017500000000000014535072535015051 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/__init__.py0000644000175000017500000000476714535072470017176 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ Provides various commands to manipulate characters and strings. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/config.py0000644000175000017500000000654514535072470016700 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('String') 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('String', True) String = conf.registerPlugin('String') conf.registerGroup(String, 'levenshtein') conf.registerGlobalValue(String.levenshtein, 'max', registry.PositiveInteger(256, _("""Determines the maximum size of a string given to the levenshtein command. The levenshtein command uses an O(m*n) algorithm, which means that with strings of length 256, it can take 1.5 seconds to finish; with strings of length 384, though, it can take 4 seconds to finish, and with strings of much larger lengths, it takes more and more time. Using nested commands, strings can get quite large, hence this variable, to limit the size of arguments passed to the levenshtein command."""))) conf.registerGroup(String, 're') conf.registerGlobalValue(String.re, 'timeout', registry.PositiveFloat(0.1, _("""Determines the maximum time, in seconds, that a regular expression is given to execute before being terminated. Since there is a possibility that user input for the re command can cause it to eat up large amounts of ram or cpu time, it's a good idea to keep this low. Most normal regexps should not take very long at all."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3577545 limnoria-2023.11.18/plugins/String/locales/0000755000175000017500000000000014535072535016473 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/locales/fi.po0000644000175000017500000002237514535072470017440 0ustar00valval# String plugin in Limnoria # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: String plugin for Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2014-12-20 12: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" "X-Generator: Poedit 1.6.10\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: config.py:48 msgid "" "Determines the maximum size of a string\n" " given to the levenshtein command. The levenshtein command uses an " "O(m*n)\n" " algorithm, which means that with strings of length 256, it can take 1.5\n" " seconds to finish; with strings of length 384, though, it can take 4\n" " seconds to finish, and with strings of much larger lengths, it takes " "more\n" " and more time. Using nested commands, strings can get quite large, " "hence\n" " this variable, to limit the size of arguments passed to the levenshtein\n" " command." msgstr "" "Määrittää levenshtein komentoon annettavan merkkiketjun maksimimäärän.\n" " Levenshtein komento käyttää O(m*n) algoritmiä, mikä tarkoittaa, että\n" " merkkiketju, jonka koko on 256 voi viedä 1.5\n" " sekuntia valmistumiseen; 384:n merkit piduudella se voi viedä 4\n" " sekuntia valmistumiseen, ja suuremmilla merkkiketjuilla, se vie\n" " enemmän ja enemmän aikaa. Käyttäen sisäkkäisia komentoja, merkkiketjut " "voivat tulla hyvin pitkiksi ja tästä johtuen\n" " tämä asetusarvo on olemassa, rajoittaakseen levenshtein komentoon " "annettujen parametrien määrää." #: config.py:59 msgid "" "Determines the maximum time, in seconds, that\n" " a regular expression is given to execute before being terminated. Since\n" " there is a possibility that user input for the re command can cause it " "to\n" " eat up large amounts of ram or cpu time, it's a good idea to keep this \n" " low. Most normal regexps should not take very long at all." msgstr "" "Määrittää enimmäisajan sekunteissa, joka annetaan säännöllisen lausekkeen\n" " suorittamiseksi, ennen lopetusta. Koska on mahdollista, että käyttäjän " "syöte\n" " re-komennolle voi aiheuttaa suuren RAM- tai CPU-ajan käytön, on hyvä ajatus " "pitää\n" " tämä matalana. Tavallisten säännöllisten lausekkeiden ei pitäisi kestää " "kauan." #: plugin.py:53 msgid "Provides useful commands for manipulating characters and strings." msgstr "" "Tarjoaa hyödyllisiä komentoja merkkien ja merkkiketjujen manipulointiin." #: plugin.py:55 #, fuzzy msgid "" "\n" "\n" " Returns the unicode codepoint of .\n" " " msgstr "" "\n" "\n" " Palauttaa 8-bittisen arvon.\n" " " #: plugin.py:63 #, fuzzy msgid "" "\n" "\n" " Returns the unicode character associated with codepoint \n" " " msgstr "" "\n" "\n" " Palauttaa merkin, joka on yhdistetty 8-bittisen arvon.\n" " " #: plugin.py:70 #, fuzzy msgid "That number doesn't map to a unicode character." msgstr "Tuo numero ei kartoitu 8-bittiseen merkkiin." #: plugin.py:74 msgid "" "\n" "\n" " Returns the name of the given unicode ." msgstr "" #: plugin.py:82 msgid "No name found for this character." msgstr "" #: plugin.py:86 msgid "" "\n" "\n" " Searches for a unicode character from its ." msgstr "" #: plugin.py:92 msgid "No character found with this name." msgstr "" #: plugin.py:96 msgid "" " \n" "\n" " Returns an encoded form of the given text; the valid encodings are\n" " available in the documentation of the Python codecs module:\n" " .\n" " " msgstr "" " \n" "\n" " Palauttaa salatun version annetusta tekstistä; kelvolliset " "salaukset\n" " ovat saatavilla Python codecs moduulin dokumentaatiossa:\n" " .\n" " " #: plugin.py:112 plugin.py:151 msgid "encoding" msgstr "salaus" #: plugin.py:133 msgid "" " \n" "\n" " Returns an un-encoded form of the given text; the valid encodings " "are\n" " available in the documentation of the Python codecs module:\n" " .\n" " " msgstr "" " \n" "\n" " Palauttaa salaamattoman version annetusta tekstistä; kelvolliset " "salaukset\n" " ovat saatavilla Python codecs moduulin dokumentaatiossa:\n" " .\n" " " #: plugin.py:157 msgid "base64 string" msgstr "base64 merkkiketju" #: plugin.py:158 msgid "" "Base64 strings must be a multiple of 4 in length, padded with '=' if " "necessary." msgstr "" "Base64 merkkiketjujen täytyy olla kerrollisia 4:llä, pehmustettuna '='-" "merkillä jos vaadittu." #: plugin.py:176 msgid "" " \n" "\n" " Returns the levenshtein distance (also known as the \"edit distance" "\"\n" " between and )\n" " " msgstr "" " \n" "\n" " Palauttaa levenshtein etäisyyden (tunnetaan myös nimellä \"muokkaus " "etäisyys\"\n" " ja välillä.)\n" " " #: plugin.py:183 msgid "" "Levenshtein distance is a complicated algorithm, try it with some smaller " "inputs." msgstr "" "Levenshtein etäisyys on monimutkainen algoritmi, kokeile sitä pienemmillä " "sisäänmenoilla." #: plugin.py:190 #, fuzzy msgid "" " []\n" "\n" " Returns the Soundex hash to a given length. The length defaults to\n" " 4, since that's the standard length for a soundex hash. For " "unlimited\n" " length, use 0. Maximum length 1024.\n" " " msgstr "" " []\n" "\n" " Palauttaa Soundex hashin annetulle pituudelle. Pituus on oletuksena " "4, koska se on\n" " peruspituus. Jos haluat loputtoman pituuden,\n" " käytä 0:aa.\n" " " #: plugin.py:203 msgid "" "\n" "\n" " Returns the length of .\n" " " msgstr "" "\n" "\n" " Palauttaa pituuden.\n" " " #: plugin.py:211 msgid "" " \n" "\n" " If is of the form m/regexp/flags, returns the portion of\n" " that matches the regexp. If is of the form\n" " s/regexp/replacement/flags, returns the result of applying such a\n" " regexp to .\n" " " msgstr "" " \n" "\n" " Jos on yksi muodosta m/säännöllinen lauseke/" "liput, palauta\n" " osa, joka täsmää säännölliseen lausekkeeseen. Jos " " on muotoa\n" " s/säännöllinen lauseke/korvaus/liput, palauttaa sellaisen " "säännöllisen lausekkeen käytön\n" " .\n" " " #: plugin.py:219 msgid "You probably don't want to match the empty string." msgstr "Et luultavasti halua täsmätä tyhjään merkkiketjuun." #: plugin.py:236 #, fuzzy msgid "" " \n" "\n" " Returns XOR-encrypted with .\n" " " msgstr "" "\n" "\n" " Palauttaa pituuden.\n" " " #: plugin.py:246 #, fuzzy msgid "" "\n" "\n" " Returns the md5 hash of a given string.\n" " " msgstr "" "\n" "\n" " Palauttaa pituuden.\n" " " #: plugin.py:254 #, fuzzy msgid "" "\n" "\n" " Returns the SHA1 hash of a given string.\n" " " msgstr "" "\n" "\n" " Palauttaa pituuden.\n" " " #~ msgid "" #~ " \n" #~ "\n" #~ " Returns XOR-encrypted with . See\n" #~ " http://www.yoe.org/developer/xor.html for information about XOR\n" #~ " encryption.\n" #~ " " #~ msgstr "" #~ " \n" #~ "\n" #~ " Palauttaa XOR-salattuna . Katso\n" #~ " http://www.yoe.org/developer/xor.html saadaksesi lisätietoja XOR\n" #~ " salauksesta.\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the md5 hash of a given string. Read\n" #~ " http://www.rsasecurity.com/rsalabs/faq/3-6-6.html for more " #~ "information\n" #~ " about md5.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa md5 hashin annetusta merkkiketjusta. Lue\n" #~ " http://www.rsasecurity.com/rsalabs/faq/3-6-6.html saadaksesi " #~ "lisätietoja\n" #~ " md5:stä.\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the SHA hash of a given string. Read\n" #~ " http://www.secure-hash-algorithm-md5-sha-1.co.uk/ for more " #~ "information\n" #~ " about SHA.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Palauttaa SHA hashin annetulle merkkiketjulle. Lue\n" #~ " http://www.secure-hash-algorithm-md5-sha-1.co.uk/ saadaksesi\n" #~ " lisätietoa SHA:sta.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/locales/fr.po0000644000175000017500000002111414535072470017437 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Limnoria \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: ASCII\n" "X-Generator: Poedit 1.5.4\n" #: config.py:48 msgid "" "Determines the maximum size of a string\n" " given to the levenshtein command. The levenshtein command uses an " "O(m*n)\n" " algorithm, which means that with strings of length 256, it can take 1.5\n" " seconds to finish; with strings of length 384, though, it can take 4\n" " seconds to finish, and with strings of much larger lengths, it takes " "more\n" " and more time. Using nested commands, strings can get quite large, " "hence\n" " this variable, to limit the size of arguments passed to the levenshtein\n" " command." msgstr "" "Détermine la taille maximum d'une chaîne donnée à la commande 'levenshtein'. " "Cette commande utiliser un algorithme O(m*n), ce qui signifie que pour une " "chaîne de taille 256, il peut prendre 1,5 seconde à se finir ; pour les " "chaînes de taille 384, il peut en prendre 4, et ainsi de suite. En utilisant " "des commandes inmbriquées, les chaînes peuvent devenir très grandes, donc, " "utilisez cette variable pour limiter la taille des arguments passés à la " "commande 'levenshtein'." #: config.py:59 msgid "" "Determines the maximum time, in seconds, that\n" " a regular expression is given to execute before being terminated. Since\n" " there is a possibility that user input for the re command can cause it " "to\n" " eat up large amounts of ram or cpu time, it's a good idea to keep this \n" " low. Most normal regexps should not take very long at all." msgstr "" "Détermine le temps maximum, en seconde, que peut prendre l’exécution d’une " "expression rationnelle avant qu’elle ne soit terminée. Étant donné qu’il est " "possible aux utilisateurs de donner des paramètres à la commande « re » qui " "font qu’elle utilise beaucoup de RAM ou de temps CPU, c’est une bonne idée " "de garder ce temps assez bas. L’exécution de la plupart des expressions " "rationnelles normales n’est pas longue du tout." #: plugin.py:53 msgid "Provides useful commands for manipulating characters and strings." msgstr "" #: plugin.py:55 #, fuzzy msgid "" "\n" "\n" " Returns the unicode codepoint of .\n" " " msgstr "" "\n" "\n" "Retourne la valeur 8 bits de la ." #: plugin.py:63 #, fuzzy msgid "" "\n" "\n" " Returns the unicode character associated with codepoint \n" " " msgstr "" "\n" "\n" "Retourne le caractère associé à la valeur 8-bits du " #: plugin.py:70 #, fuzzy msgid "That number doesn't map to a unicode character." msgstr "Ce nombre ne correspond pas à un caractère 8 bits." #: plugin.py:74 msgid "" "\n" "\n" " Returns the name of the given unicode ." msgstr "" #: plugin.py:82 msgid "No name found for this character." msgstr "" #: plugin.py:86 msgid "" "\n" "\n" " Searches for a unicode character from its ." msgstr "" #: plugin.py:92 msgid "No character found with this name." msgstr "" #: plugin.py:96 msgid "" " \n" "\n" " Returns an encoded form of the given text; the valid encodings are\n" " available in the documentation of the Python codecs module:\n" " .\n" " " msgstr "" " \n" "\n" "Retourne la forme encodée du texte donnée ; les encodages valides sont " "disponibles dans la documentation du module codec de Python : http://docs." "python.org/library/codecs.html#standard-encodings" #: plugin.py:112 plugin.py:151 msgid "encoding" msgstr "encodage" #: plugin.py:133 msgid "" " \n" "\n" " Returns an un-encoded form of the given text; the valid encodings " "are\n" " available in the documentation of the Python codecs module:\n" " .\n" " " msgstr "" " \n" "\n" "Retourne la forme désencodée du texte donnée ; les encodages valides sont " "disponibles dans la documentation du module codec de Python : http://docs." "python.org/library/codecs.html#standard-encodings" #: plugin.py:157 msgid "base64 string" msgstr "chaîne base64" #: plugin.py:158 msgid "" "Base64 strings must be a multiple of 4 in length, padded with '=' if " "necessary." msgstr "" "Les chaînes base64 doivent avoir une longueur multiple de 4, entourées de " "'=' si nécessaire." #: plugin.py:176 msgid "" " \n" "\n" " Returns the levenshtein distance (also known as the \"edit distance" "\"\n" " between and )\n" " " msgstr "" " \n" "\n" "Retourne la distance levenhtein (aussi connue sous le nom de \"distance " "d'édition) entre les deux chaînes." #: plugin.py:183 msgid "" "Levenshtein distance is a complicated algorithm, try it with some smaller " "inputs." msgstr "" "La distance levenshtein est un algorithme compliqué ; essayez avec des " "données plus petites." #: plugin.py:190 msgid "" " []\n" "\n" " Returns the Soundex hash to a given length. The length defaults to\n" " 4, since that's the standard length for a soundex hash. For " "unlimited\n" " length, use 0. Maximum length 1024.\n" " " msgstr "" " []\n" "\n" "Retourne un hash Soundex de la longueur donnée. La longueur par défaut est " "de 4, c'est le standard pour un hash soundex. Pour une longueur illimitée, " "utilisez 0. La longueur maximum est 1024." #: plugin.py:203 msgid "" "\n" "\n" " Returns the length of .\n" " " msgstr "" "\n" "\n" "Retourne la longueur du ." #: plugin.py:211 msgid "" " \n" "\n" " If is of the form m/regexp/flags, returns the portion of\n" " that matches the regexp. If is of the form\n" " s/regexp/replacement/flags, returns the result of applying such a\n" " regexp to .\n" " " msgstr "" " \n" "\n" "Si l' est de la forme de m/regexp/flags, retourne la " "portion du correspondant à l'expression régulière. Si l' est de la forme de s/regexp/remplacement/flags, retourne le " "résultat de l'application de l'expression régulière sur le ." #: plugin.py:219 msgid "You probably don't want to match the empty string." msgstr "" "Vous ne voulez probablement appliquer une expression régulière sur une " "chaîne vide." #: plugin.py:236 #, fuzzy msgid "" " \n" "\n" " Returns XOR-encrypted with .\n" " " msgstr "" "\n" "\n" "Retourne la longueur du ." #: plugin.py:246 #, fuzzy msgid "" "\n" "\n" " Returns the md5 hash of a given string.\n" " " msgstr "" "\n" "\n" "Retourne la longueur du ." #: plugin.py:254 #, fuzzy msgid "" "\n" "\n" " Returns the SHA1 hash of a given string.\n" " " msgstr "" "\n" "\n" "Retourne la longueur du ." #~ msgid "" #~ " \n" #~ "\n" #~ " Returns XOR-encrypted with . See\n" #~ " http://www.yoe.org/developer/xor.html for information about XOR\n" #~ " encryption.\n" #~ " " #~ msgstr "" #~ " \n" #~ "\n" #~ "Retourne le , encrypté par la méthode XOR avec le . " #~ "Lisez http://www.yoe.org/developer/xor.html pour plus d'informations sur " #~ "le cryptage XOR." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the md5 hash of a given string. Read\n" #~ " http://www.rsasecurity.com/rsalabs/faq/3-6-6.html for more " #~ "information\n" #~ " about md5.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne le hash md5 de la chaîne donnée. Lisez http://www.rsasecurity." #~ "com/rsalabs/faq/3-6-6.html pour plus d'informations à propos de md5." #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the SHA hash of a given string. Read\n" #~ " http://www.secure-hash-algorithm-md5-sha-1.co.uk/ for more " #~ "information\n" #~ " about SHA.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ "Retourne le hash SDA de la chaîne donnée. Lisez http://www.secure-hash-" #~ "algorithm-md5-sha-1.co.uk/ pour plus d'informations à propos de SHA." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/locales/it.po0000644000175000017500000002055514535072470017454 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-07-07 11:35+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:48 msgid "" "Determines the maximum size of a string\n" " given to the levenshtein command. The levenshtein command uses an " "O(m*n)\n" " algorithm, which means that with strings of length 256, it can take 1.5\n" " seconds to finish; with strings of length 384, though, it can take 4\n" " seconds to finish, and with strings of much larger lengths, it takes " "more\n" " and more time. Using nested commands, strings can get quite large, " "hence\n" " this variable, to limit the size of arguments passed to the levenshtein\n" " command." msgstr "" "Determina la dimensione massima di una stringa passata al comando " "\"levenshtein\".\n" " Quest'ultimo utilizza un algoritmo O(m*n), significa che con stringhe di " "lunghezza\n" " 256, può impiegare 1.5 secondi a terminare; con stringhe lunghe 384, " "sebbene,\n" " impiega 4 secondi e con stringhe più lunghe impiega molto più tempo. " "Utilizzando\n" " comandi nidificati le stringhe possono essere abbastanza grosse; questa " "variabile\n" " esiste per limitare la dimensione degli argomenti passati al comando " "\"levenshtein\"." #: config.py:59 msgid "" "Determines the maximum time, in seconds, that\n" " a regular expression is given to execute before being terminated. Since\n" " there is a possibility that user input for the re command can cause it " "to\n" " eat up large amounts of ram or cpu time, it's a good idea to keep this \n" " low. Most normal regexps should not take very long at all." msgstr "" #: plugin.py:53 msgid "Provides useful commands for manipulating characters and strings." msgstr "" #: plugin.py:55 #, fuzzy msgid "" "\n" "\n" " Returns the unicode codepoint of .\n" " " msgstr "" "\n" "\n" " Restituisce il valore 8-bit di .\n" " " #: plugin.py:63 #, fuzzy msgid "" "\n" "\n" " Returns the unicode character associated with codepoint \n" " " msgstr "" "\n" "\n" " Restituisce il carattere associato al valore 8-bit di \n" " " #: plugin.py:70 #, fuzzy msgid "That number doesn't map to a unicode character." msgstr "Questo numero non corrisponde a un carattere 8-bit." #: plugin.py:74 msgid "" "\n" "\n" " Returns the name of the given unicode ." msgstr "" #: plugin.py:82 msgid "No name found for this character." msgstr "" #: plugin.py:86 msgid "" "\n" "\n" " Searches for a unicode character from its ." msgstr "" #: plugin.py:92 msgid "No character found with this name." msgstr "" #: plugin.py:96 msgid "" " \n" "\n" " Returns an encoded form of the given text; the valid encodings are\n" " available in the documentation of the Python codecs module:\n" " .\n" " " msgstr "" " \n" "\n" " Restituisce la forma codificata del testo specificato; le codifiche\n" " valide sono disponibili nella documentazione del modulo Python " "codecs:\n" " .\n" " " #: plugin.py:112 plugin.py:151 msgid "encoding" msgstr "codifica" #: plugin.py:133 msgid "" " \n" "\n" " Returns an un-encoded form of the given text; the valid encodings " "are\n" " available in the documentation of the Python codecs module:\n" " .\n" " " msgstr "" " \n" "\n" " Restituisce la forma non codificata del testo specificato; le " "codifiche\n" " valide sono disponibili nella documentazione del modulo Python " "codecs:\n" " .\n" " " #: plugin.py:157 msgid "base64 string" msgstr "stringa base64" #: plugin.py:158 msgid "" "Base64 strings must be a multiple of 4 in length, padded with '=' if " "necessary." msgstr "" "Le stringhe in base64 devono avere una lunghezza in multipli di 4, " "circondate da \"=\" se necessario." #: plugin.py:176 msgid "" " \n" "\n" " Returns the levenshtein distance (also known as the \"edit distance" "\"\n" " between and )\n" " " msgstr "" " \n" "\n" " Riporta la distanza levenshtein (anche conosciuta come \"distanza " "di\n" " modifica\" tra e )\n" " " #: plugin.py:183 msgid "" "Levenshtein distance is a complicated algorithm, try it with some smaller " "inputs." msgstr "" "La distanza levenshtein è un algoritmo complesso, prova a inserire meno dati." #: plugin.py:190 #, fuzzy msgid "" " []\n" "\n" " Returns the Soundex hash to a given length. The length defaults to\n" " 4, since that's the standard length for a soundex hash. For " "unlimited\n" " length, use 0. Maximum length 1024.\n" " " msgstr "" " []\n" "\n" " Restituisce l'hash Soundex alla lunghezza specificata. In modo " "predefinito\n" " è impostata a 4, giacché è la lunghezza standard per questo tipo di " "hash.\n" " Per lunghezze illimitate usa 0.\n" " " #: plugin.py:203 msgid "" "\n" "\n" " Returns the length of .\n" " " msgstr "" "\n" "\n" " Riporta la lunghezza di .\n" " " #: plugin.py:211 msgid "" " \n" "\n" " If is of the form m/regexp/flags, returns the portion of\n" " that matches the regexp. If is of the form\n" " s/regexp/replacement/flags, returns the result of applying such a\n" " regexp to .\n" " " msgstr "" " \n" "\n" " Se è nella forma m/regexp/flags, restituisce la porzione\n" " di che corrisponde alla regexp. Se non lo è, restituisce\n" " il risultato dell'applicare la regexp a .\n" " " #: plugin.py:219 msgid "You probably don't want to match the empty string." msgstr "È probabile che tu non voglia confrontare una stringa vuota." #: plugin.py:236 #, fuzzy msgid "" " \n" "\n" " Returns XOR-encrypted with .\n" " " msgstr "" "\n" "\n" " Riporta la lunghezza di .\n" " " #: plugin.py:246 #, fuzzy msgid "" "\n" "\n" " Returns the md5 hash of a given string.\n" " " msgstr "" "\n" "\n" " Riporta la lunghezza di .\n" " " #: plugin.py:254 #, fuzzy msgid "" "\n" "\n" " Returns the SHA1 hash of a given string.\n" " " msgstr "" "\n" "\n" " Riporta la lunghezza di .\n" " " #~ msgid "" #~ " \n" #~ "\n" #~ " Returns XOR-encrypted with . See\n" #~ " http://www.yoe.org/developer/xor.html for information about XOR\n" #~ " encryption.\n" #~ " " #~ msgstr "" #~ " \n" #~ "\n" #~ " Restituisce cifrato con XOR con . Vedi\n" #~ " http://www.yoe.org/developer/xor.html per informazioni sulla " #~ "cifratura XOR.\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the md5 hash of a given string. Read\n" #~ " http://www.rsasecurity.com/rsalabs/faq/3-6-6.html for more " #~ "information\n" #~ " about md5.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce l'hash md5 di una data stringa. Vedi\n" #~ " http://www.rsasecurity.com/rsalabs/faq/3-6-6.html per ulteriori " #~ "informazioni su md5.\n" #~ " " #~ msgid "" #~ "\n" #~ "\n" #~ " Returns the SHA hash of a given string. Read\n" #~ " http://www.secure-hash-algorithm-md5-sha-1.co.uk/ for more " #~ "information\n" #~ " about SHA.\n" #~ " " #~ msgstr "" #~ "\n" #~ "\n" #~ " Restituisce l'hash SHA di una data stringa. Vedi\n" #~ " http://www.secure-hash-algorithm-md5-sha-1.co.uk/ per ulteriori " #~ "informazioni su SHA.\n" #~ " " ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/plugin.py0000644000175000017500000002326714535072470016731 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2008-2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import sys import types import codecs import base64 import binascii import unicodedata 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 _ = PluginInternationalization('String') import multiprocessing class String(callbacks.Plugin): """Provides useful commands for manipulating characters and strings.""" def ord(self, irc, msg, args, s): """ Returns the unicode codepoint of characters in . """ irc.replies([str(ord(char)) for char in s]) ord = wrap(ord, ['text']) def chr(self, irc, msg, args, i): """ Returns the unicode character associated with codepoint """ try: irc.reply(chr(i), stripCtcp=False) except ValueError: irc.error(_('That number doesn\'t map to a unicode character.')) chr = wrap(chr, ['int']) def unicodename(self, irc, msg, args, s): """ Returns the name of characters in . This will error if any character is not a valid Unicode character.""" replies = [] for idx, char in enumerate(s): try: replies.append(unicodedata.name(char)) except ValueError: irc.error(_('No name found for character %r at position %d.') % (char, idx), Raise=True) irc.replies(replies) unicodename = wrap(unicodename, ['text']) def unicodesearch(self, irc, msg, args, name): """ Searches for a unicode character from its .""" try: irc.reply(unicodedata.lookup(name)) except KeyError: irc.error(_('No character found with this name.')) unicodesearch = wrap(unicodesearch, ['text']) def encode(self, irc, msg, args, encoding, text): """ Returns an encoded form of the given text; the valid encodings are available in the documentation of the Python codecs module: . """ # Binary codecs are prefixed with _codec in Python 3 if encoding in 'base64 bz2 hex quopri uu zlib': encoding += '_codec' if encoding.endswith('_codec'): text = text.encode() # Do the encoding try: encoder = codecs.getencoder(encoding) except LookupError: irc.errorInvalid(_('encoding'), encoding) text = encoder(text)[0] # If this is a binary codec, re-encode it with base64 if encoding.endswith('_codec') and encoding != 'base64_codec': text = codecs.getencoder('base64_codec')(text)[0].decode() # Change result into a string if minisix.PY2 and isinstance(text, unicode): text = text.encode('utf-8') elif minisix.PY3 and isinstance(text, bytes): text = text.decode() if encoding in ('base64', 'base64_codec'): text = text.replace('\n', '') # Reply irc.reply(text.rstrip('\n')) encode = wrap(encode, ['something', 'text']) def decode(self, irc, msg, args, encoding, text): """ Returns an un-encoded form of the given text; the valid encodings are available in the documentation of the Python codecs module: . """ # Binary codecs are prefixed with _codec in Python 3 if encoding in 'base64 bz2 hex quopri uu zlib': encoding += '_codec' # If this is a binary codec, pre-decode it with base64 if encoding.endswith('_codec') and encoding != 'base64_codec': text = codecs.getdecoder('base64_codec')(text.encode())[0] # Do the decoding try: decoder = codecs.getdecoder(encoding) except LookupError: irc.errorInvalid(_('encoding'), encoding) if minisix.PY3 and not isinstance(text, bytes): text = text.encode() try: text = decoder(text)[0] except binascii.Error: irc.errorInvalid(_('base64 string'), s=_('Base64 strings must be a multiple of 4 in ' 'length, padded with \'=\' if necessary.')) return # Change result into a string if minisix.PY2 and isinstance(text, unicode): text = text.encode('utf-8') elif minisix.PY3 and isinstance(text, bytes): try: text = text.decode() except UnicodeDecodeError: pass # Reply irc.reply(text) decode = wrap(decode, ['something', 'text']) def levenshtein(self, irc, msg, args, s1, s2): """ Returns the levenshtein distance (also known as the "edit distance" between and ) """ max = self.registryValue('levenshtein.max') if len(s1) > max or len(s2) > max: irc.error(_('Levenshtein distance is a complicated algorithm, try ' 'it with some smaller inputs.')) else: irc.reply(str(utils.str.distance(s1, s2))) levenshtein = thread(wrap(levenshtein, ['something', 'text'])) def soundex(self, irc, msg, args, text, length): """ [] Returns the Soundex hash to a given length. The length defaults to 4, since that's the standard length for a soundex hash. For unlimited length, use 0. Maximum length 1024. """ if length > 1024: irc.error("Maximum allowed length is 1024.") return irc.reply(utils.str.soundex(text, length)) soundex = wrap(soundex, ['somethingWithoutSpaces', additional('int', 4)]) def len(self, irc, msg, args, text): """ Returns the length of . """ irc.reply(str(len(text))) len = wrap(len, ['text']) def re(self, irc, msg, args, f, text): """ If is of the form m/regexp/flags, returns the portion of that matches the regexp. If is of the form s/regexp/replacement/flags, returns the result of applying such a regexp to . """ try: if f('') and len(f(' ')) > len(f(''))+1: # Matches the empty string. s = _('You probably don\'t want to match the empty string.') irc.error(s) else: t = self.registryValue('re.timeout') try: v = process(f, text, timeout=t, pn=self.name(), cn='re') if isinstance(v, list): v = format('%L', v) irc.reply(v) except commands.ProcessTimeoutError as e: irc.error("ProcessTimeoutError: %s" % (e,)) except re.error as e: irc.error(e.args[0]) re = thread(wrap(re, [first('regexpMatcherMany', 'regexpReplacer'), 'text'])) def xor(self, irc, msg, args, password, text): """ Returns XOR-encrypted with . """ chars = utils.iter.cycle(password) ret = [chr(ord(c) ^ ord(next(chars))) for c in text] irc.reply(''.join(ret)) xor = wrap(xor, ['something', 'text']) def md5(self, irc, msg, args, text): """ Returns the md5 hash of a given string. """ irc.reply(utils.crypt.md5(text.encode('utf8')).hexdigest()) md5 = wrap(md5, ['text']) def sha(self, irc, msg, args, text): """ Returns the SHA1 hash of a given string. """ irc.reply(utils.crypt.sha(text.encode('utf8')).hexdigest()) sha = wrap(sha, ['text']) Class = String # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/String/test.py0000644000175000017500000001664114535072470016410 0ustar00valval### # Copyright (c) 2003-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re from supybot.test import * import supybot.utils as utils nicks = ['fatjim','scn','moshez','LordVan','MetaCosm','pythong','fishfart', 'alb','d0rt','jemfinch','StyxAlso','fors','deltab','gd', 'hellz_hunter','are_j|pub_comp','jason_','dreid','sayke_','winjer', 'TenOfTen','GoNoVas','queuetue','the|zzz','Hellfried','Therion', 'shro','DaCa','rexec','polin8','r0ky','aaron_','ironfroggy','eugene', 'faassen','tirloni','mackstann','Yhg1s','ElBarono','vegai','shang', 'typo_','kikoforgetme','asqui','TazyTiggy','fab','nixman','liiwi', 'AdamV','paolo','red_one','_AleX_','lament','jamessan','supybot', 'macr0_zzz','plaisthos','redghost','disco','mphardy','gt3','mathie', 'jonez','r0ky-office','tic','d33p','ES3merge','talin','af','flippo', 'sholden','ameoba','shepherg','j2','Acapnotic','dash','merlin262', 'Taaus','_moshez','rik','jafo__','blk-majik','JT__','itamar', 'kermit-','davidmccabe','glyph','jojo','dave_p','goo','hyjinx', 'SamB','exarkun','drewp','Ragica','skylan','redgore','k3','Ra1stlin', 'StevenK','carball','h3x','carljm','_jacob','teratorn','frangen', 'phed','datazone','Yaggo','acct_','nowhere','pyn','ThomasWaldmann', 'dunker','pilotLight','brainless','LoganH_','jmpnz','steinn', 'EliasREC','lowks__','OldSmrf','Mad77','snibril','delta','psy', 'skimpIzu','Kengur','MoonFallen','kotkis','Hyperi'] def group(seq, groupSize, noneFill=True): """Groups a given sequence into sublists of length groupSize.""" ret = [] L = [] i = groupSize for elt in seq: if i > 0: L.append(elt) else: ret.append(L) i = groupSize L = [] L.append(elt) i -= 1 if L: if noneFill: while len(L) < groupSize: L.append(None) ret.append(L) return ret class StringTestCase(PluginTestCase): plugins = ('String', 'Format', 'Status') config = { 'plugins.String.re.timeout': 2. # flaky on busy CIs otherwise } def testLen(self): self.assertResponse('len foo', '3') self.assertHelp('len') def testNoErrors(self): self.assertNotError('levenshtein Python Perl') def testSoundex(self): self.assertNotError('soundex jemfinch') self.assertNotRegexp('soundex foobar 3:30', 'ValueError') def testChr(self): for i in range(256): c = chr(i) regexp = r'%s|%s' % (re.escape(c), re.escape(repr(c))) self.assertRegexp('chr %s' % i, regexp) def testOrd(self): for c in map(chr, range(256)): i = ord(c) self.assertResponse('ord %s' % utils.str.dqrepr(c), str(i)) self.assertResponse('ord é', '233') self.assertResponse('ord 🆒', '127378') self.assertResponse('ord 🇦🇶', '127462 and 127478') def testUnicode(self): self.assertResponse('unicodename ☃', 'SNOWMAN') self.assertResponse('unicodesearch SNOWMAN', '☃') self.assertResponse('unicodename ?', 'QUESTION MARK') # multi-char strings and ZWJ sequences self.assertResponse('unicodename :O', 'COLON and LATIN CAPITAL LETTER O') self.assertResponse('unicodename 🤷‍♂️', 'SHRUG, ZERO WIDTH JOINER, MALE SIGN, and VARIATION SELECTOR-16') self.assertError('unicodename "\\uFFFF"') self.assertError('unicodename "!@#\\uFFFF$"') self.assertResponse('unicodesearch FOO', 'Error: No character found with this name.') def testMd5(self): self.assertResponse('md5 supybot', '1360578d1276e945cc235654a53f9c65') def testEncodeDecode(self): # This no longer works correctly. It almost seems like were throwing # in a repr() somewhere. s = 'the recalcitrant jamessan tests his scramble function' self.assertNotRegexp('encode aldkfja foobar', 'LookupError') self.assertNotRegexp('decode asdflkj foobar', 'LookupError') self.assertResponse('decode zlib [encode zlib %s]' % s, s) self.assertRegexp('decode base64 $BCfBg7;9D;R(B', 'padded with') def testRe(self): self.assertResponse('re "m/system time/" [status cpu]', 'system time') self.assertResponse('re s/user/luser/g user user', 'luser luser') self.assertResponse('re s/user/luser/ user user', 'luser user') self.assertNotRegexp('re m/foo/ bar', 'has no attribute') self.assertResponse('re m/a\\S+y/ "the bot angryman is hairy"', 'angry') self.assertResponse('re m/a\\S+y/g "the bot angryman is hairy"', 'angry and airy') def testReNotEmptyString(self): self.assertError('re s//foo/g blah') def testReWorksWithJustCaret(self): self.assertResponse('re s/^/foo/ bar', 'foobar') def testReNoEscapingUnpackListOfWrongSize(self): self.assertNotRegexp('re foo bar baz', 'unpack list of wrong size') def testReBug850931(self): self.assertResponse(r're s/\b(\w+)\b/\1./g foo bar baz', 'foo. bar. baz.') def testNotOverlongRe(self): self.assertError('re [strjoin "" s/./ [eval \'xxx\'*400]] blah blah') def testXor(self): # This no longer works correctly. It almost seems like were throwing # in a repr() somewhere. L = [nick for nick in nicks if '|' not in nick and '[' not in nick and ']' not in nick] for s0, s1, s2, s3, s4, s5, s6, s7, s8, s9 in group(L, 10): data = '%s%s%s%s%s%s%s%s%s' % (s0, s1, s2, s3, s4, s5, s6, s7, s8) self.assertResponse('xor %s [xor %s %s]' % (s9, s9, data), data) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3617547 limnoria-2023.11.18/plugins/Success/0000755000175000017500000000000014535072535015213 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/__init__.py0000644000175000017500000000512614535072470017326 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, 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. ### """ The Success plugin spices up success replies by allowing custom messages instead of the default 'The operation succeeded.' message; like Dunno does for "no such command" replies. """ import supybot import supybot.world as world # Use this for the version of this plugin. You may wish to put a CVS keyword # in here if you're keeping the plugin in CVS or some similar system. __version__ = "" __author__ = supybot.authors.strike __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {} from . import config from . import plugin from importlib 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=8 expandtab textwidth=78: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/config.py0000644000175000017500000000527714535072470017043 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Success') 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('Success', True) Success = conf.registerPlugin('Success') # This is where your configuration variables (if any) should go. For example: # conf.registerGlobalValue(Success, 'someConfigVariableName', # registry.Boolean(False, _("""Help for someConfigVariableName."""))) conf.registerChannelValue(conf.supybot.plugins.Success, 'prefixNick', registry.Boolean(True, _("""Determines whether the bot will prefix the nick of the user giving an invalid command to the success response."""))) # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3617547 limnoria-2023.11.18/plugins/Success/locales/0000755000175000017500000000000014535072535016635 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/locales/fi.po0000644000175000017500000000305214535072470017571 0ustar00valval# Success plugin in Limnoria. # Copyright (C) 2011 Limnoria # Mikaela Suomalainen , 2011. # msgid "" msgstr "" "Project-Id-Version: \n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-10-25 15:18+0200\n" "Last-Translator: Mikaela Suomalainen \n" "Language-Team: \n" "Language: fi_FI\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: config.py:50 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the success response." msgstr "" "Määrittää lisääkö botti käyttäjän nimimerkin lähettämänsä viestin alkuun, " "jos\n" " käyttäjä antaa viallisen komennon onnistumisvastaukselle." #: plugin.py:39 msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'The operation succeeded.' messages\n" " with messages kept in a database, able to give more personable\n" " responses." msgstr "" "Tämä lisäosa kirjoitettiin alunperin toimimaan MoobotFactoids lisäosan " "kanssa, molemmat kaksi\n" " tarjoaisivat samankaltainen-kuin-moobot-ja-blootbot käyttöliittymän " "factoideille.\n" " Perusteellisesti, se korvaa perus 'Tehtävä suoritettu.' viestit\n" " vastauksilla tietokannoista, antaen mahdollisuuden luoda " "persoonallisempia\n" " vastauksia." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/locales/fr.po0000644000175000017500000000263314535072470017606 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: \n" "Last-Translator: Valentin Lorentz \n" "Language-Team: Limnoria \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Français\n" "X-Poedit-Country: France\n" "X-Poedit-SourceCharset: ASCII\n" #: config.py:50 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the success response." msgstr "" "Détermine si le bot utilisera le nick de l'utilisateur donnant une commande " "invalide comme préfixe pour la réponse de Success." #: plugin.py:39 msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'The operation succeeded.' messages\n" " with messages kept in a database, able to give more personable\n" " responses." msgstr "" "Ce plugin était à l'origine écrit pour fonctionner avec MoobotFactoids, qui " "fournit une interface similaire à celle de moobot et blootbot pour les " "factoids. Il remplacement le message 'the operation succeeded.' par un " "message stoché dans la base de données, ce qui vous permet d'en avoir un " "plus personnalisable." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/locales/it.po0000644000175000017500000000245314535072470017613 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Limnoria\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2011-06-12 18: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:50 msgid "" "Determines whether the bot will prefix the nick\n" " of the user giving an invalid command to the success response." msgstr "" "Determina se il bot userà il nick dell'utente che dà un comando non\n" " valido come prefisso per la risposta di successo." #: plugin.py:39 msgid "" "This plugin was written initially to work with MoobotFactoids, the two\n" " of them to provide a similar-to-moobot-and-blootbot interface for " "factoids.\n" " Basically, it replaces the standard 'The operation succeeded.' messages\n" " with messages kept in a database, able to give more personable\n" " responses." msgstr "" "Questo plugin fu inizialmente scritto per funzionare con MoobotFactoids, " "per\n" " fornire un'interfaccia simile ai factoid di moobot e blootbot.\n" " Sostituisce i messaggi standard \"L'operazione è riuscita.\"\n" " con quelli contenuti in un database, in modo da rendere le risposte più " "piacevoli." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/plugin.py0000644000175000017500000000661714535072470017073 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Success') class Success(plugins.ChannelIdDatabasePlugin): """This plugin was written initially to work with MoobotFactoids, the two of them to provide a similar-to-moobot-and-blootbot interface for factoids. Basically, it replaces the standard 'The operation succeeded.' messages with messages kept in a database, able to give more personable responses.""" def __init__(self, irc): self.__parent = super(Success, self) self.__parent.__init__(irc) self.target = None pluginSelf = self self.originalClass = conf.supybot.replies.success.__class__ class MySuccessClass(self.originalClass): __slots__ = () def __call__(self): msg = dynamic.msg ret = pluginSelf.db.random(msg.channel or msg.args[0]) if ret is None: try: self.__class__ = pluginSelf.originalClass ret = self() finally: self.__class__ = MySuccessClass else: ret = ret.text return ret def get(self, attr): if irc.isChannel(attr): pluginSelf.target = attr return self conf.supybot.replies.success.__class__ = MySuccessClass def die(self): self.__parent.die() conf.supybot.replies.success.__class__ = self.originalClass Success = internationalizeDocstring(Success) Class = Success # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Success/test.py0000644000175000017500000000434014535072470016543 0ustar00valval### # Copyright (c) 2005, Daniel DiPaolo # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf from supybot.test import * class SuccessTestCase(ChannelPluginTestCase): plugins = ('Success', 'User') def setUp(self): ChannelPluginTestCase.setUp(self) self.prefix = 'mf2!bar@baz' self.irc.feedMsg(ircmsgs.privmsg(self.nick, 'register tester moo', prefix=self.prefix)) def testSuccess(self): self.assertResponse('success add success1:', 'The operation succeeded.') self.assertResponse('success add success2:', 'success1: Success #1 added.') # vim:set shiftwidth=4 softtabstop=8 expandtab textwidth=78: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3617547 limnoria-2023.11.18/plugins/Time/0000755000175000017500000000000014535072535014501 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Time/__init__.py0000644000175000017500000000516114535072470016613 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ A plugin for time-related functions. """ 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 __maintainer__ = supybot.authors.limnoria_core # This is a dictionary mapping supybot.Author instances to lists of # contributions. __contributors__ = {supybot.authors.progval: ['tztime'], supybot.Author('Ken Spencer', 'kspencer', 'iota@electrocode.net'): ['ddate']} from . import config from . import plugin from importlib 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Time/config.py0000644000175000017500000000512014535072470016314 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot.conf as conf import supybot.registry as registry from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Time') 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('Time', True) Time = conf.registerPlugin('Time') conf.registerChannelValue(Time, 'format', registry.String(str(conf.supybot.reply.format.time()), _("""Determines the format string for timestamps. Refer to the Python documentation for the time module to see what formats are accepted. If you set this variable to the empty string, the timestamp will not be shown."""))) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3617547 limnoria-2023.11.18/plugins/Time/locales/0000755000175000017500000000000014535072535016123 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/plugins/Time/locales/de.po0000644000175000017500000001255014535072470017054 0ustar00valvalmsgid "" msgstr "" "Project-Id-Version: Supybot\n" "POT-Creation-Date: 2022-02-06 00:12+0100\n" "PO-Revision-Date: 2012-04-27 15:34+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" #: config.py:46 msgid "" "Determines the\n" " format string for timestamps. Refer to the Python documentation for " "the\n" " time module to see what formats are accepted. If you set this variable " "to\n" " the empty string, the timestamp will not be shown." msgstr "" "Legt die Formatiertung der Zeichenkette für Zeitstempel fest. Siehe Python " "Dokumentation für das time Modul um zu sehen welche Formate akzeptiert " "werden. Falls du diese Variable auf eine leere Zeichenkette setzt, wird kein " "Zeitstempel angezeigt." #: plugin.py:75 msgid "This plugin allows you to use different time-related functions." msgstr "" #: plugin.py:78 msgid "" "[y] [w] [d] [h] [m] [s]\n" "\n" " Returns the number of seconds in the number of , ,\n" " , , , and given. An example usage " "is\n" " \"seconds 2h 30m\", which would return 9000, which is '3600*2 + " "30*60'.\n" " Useful for scheduling events at a given number of seconds in the\n" " future.\n" " " msgstr "" "[y] [w] [d] [h] [m] [s]\n" "\n" "Gibt die Sekunden von der Anzahl von , , , , " " und Sekunden an. Ein Beispiel ist \"seconds 2h 30m\", das würde " "9000 zurückgeben, dass ist '3600*2 + 30*60'. Nützlich um Events zu einem " "Zeitpunkt von Sekunden in der Zukunft zu planen." #: plugin.py:113 #, fuzzy msgid "" "[
  • '.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[1:]) # strip leading / 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) class SupyWellKnown(SupyHTTPServerCallback): """Serves /.well-known/ resources.""" name = 'well-known' defaultResponse = _('Request not handled') public = False def doGetOrHead(self, handler, path, write_content): for callback in handler.server.callbacks.values(): resp = callback.doWellKnown(handler, path) if resp: (status, headers, content) = resp handler.send_response(status) for header in headers.items(): self.send_header(*header) self.end_headers() if write_content: self.wfile.write(content) return handler.send_response(404) self.end_headers() self.wfile.write(b"Error 404. There is nothing to see here.") DEFAULT_CALLBACKS = {'.well-known': SupyWellKnown()} 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 = DEFAULT_CALLBACKS.copy() 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, None) if callback: 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 = {} self.server_address = ("0.0.0.0", 0) def serve_forever(self, *args, **kwargs): pass def shutdown(self, *args, **kwargs): pass if world.testing or world.documenting: SupyHTTPServer = TestSupyHTTPServer else: SupyHTTPServer = RealSupyHTTPServer 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): try: server = SupyHTTPServer(address, protocol, SupyHTTPRequestHandler) except OSError as e: log.error( 'Failed to start HTTP server with protocol %s at address: %s', protocol, address, e) if e.args[0] == 98: log.error( 'This means the port (and address) is already in use by an ' 'other process. Either find the process using the port ' 'and stop it, or change the port configured in ' 'supybot.servers.http.port.') continue except: log.exception( "Failed to start HTTP server with protocol %s at address", protocol, address) continue 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 list(http_servers): server.unhook(subdir) if len(set(server.callbacks) - set(DEFAULT_CALLBACKS)) <= 0 \ and not configGroup.keepAlive(): server.shutdown() http_servers.remove(server) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/i18n.py0000644000175000017500000003202414535072470014041 0ustar00valval### # Copyright (c) 2010-2021, 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 NEXT_IS_FUZZY = 5 MSGID = 'msgid "' MSGSTR = 'msgstr "' FUZZY = '#, fuzzy' currentLocale = 'en' SUPPORTED_LANGUAGES = ['de', 'en', 'es', 'fi', 'fr', 'it'] class PluginNotFound(Exception): pass def getLocaleFromRegistryCache(): """Called by the 'supybot' script. Gets the locale name before conf is loaded.""" global currentLocale import supybot.registry as registry try: currentLocale = registry._cache['supybot.language'] except KeyError: pass else: reloadLocales() def import_conf(): """Imports the conf into this module""" global conf conf = __import__('supybot.conf').conf class Languages(conf.registry.OnlySomeStrings): validStrings = SUPPORTED_LANGUAGES errormsg = 'Value should be a supported language (%s), not %%r' % ( ', '.join(validStrings)) conf.registerGlobalValue(conf.supybot, 'language', Languages(currentLocale, """Determines the bot's default language if translations exist. Currently supported are: %s""" % ', '.join(Languages.validStrings))) 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() def reloadLocalesIfRequired(): global currentLocale if conf is None: return if currentLocale != conf.supybot.language(): currentLocale = conf.supybot.language() reloadLocales() def reloadLocales(): import supybot.utils as utils for pluginClass in i18nClasses.values(): pluginClass.loadLocale() for command in list(internationalizedCommands.values()): internationalizeDocstring(command) utils.str._relocalizeFunctions(PluginInternationalization()) 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 step == WAITING_FOR_MSGID and line.startswith(FUZZY): step = NEXT_IS_FUZZY elif step == NEXT_IS_FUZZY and line.startswith(MSGID): # Don't use fuzzy strings; they may have a mismatched number of %s or be # outright wrong; use English instead. step = WAITING_FOR_MSGID elif 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 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] 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 def _install(): from . import utils _ = PluginInternationalization() utils.gen._ = _ utils.str._ = _ utils.str._relocalizeFunctions(_) _install() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/ircdb.py0000644000175000017500000014527314535072470014360 0ustar00valval### # Copyright (c) 2002-2009, Jeremiah Fincher # Copyright (c) 2009,2013, James McCoy # Copyright (c) 2010-2021, 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 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.""" __slots__ = ('__parent',) 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.""" __slots__ = ('__parent',) 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.""" __slots__ = ('id', 'auth', 'name', 'ignore', 'secure', 'hashed', 'password', 'capabilities', 'hostmasks', 'nicks', 'gpgkeys') 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) # hostmasks used for recognition self.hostmasks = ircutils.HostmaskSet(hostmasks or []) if nicks is None: # {'network1': ['foo', 'bar'], 'network': ['baz']} self.nicks = ircutils.IrcDict() 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 password is None, it will be disabled.""" if hashed or self.hashed: self.hashed = True if password is None: self.password = "" else: self.password = utils.saltHash(password) else: self.password = password def checkPassword(self, password): """Checks the user's password.""" if password is None or not self.password: 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()) matched_pattern = self.hostmasks.match(hostmask) if matched_pattern is not None: return matched_pattern 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.""" __slots__ = ('defaultAllow', 'expiredBans', 'bans', 'ignores', 'silences', 'exceptions', 'capabilities', 'lobotomized') 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 = ircutils.ExpiringHostmaskDict(ignores) 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 if self.ignores.match(hostmask): return True 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 IrcNetwork(object): """This class holds dynamic information about a network that should be preserved across restarts.""" __slots__ = ('stsPolicies', 'lastDisconnectTimes') def __init__(self, stsPolicies=None, lastDisconnectTimes=None): self.stsPolicies = stsPolicies or {} self.lastDisconnectTimes = lastDisconnectTimes or {} def __repr__(self): return '%s(stsPolicies=%r, lastDisconnectTimes=%s)' % \ (self.__class__.__name__, self.stsPolicies, self.lastDisconnectTimes) def addStsPolicy(self, server, port, stsPolicy): assert isinstance(port, int), repr(port) assert isinstance(stsPolicy, str), repr(stsPolicy) self.stsPolicies[server] = (port, stsPolicy) def expireStsPolicy(self, server): if server in self.stsPolicies: del self.stsPolicies[server] def addDisconnection(self, server): self.lastDisconnectTimes[server] = int(time.time()) def preserve(self, fd, indent=''): def write(s): fd.write(indent) fd.write(s) fd.write(os.linesep) for (server, (port, stsPolicy)) in sorted(self.stsPolicies.items()): assert isinstance(port, int), repr(port) assert isinstance(stsPolicy, str), repr(stsPolicy) write('stsPolicy %s %s %s' % (server, port, stsPolicy)) for (server, disconnectTime) in \ sorted(self.lastDisconnectTimes.items()): write('lastDisconnectTime %s %s' % (server, disconnectTime)) fd.write(os.linesep) class Creator(object): __slots__ = () def badCommand(self, command, rest, lineno): raise ValueError('Invalid command on line %s: %s' % (lineno, command)) class IrcUserCreator(Creator): __slots__ = ('users') 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): __slots__ = ('c', 'channels', 'hadChannel') 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 IrcNetworkCreator(Creator): __slots__ = ('net', 'networks') name = None def __init__(self, networks): self.net = IrcNetwork() self.networks = networks def network(self, rest, lineno): IrcNetworkCreator.name = rest def stspolicy(self, rest, lineno): L = rest.split() if len(L) == 2: # Old policy missing a port. Discard it return (server, policyPort, stsPolicy) = L self.net.addStsPolicy(server, int(policyPort), stsPolicy) def lastdisconnecttime(self, rest, lineno): (server, when) = rest.split() when = int(when) self.net.lastDisconnectTimes[server] = when def finish(self): if self.name: self.networks.setNetwork(self.name, self.net) self.net = IrcNetwork() class DuplicateHostmask(ValueError): pass class UsersDictionary(utils.IterableMap): """A simple serialized-to-file User Database.""" __slots__ = ('noFlush', 'filename', 'users', '_nameCache', '_hostmaskCache', 'nextId') 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.""" if self.filename is not None: self.nextId = 0 self.users.clear() self._nameCache.clear() self._hostmaskCache.clear() 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: fd = utils.file.AtomicFile(self.filename) for (id, u) in sorted(self.users.items()): 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): __slots__ = ('noFlush', 'filename', 'channels') 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 sorted(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 NetworksDictionary(utils.IterableMap): __slots__ = ('noFlush', 'filename', 'networks') def __init__(self): self.noFlush = False self.filename = None self.networks = ircutils.IrcDict() def open(self, filename): self.noFlush = True try: self.filename = filename reader = unpreserve.Reader(IrcNetworkCreator, self) try: reader.readFile(filename) self.noFlush = False self.flush() except EnvironmentError as e: log.error('Invalid network database, resetting to empty.') log.error('Exact error: %s', utils.exnToString(e)) except Exception as e: log.error('Invalid network database, resetting to empty.') log.exception('Exact error:') finally: self.noFlush = False def flush(self): """Flushes the network database to its file.""" if not self.noFlush: if self.filename is not None: fd = utils.file.AtomicFile(self.filename) for (network, net) in sorted(self.networks.items()): fd.write('network %s' % network) fd.write(os.linesep) net.preserve(fd, indent=' ') fd.close() else: log.warning('NetworksDictionary.flush without self.filename.') else: log.debug('Not flushing NetworksDictionary because of noFlush.') def close(self): self.flush() if self.flush in world.flushers: world.flushers.remove(self.flush) self.networks.clear() def reload(self): """Reloads the network database from its file.""" if self.filename is not None: self.networks.clear() try: self.open(self.filename) except EnvironmentError as e: log.warning('NetworksDictionary.reload failed: %s', e) else: log.warning('NetworksDictionary.reload without self.filename.') def getNetwork(self, network): """Returns an IrcNetwork object for the given network.""" network = network.lower() if network in self.networks: return self.networks[network] else: c = IrcNetwork() self.networks[network] = c return c def setNetwork(self, network, ircNetwork): """Sets a given network to the IrcNetwork object given.""" network = network.lower() self.networks[network] = ircNetwork self.flush() def items(self): return self.networks.items() class IgnoresDB(object): __slots__ = ('filename', 'hostmasks') def __init__(self): self.filename = None self.hostmasks = ircutils.ExpiringHostmaskDict() 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 = list(self.hostmasks.items()) self.hostmasks.clear() try: self.open(self.filename) except EnvironmentError as e: log.warning('IgnoresDB.reload failed: %s', e) # Let's be somewhat transactional. for (hostmask, expiration) in oldhostmasks: self.hostmasks.add(hostmask, expiration) else: log.warning('IgnoresDB.reload called without self.filename.') def checkIgnored(self, prefix): return bool(self.hostmasks.match(prefix)) 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: networkFile = os.path.join(confDir, conf.supybot.databases.networks.filename()) networks = NetworksDictionary() networks.open(networkFile) except EnvironmentError as e: log.warning('Couldn\'t open network 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(channels.flush) world.flushers.append(networks.flush) world.flushers.append(ignores.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) except KeyError: # If there's no user... if conf.supybot.defaultIgnore(): log.debug('Ignoring %s due to conf.supybot.defaultIgnore', hostmask) return True else: try: if user._checkCapability('trusted'): # Trusted users (including owners) shouldn't ever be ignored. return False except KeyError: # neither explicitly trusted or -trusted -> consider them not # trusted pass if user.ignore: log.debug('Ignoring %s due to their IrcUser ignore flag.', 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.SpaceSeparatedSetOfStrings): __slots__ = () List = CapabilitySet class DefaultCapabilities(SpaceSeparatedListOfCapabilities): __slots__ = () # 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() 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().add('-owner') conf.registerGlobalValue(conf.supybot, 'capabilities', DefaultCapabilities([ '-owner', '-admin', '-trusted', '-aka.add', '-aka.set', '-aka.remove', '-alias.add', '-alias.remove', '-scheduler.add', '-scheduler.remove', '-scheduler.repeat', ], """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. To set this in a channel-specific way, use the 'channel capability setdefault' command.""")) 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/irclib.py0000644000175000017500000027743114535072470014543 0ustar00valval### # Copyright (c) 2002-2005 Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import copy import time import enum import random import base64 import textwrap import warnings import collections try: class crypto: import cryptography from cryptography.hazmat.primitives.serialization \ import load_pem_private_key from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.hazmat.primitives.hashes import SHA256 except ImportError: crypto = None try: import pyxmpp2_scram as scram except ImportError: scram = None from . import conf, ircdb, ircmsgs, ircutils, log, utils, world from .drivers import Server from .utils.str import rsplit from .utils.iter import chain from .utils.structures import smallqueue, RingBuffer, ExpiringDict MAX_LINE_SIZE = 512 # Including \r\n, but excluding server_tags ### # 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, args=None): """Given a string 'command', dispatches to doCommand.""" if args is None: warnings.warn( "dispatchCommand now takes an 'args' attribute, which is " "a list of the command's arguments (ie. IrcMsg.args).", DeprecationWarning) args = [] command = command.upper() subcommand = None method = None # Dispatch on command + subcommand, if there is a subcommand, and # a method with the matching name exists if command in ('FAIL', 'WARN', 'NOTE') and len(args) >= 1: subcommand = args[0] elif command in ('CAP',) and len(args) >= 2: # Note: this only covers the server-to-client format subcommand = args[1] command = command.capitalize() if subcommand is not None: subcommand = subcommand.capitalize() method = getattr(self, 'do' + command + subcommand, None) # If not dispatched on command + subcommand, then dispatch on command if method is None: method = getattr(self, 'do' + command, None) return method 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 = () echoMessage = False echo_message = False # deprecated alias of echoMessage __firewalled__ = {'die': None, 'reset': None, '__call__': None, 'inFilter': lambda self, irc, msg: msg, 'outFilter': lambda self, irc, msg: msg, 'postTransition': None, '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 postTransition(self, irc, msg, from_state, to_state): """Called when the state of the IRC connection changes. `msg` is the message that triggered the transition, if any.""" pass def __call__(self, irc, msg): """Used for handling each message.""" if not self.echoMessage and not self.echo_message \ and msg.command in ('PRIVMSG', 'NOTICE', 'TAGMSG') \ and ('label' in msg.server_tags or msg.tagged('emulatedEcho')): # This is an echo of a message we sent; and the plugin didn't # opt-in to receiving echos; ignoring it. # `'label' in msg.server_tags` detects echos when labeled-response # is enabled; and `msg.tag('emulatedEcho')` detects simulated # echos. As we don't enable real echo-message unless # labeled-response is enabled; this is an exhaustive check of echos # in all cases. # See "When a client sends a private message to its own nick" at # return method = self.dispatchCommand(msg.command, msg.args) 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): """Represents the known state of an IRC channel. .. attribute:: topic The topic of a channel (possibly the empty stringà :type: str .. attribute:: created Timestamp of the channel creation, according to the server. :type: int .. attribute:: ops Set of the nicks of all the operators of the channel. :type: ircutils.IrcSet[str] .. attribute:: halfops Set of the nicks of all the half-operators of the channel. :type: ircutils.IrcSet[str] .. attribute:: voices Set of the nicks of all the voiced users of the channel. :type: ircutils.IrcSet[str] .. attribute:: users Set of the nicks of all the users in the channel. :type: ircutils.IrcSet[str] .. attribute:: bans Set of the all the banmasks set in the channel. :type: ircutils.IrcSet[str] .. attribute:: modes Dict of all the modes set in the channel, with they value, if any. This excludes the following modes: ovhbeq :type: Dict[str, Optional[str]] """ __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): """Returns whether the given nick is an op.""" return nick in self.ops def isOpPlus(self, nick): """Returns whether the given nick is an op.""" return nick in self.ops def isVoice(self, nick): """Returns whether the given nick is voiced.""" return nick in self.voices def isVoicePlus(self, nick): """Returns whether the given nick is voiced, an halfop, or an op.""" return nick in self.voices or nick in self.halfops or nick in self.ops def isHalfop(self, nick): """Returns whether the given nick is an halfop.""" return nick in self.halfops def isHalfopPlus(self, nick): """Returns whether the given nick is an halfop, or an op.""" return nick in self.halfops or nick in self.ops def addUser(self, user, prefix_chars='@%+&~!'): "Adds a given user to the ChannelState. Power prefixes are handled." nick = user.lstrip(prefix_chars) 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 prefix_chars: (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', 'name type arguments messages parent_batch') """Represents a batch of messages, see Only access attributes by their name and do not create Batch objects in plugins; so we can extend the structure without breaking plugins.""" class IrcStateFsm(object): '''Finite State Machine keeping track of what part of the connection initialization we are in.''' __slots__ = ('state',) @enum.unique class States(enum.Enum): """Enumeration of all the states of an IRC connection.""" UNINITIALIZED = 10 '''Nothing received yet (except server notices)''' INIT_CAP_NEGOTIATION = 20 '''Sent CAP LS, did not send CAP END yet''' INIT_SASL = 30 '''In an AUTHENTICATE session''' INIT_WAITING_MOTD = 50 '''Waiting for start of MOTD''' INIT_MOTD = 60 '''Waiting for end of MOTD''' CONNECTED = 70 '''Normal state of the connections''' CONNECTED_SASL = 80 '''Doing SASL authentication in the middle of a connection.''' SHUTTING_DOWN = 100 def __init__(self): self.reset() def reset(self): if getattr(self, 'state', None) is not None: log.debug('resetting from %s to %s', self.state, self.States.UNINITIALIZED) self.state = self.States.UNINITIALIZED def _transition(self, irc, msg, to_state, expected_from=None): """Transitions to state `to_state`. If `expected_from` is not `None`, first checks the current state is in the set. After the transition, calls the `postTransition(irc, msg, from_state, to_state)` method of all objects in `irc.callbacks`. `msg` may be None if the transition isn't triggered by a message, but `irc` may not.""" from_state = self.state if expected_from is None or from_state in expected_from: log.debug('transition from %s to %s', self.state, to_state) self.state = to_state for callback in reversed(irc.callbacks): msg = callback.postTransition(irc, msg, from_state, to_state) else: raise ValueError('unexpected transition to %s while in state %s' % (to_state, self.state)) def expect_state(self, expected_states): if self.state not in expected_states: raise ValueError(('Connection in state %s, but expected to be ' 'in state %s') % (self.state, expected_states)) def on_init_messages_sent(self, irc): '''As soon as USER/NICK/CAP LS are sent''' self._transition(irc, None, self.States.INIT_CAP_NEGOTIATION, [ self.States.UNINITIALIZED, ]) def on_sasl_start(self, irc, msg): '''Whenever we initiate a SASL transaction.''' if self.state == self.States.INIT_CAP_NEGOTIATION: self._transition(irc, msg, self.States.INIT_SASL) elif self.state == self.States.CONNECTED: self._transition(irc, msg, self.States.CONNECTED_SASL) else: raise ValueError('Started SASL while in state %s' % self.state) def on_sasl_auth_finished(self, irc, msg): '''When sasl auth either succeeded or failed.''' if self.state == self.States.INIT_SASL: self._transition(irc, msg, self.States.INIT_CAP_NEGOTIATION) elif self.state == self.States.CONNECTED_SASL: self._transition(irc, msg, self.States.CONNECTED) else: raise ValueError('Finished SASL auth while in state %s' % self.state) def on_cap_end(self, irc, msg): '''When we send CAP END''' self._transition(irc, msg, self.States.INIT_WAITING_MOTD, [ self.States.INIT_CAP_NEGOTIATION, ]) def on_start_motd(self, irc, msg): '''On 375 (RPL_MOTDSTART)''' self._transition(irc, msg, self.States.INIT_MOTD, [ self.States.INIT_CAP_NEGOTIATION, self.States.INIT_WAITING_MOTD, self.States.CONNECTED, self.States.CONNECTED_SASL, ]) def on_end_motd(self, irc, msg): '''On 376 (RPL_ENDOFMOTD) or 422 (ERR_NOMOTD)''' self._transition(irc, msg, self.States.CONNECTED, [ self.States.INIT_CAP_NEGOTIATION, self.States.INIT_WAITING_MOTD, self.States.INIT_MOTD, self.States.CONNECTED, self.States.CONNECTED_SASL, ]) def on_shutdown(self, irc, msg): self._transition(irc, msg, self.States.SHUTTING_DOWN) class IrcState(IrcCommandDispatcher, log.Firewalled): """Maintains state of the Irc connection. Should also become smarter. .. attribute:: fsm A finite-state machine representing the current state of the IRC connection: various steps while connecting, then remains in the CONNECTED state (or CONNECTED_SASL when doing SASL in the middle of a connection). :type: IrcStateFsm .. attribute:: capabilities_req Set of all capabilities requested from the server. See :type: Set[str] .. attribute:: capabilities_ack Set of all capabilities requested from and acknowledged by the server. See :type: Set[str] .. attribute:: capabilities_nak Set of all capabilities requested from and refused by the server. This should always be empty unless the bot, a plugin, or the server is misbehaving. See :type: Set[str] .. attribute:: capabilities_ls Stores all the capabilities advertised by the server, as well as their value, if any. :type: Dict[str, Optional[str]] .. attribute:: ircd Identification string of the software running the server we are connected to. See :type: str .. attribute:: supported Stores the value of ISUPPORT sent when connecting. See for the list of keys. :type: utils.InsensitivePreservingDict[str, Any] .. attribute:: history History of messages received from the network. Automatically discards messages so it doesn't exceed ``supybot.protocols.irc.maxHistoryLength``. :type: RingBuffer[ircmsgs.IrcMsg] .. attribute:: channels Store channel states. :type: ircutils.IrcDict[str, ChannelState] .. attribute:: nicksToHostmasks Stores the last hostmask of a seen nick. :type: ircutils.IrcDict[str, str] """ __firewalled__ = {'addMsg': None} def __init__(self, history=None, supported=None, nicksToHostmasks=None, channels=None, capabilities_req=None, capabilities_ack=None, capabilities_nak=None, capabilities_ls=None): self.fsm = IrcStateFsm() 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_req = capabilities_req or set() 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 # Batches usually finish and are way shorter than 3600s, but # we need to: # * keep them in case the connection breaks (and reset() can't # clear the list itself) # * make sure to avoid leaking memory in general self.batches = ExpiringDict(timeout=3600) def reset(self): """Resets the state to normal, unconnected state.""" self.fsm.reset() self.history.reset() self.history.resize(conf.supybot.protocols.irc.maxHistoryLength()) self.ircd = None self.channels.clear() self.supported.clear() self.nicksToHostmasks.clear() self.capabilities_req = set() self.capabilities_ack = set() self.capabilities_nak = set() self.capabilities_ls = {} # Don't clear batches right now. reset() is called on ERROR messages, # which may be part of a BATCH so we need to remember that batch. # At worst, the batch will expire in the near future, as self.batches # is an instance of ExpiringDict. # If we did clear the batch, then this would happen: # 1. IrcState.addMsg() would crash on the ERROR, because its batch # server-tag references an unknown batch, so it would not set the # 'batch' supybot-tag # 2. Irc.doBatch would crash on the closing BATCH, for the same reason # 3. Owner.doBatch would crash because it expects the batch # supybot-tag to be set, but it wasn't because of 1 #self.batches.clear() 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_name = msg.server_tags['batch'] assert batch_name in self.batches, \ 'Server references undeclared batch %r' % batch_name for batch in self.getParentBatches(msg): batch.messages.append(msg) method = self.dispatchCommand(msg.command, msg.args) 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 getParentBatches(self, msg): """Given an IrcMsg, returns a list of all batches that contain it, innermost first. Raises ValueError if ``msg`` is not in a batch; or if it is in a batch that has already ended. This restriction may be relaxed in the future. This means that you should not call ``getParentBatches`` on a message that was already processed. For example, assume Limnoria received the following:: :irc.host BATCH +outer example.com/foo @batch=outer :irc.host BATCH +inner example.com/bar @batch=inner :nick!user@host PRIVMSG #channel :Hi @batch=outer :irc.host BATCH -inner :irc.host BATCH -outer If you call getParentBatches on any of the middle three messages, you get ``[Batch(name='inner', ...), Batch(name='outer', ...)]``. And if you call getParentBatches on either the first or the last message, you get ``[Batch(name='outer', ...)]`` And you may only call `getParentBatches`` on the PRIVMSG if only the first three messages were processed. """ batch = msg.tagged('batch') if not batch: # msg is not a BATCH command batch_name = msg.server_tags.get('batch') if batch_name: batch = self.batches.get(batch_name) if not batch: raise ValueError( 'Called getParentBatches for a message in a batch that ' 'already ended.' ) else: raise ValueError( 'Called getParentBatches for a message not in a batch.') batches = [] while batch: batches.append(batch) batch = batch.parent_batch return batches def getClientTagDenied(self, tag): """Returns whether the given tag is denied by the server, according to its CLIENTTAGDENY policy. This is only informative, and servers may still allow or deny tags at their discretion. For details, see the RPL_ISUPPORT section in """ tag = tag.lstrip("+") denied_tags = self.supported.get('CLIENTTAGDENY') if not denied_tags: return False denied_tags = denied_tags.split(',') if '*' in denied_tags: # All tags are denied by default, check the whitelist return ('-' + tag) not in denied_tags else: return tag in denied_tags 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) > 2 else msg.args[1] # The conditionals are for Twitch, which doesn't send umodes or # chanmodes. if len(msg.args) > 3: self.supported['umodes'] = frozenset(msg.args[3]) if len(msg.args) > 4: self.supported['chanmodes'] = frozenset(msg.args[4]) _005converters = utils.InsensitivePreservingDict({ 'modes': lambda s: int(s) if s else None, # it's optional '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] # Set of prefixes servers may append before a NAMES reply when # the user is op/halfop/voice/... # https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.15 prefix = self.supported.get('PREFIX') if prefix is None: prefix_chars = '@%+&~!' # see the comments in addUser else: prefix_chars = ''.join(prefix.values()) for item in items.split(): stripped_item = item.lstrip(prefix_chars) item_prefix = item[0:-len(stripped_item)] if ircutils.isUserHostmask(stripped_item): nick = ircutils.nickFromHostmask(stripped_item) self.nicksToHostmasks[nick] = stripped_item name = item_prefix + nick else: name = item c.addUser(name, prefix_chars=prefix_chars) 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 irc.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:]) # Both are possibly None: parent_batch_name = msg.server_tags.get("batch") parent_batch = self.batches.get(parent_batch_name) batch = Batch( name=batch_name, type=batch_type, arguments=batch_arguments, messages=[msg], parent_batch=parent_batch ) msg.tag('batch', batch) self.batches[batch_name] = batch elif msg.args[0].startswith('-'): batch = self.batches.pop(batch_name) batch.messages.append(msg) 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. .. attribute:: zombie Whether or not this object represents a living IRC connection. :type: bool .. attribute:: network The name of the network this object is connected to. :type: str .. attribute:: startedAt When this connection was (re)started. :type: float .. attribute:: callbacks List of all callbacks (ie. plugins) currently loaded :type: List[IrcCallback] .. attribute:: queue Queue of messages waiting to be sent. Plugins should use the ``queueMsg`` method instead of accessing this directly. :type: IrcMsgQueue .. attribute:: fastqueue Same as ``queue``, but for messages with high priority. Plugins should use the ``sendMsg`` method instead of accessing this directly (or `queueMsg` if the message isn't high priority). :type: smallqueue .. attribute:: driver Driver of the IRC connection (normally, a :py:class:`supybot.drivers.Socket.SocketDriver` object). Plugins normally do not need to access this. .. attribute:: startedSync When joining a channel, a ``'#channel': time.time()`` entry is added to this dict, which is then removed when the join is completed. Plugins should not change this value, it is automatically handled when they send a JOIN. :type: ircutils.IrcDict[str, float] .. attribute:: monitoring A dict with nicks as keys and the number of plugins monitoring this nick as value. Plugins should not access this directly, and should use the ``monitor`` and ``unmonitor`` methods instead. :type: ircutils.IrcDict[str, int] .. attribute:: state An :py:class:`supybot.irclib.IrcState` object, which stores all the known information about the connection with the IRC network. :type: supybot.irclib.IrcState """ __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() # Messages of batches that are currently in one self.queue (not # self.fastqueue). # This works by adding only the first message of a batch in a queue, # and when self.takeMsg pops that message from the queue, it will # also pop the whole batch from self._queued_batches and atomically # add it to self.fastqueue self._queued_batches = {} 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): """Returns whether the given argument is a valid nick on this network. """ 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 msg.command.upper() == 'BATCH': log.error('Tried to send a BATCH message using queueMsg ' 'instead of queueBatch: %r', msg) 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 msg.command.upper() == 'BATCH': log.error('Tried to send a BATCH message using sendMsg ' 'instead of queueBatch: %r', msg) if not self.zombie: self.fastqueue.enqueue(msg) else: log.warning('Refusing to send %r; %s is a zombie.', msg, self) def queueBatch(self, msgs): """Queues a batch of messages to be sent to the server. See queueMsg/sendMsg must not be used repeatedly to send a batch, because they do not guarantee the batch is send atomically, which is required because "Clients MUST NOT send messages other than PRIVMSG while a multiline batch is open." -- """ if not conf.supybot.protocols.irc.experimentalExtensions(): raise ValueError( 'queueBatch is disabled because it depends on draft ' 'IRC specifications. If you know what you are doing, ' 'set supybot.protocols.irc.experimentalExtensions.') if len(msgs) < 2: raise ValueError( 'queueBatch called with less than two messages.') if msgs[0].command.upper() != 'BATCH' or msgs[0].args[0][0] != '+': raise ValueError( 'queueBatch called with non-"BATCH +" as first message.') if msgs[-1].command.upper() != 'BATCH' or msgs[-1].args[0][0] != '-': raise ValueError( 'queueBatch called with non-"BATCH -" as last message.') batch_name = msgs[0].args[0][1:] if msgs[-1].args[0][1:] != batch_name: raise ValueError( 'queueBatch called with mismatched BATCH name args.') if any(msg.server_tags['batch'] != batch_name for msg in msgs[1:-1]): raise ValueError( 'queueBatch called with mismatched batch names.') return if batch_name in self._queued_batches: raise ValueError( 'queueBatch called with a batch name already in flight') self._queued_batches[batch_name] = msgs # Enqueue only the start of the batch. When takeMsg sees it, it will # enqueue the full batch in self.fastqueue. # We don't enqueue the full batch in self.fastqueue here, because # there is no reason for this batch to jump in front of all other # queued messages. # TODO: the batch will be ordered with the priority of a BATCH # message (ie. normal), but if the batch is made only of low-priority # messages like PRIVMSG, it should have that priority. # (or maybe order on the batch type instead of commands inside # the batch?) self.queue.enqueue(msgs[0]) def _truncateMsg(self, msg): msg_str = str(msg) if msg_str[0] == '@': (msg_tags_str, msg_rest_str) = msg_str.split(' ', 1) msg_tags_str += ' ' else: msg_tags_str = '' msg_rest_str = msg_str msg_rest_bytes = msg_rest_str.encode() if len(msg_rest_bytes) > MAX_LINE_SIZE: # 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) # Truncate to 512 bytes (minus 2 for '\r\n') msg_rest_bytes = msg_rest_bytes[:MAX_LINE_SIZE-2] # The above truncation may have truncated in the middle of a # multi-byte character. # I was about to write a UTF-8 decoder here just to trim them # properly, but fortunately there is a neat trick to trim it # while decoding: just ignore invalid bytes! # https://stackoverflow.com/a/1820949/539465 msg_rest_str = msg_rest_bytes.decode(errors="ignore") msg._str = msg_tags_str + msg_rest_str + '\r\n' msg._len = len(str(msg)) # TODO: truncate tags 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: # Copy the msg before altering it back. Without a copy, it # can cause all sorts of issues if the msg is reused (eg. Relay # sends the same message object to the same network, so when # sending the msg for the second time, it would already be # tagged with emulatedEcho and fail the assertion; or it can be # added a label because we have labeled-response on a network) msg = ircmsgs.IrcMsg(msg=msg) if msg.command.upper() == 'BATCH': if not conf.supybot.protocols.irc.experimentalExtensions(): log.error('Dropping outgoing batch. ' 'supybot.protocols.irc.experimentalExtensions ' 'is disabled, so plugins should not send ' 'batches. This is a bug, please report it.') return None if msg.args[0].startswith('+'): # Start of a batch; created by self.queueBatch. We need to # *prepend* the rest of the batch to the fastqueue # so that no other message is sent while the batch is # open. # "Clients MUST NOT send messages other than PRIVMSG while # a multiline batch is open." # -- # # (Yes, *prepend* to the queue. Fortunately, it should be # empty, because BATCH cannot be queued in the fastqueue # and we just got a BATCH, which means it's from the # regular queue, which means the fastqueue is empty. # But let's not take any risk, eg. if race condition # with a plugin appending directly to the fastqueue.) batch_name = msg.args[0][1:] batch_messages = self._queued_batches.pop(batch_name) if batch_messages[0] != msg: log.error('Enqueue "BATCH +" message does not match ' 'the one of the batch in flight.') self.fastqueue[:0] = batch_messages[1:] if not world.testing and 'label' not in msg.server_tags \ and 'labeled-response' in self.state.capabilities_ack: # Not adding labels while testing, because it would break # all plugin tests using IrcMsg equality (unless they # explicitly add the label, but it becomes a burden). msg.server_tags['label'] = ircutils.makeLabel() msg._len = msg._str = None for callback in reversed(self.callbacks): self._setMsgChannel(msg) try: msg = callback.outFilter(self, msg) except: log.exception('Uncaught exception in outFilter:') continue if msg is None: log.debug('%s.outFilter returned None.', callback.name()) return self.takeMsg() world.debugFlush() self._truncateMsg(msg) if msg.command.upper() in ('PRIVMSG', 'NOTICE', 'TAGMSG') \ and 'echo-message' not in self.state.capabilities_ack: # echo-message is not implemented by server; let's emulate it # here, just before sending it to the driver. assert not msg.tagged('receivedAt') if not world.testing: assert not msg.tagged('emulatedEcho') msg.tag('emulatedEcho', True) self.feedMsg(msg, tag=False) else: # 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 def _tagMsg(self, msg): """Sets attribute on an incoming IRC message. Will usually only be called by feedMsg, but may be useful in tests as well.""" msg.tag('receivedBy', self) msg.tag('receivedOn', self.network) msg.tag('receivedAt', time.time()) self._setMsgChannel(msg) def _setMsgChannel(self, msg): channel = None if msg.args: channel = msg.args[0] if msg.command in ('NOTICE', 'PRIVMSG') and \ not conf.supybot.protocols.irc.strictRfc(): channel = self.stripChannelPrefix(channel) if not self.isChannel(channel): channel = None msg.channel = channel def stripChannelPrefix(self, channel): statusmsg_chars = self.state.supported.get('statusmsg', '') return channel.lstrip(statusmsg_chars) _numericErrorCommandRe = re.compile(r'^[45][0-9][0-9]$') def feedMsg(self, msg, tag=True): """Called by the IrcDriver; feeds a message received. `tag=False` is used when simulating echo messages, to skip adding received* tags.""" if tag: self._tagMsg(msg) channel = msg.channel # used by dynamicScope (ew) 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, msg.args) 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: try: callback.reset() except Exception: log.exception('Uncaught exception in %r.reset()', callback) 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.triedNicks = ircutils.IrcSet() 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': if not crypto: log.debug('Skipping SASL %s, crypto module ' 'is not available', mechanism) elif not self.sasl_username or not self.sasl_ecdsa_key: log.debug('Skipping SASL %s, missing username and/or key', mechanism) else: self.sasl_next_mechanisms.append(mechanism) elif mechanism == 'external': if not network_config.certfile() and \ not conf.supybot.protocols.irc.certfile(): log.debug('Skipping SASL %s, missing cert file', mechanism) else: self.sasl_next_mechanisms.append(mechanism) elif mechanism.startswith('scram-'): if not scram: log.debug('Skipping SASL %s, scram module ' 'is not available', mechanism) elif not self.sasl_username or not self.sasl_password: log.debug('Skipping SASL %s, missing username and/or ' 'password', mechanism) else: self.sasl_next_mechanisms.append(mechanism) elif mechanism == 'plain': if not self.sasl_username or not self.sasl_password: log.debug('Skipping SASL %s, missing username and/or ' 'password', mechanism) else: self.sasl_next_mechanisms.append(mechanism) # Note: echo-message is only requested if labeled-response is available. REQUEST_CAPABILITIES = set(['account-notify', 'extended-join', 'multi-prefix', 'metadata-notify', 'account-tag', 'userhost-in-names', 'invite-notify', 'server-time', 'chghost', 'batch', 'away-notify', 'message-tags', 'msgid', 'setname', 'labeled-response', 'echo-message', 'sasl', 'standard-replies']) """IRCv3 capabilities requested when they are available. echo-message is special-cased to be requested only with labeled-response. To check if a capability was negotiated, use `irc.state.capabilities_ack`. """ REQUEST_EXPERIMENTAL_CAPABILITIES = set(['draft/account-registration', 'draft/multiline']) """Like REQUEST_CAPABILITIES, but these capabilities are only requested if supybot.protocols.irc.experimentalExtensions is enabled.""" def _queueConnectMessages(self): if self.zombie: self.driver.die() self._reallyDie() return self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('LS', '302'))) self.sendAuthenticationMessages() self.state.fsm.on_init_messages_sent(self) 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 capUpkeep(self, msg): """ Called after getting a CAP ACK/NAK to check it's consistent with what was requested, and to end the cap negotiation when we received all the ACK/NAKs we were waiting for. `msg` is the message that triggered this call.""" self.state.fsm.expect_state([ # Normal CAP ACK / CAP NAK during cap negotiation: IrcStateFsm.States.INIT_CAP_NEGOTIATION, # Sigyn sends CAP REQ when it sees RPL_SASLSUCCESS, so we get the # CAP ACK while waiting for MOTD on some IRCds (eg. InspIRCd): IrcStateFsm.States.INIT_WAITING_MOTD, IrcStateFsm.States.INIT_MOTD, # CAP ACK / CAP NAK after a CAP NEW (probably): IrcStateFsm.States.CONNECTED, ]) capabilities_responded = (self.state.capabilities_ack | self.state.capabilities_nak) if not capabilities_responded <= self.state.capabilities_req: log.error('Server responded with unrequested ACK/NAK ' 'capabilities: req=%r, ack=%r, nak=%r', self.state.capabilities_req, self.state.capabilities_ack, self.state.capabilities_nak) self.driver.reconnect(wait=True) elif capabilities_responded == self.state.capabilities_req: log.debug('Got all capabilities ACKed/NAKed') # We got all the capabilities we asked for if 'sasl' in self.state.capabilities_ack: if self.state.fsm.state in [ IrcStateFsm.States.INIT_CAP_NEGOTIATION, IrcStateFsm.States.CONNECTED]: self._maybeStartSasl(msg) else: pass # Already in the middle of a SASL auth elif self.state.fsm.state != IrcStateFsm.States.CONNECTED: # If we are still in the initial cap negotiation (ie. if this # is not in response to a 'CAP NEW'), send a CAP END so the # server sends us the MOTD self.endCapabilityNegociation(msg) else: log.debug('Waiting for ACK/NAK of capabilities: %r', self.state.capabilities_req - capabilities_responded) pass # Do nothing, we'll get more def endCapabilityNegociation(self, msg): self.state.fsm.on_cap_end(self, msg) 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, msg): self.state.fsm.expect_state([ IrcStateFsm.States.INIT_SASL, IrcStateFsm.States.CONNECTED_SASL, ]) log.debug('Next SASL mechanisms: %s', self.sasl_next_mechanisms) 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(),))) elif conf.supybot.networks.get(self.network).sasl.required(): log.error('None of the configured SASL mechanisms succeeded, ' 'aborting connection.') else: self.sasl_current_mechanism = None self.state.fsm.on_sasl_auth_finished(self, msg) if self.state.fsm.state == IrcStateFsm.States.INIT_CAP_NEGOTIATION: self.endCapabilityNegociation(msg) def _maybeStartSasl(self, msg): if not self.sasl_authenticated and \ 'sasl' in self.state.capabilities_ack: self.startSasl(msg) def startSasl(self, msg): self.state.fsm.on_sasl_start(self, msg) assert 'sasl' in self.state.capabilities_ls, ( 'Starting SASL without receiving "CAP LS sasl" or ' '"CAP NEW sasl" first.') self.resetSasl() s = self.state.capabilities_ls['sasl'] if s is not None: available = set(map(str.lower, s.split(','))) self.sasl_next_mechanisms = [ x for x in self.sasl_next_mechanisms if x.lower() in available] self.tryNextSaslMechanism(msg) def doAuthenticate(self, msg): self.state.fsm.expect_state([ IrcStateFsm.States.INIT_SASL, IrcStateFsm.States.CONNECTED_SASL, ]) 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(msg, 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(msg, 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(msg, string) else: assert False except scram.ScramException: self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) self.tryNextSaslMechanism(msg) 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, msg, string): if string == b'': self.sendSaslString(self.sasl_username.encode('utf-8')) return try: with open(self.sasl_ecdsa_key, 'rb') as fd: private_key = crypto.load_pem_private_key( fd.read(),password=None, backend=crypto.default_backend()) authstring = private_key.sign( string, crypto.ECDSA(crypto.Prehashed(crypto.SHA256()))) self.sendSaslString(authstring) except (OSError, ValueError): self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) self.tryNextSaslMechanism(msg) def _doAuthenticateScramFirst(self, msg, 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(msg) 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, msg, 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.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) self.tryNextSaslMechanism(msg) 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.state.fsm.on_sasl_auth_finished(self, msg) if self.state.fsm.state == IrcStateFsm.States.INIT_CAP_NEGOTIATION: self.endCapabilityNegociation(msg) def do904(self, msg): log.warning('%s: SASL authentication failed (mechanism: %s): %s', self.network, self.sasl_current_mechanism, msg.args[-1]) self.tryNextSaslMechanism(msg) def do905(self, msg): log.warning('%s: SASL authentication failed because the username or ' 'password is too long.', self.network) self.tryNextSaslMechanism(msg) def do906(self, msg): log.warning('%s: SASL authentication aborted', self.network) if self.state.fsm.state == IrcStateFsm.States.INIT_WAITING_MOTD: # This 906 was triggered by sending 'CAP END' after we exhausted # all authentication mechanism; so it does not make sense to try # self.tryNextSaslMechanism() again. And it would crash anyway, # because it does not expect the connection to be in this state. pass else: self.tryNextSaslMechanism(msg) def do907(self, msg): log.warning('%s: Attempted SASL authentication when we were already ' 'authenticated.', self.network) self.tryNextSaslMechanism(msg) def do908(self, msg): log.info('%s: Supported SASL mechanisms: %s', self.network, msg.args[1]) 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: %s', self.network, caps) self.state.capabilities_ack.update(caps) self.capUpkeep(msg) 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: %s', self.network, caps) self.capUpkeep(msg) def _onCapSts(self, policy, msg): tls_connection = self.driver.currentServer.force_tls_verification \ or self.driver.ssl secure_connection = self.driver.currentServer.force_tls_verification \ or (self.driver.ssl and self.driver.anyCertValidationEnabled()) parsed_policy = ircutils.parseStsPolicy( log, policy, tls_connection=tls_connection) if parsed_policy is None: # There was an error (and it was logged). Ignore it and proceed # with the connection. # Currently this shouldn't happen, but let's future-proof it, eg. # in case https://github.com/ircv3/ircv3-specifications/pull/390 # gets adopted. return if secure_connection: # TLS is enabled and certificate is verified; write the STS policy # in stone. # For future-proofing (because we don't want to write an invalid # value), we write the raw policy received from the server instead # of the parsed one. log.debug('Storing STS policy for %s (TLS port %s): %s', self.driver.currentServer.hostname, self.driver.currentServer.port, policy) ircdb.networks.getNetwork(self.network).addStsPolicy( self.driver.currentServer.hostname, self.driver.currentServer.port, policy) elif self.driver.ssl: # SSL enabled, but certificates are not checked -> reconnect on the # same port and check certificates, before storing the STS policy. hostname = self.driver.currentServer.hostname port = self.driver.currentServer.port attempt = self.driver.currentServer.attempt log.info('Got STS policy over insecure TLS connection; ' 'reconnecting to check certificates. %r', self.driver.currentServer) # Reconnect to the server, but with TLS *and* certificate # validation this time. self.state.fsm.on_shutdown(self, msg) self.driver.reconnect( server=Server(hostname, port, attempt, True), wait=True) else: hostname = self.driver.currentServer.hostname attempt = self.driver.currentServer.attempt log.info('Got STS policy over insecure (cleartext) connection; ' 'reconnecting to secure port. %r', self.driver.currentServer) # Reconnect to the server, but with TLS *and* certificate # validation this time. self.state.fsm.on_shutdown(self, msg) self.driver.reconnect( server=Server(hostname, parsed_policy['port'], attempt, True), wait=True) def _addCapabilities(self, capstring, msg): for item in capstring.split(): while item.startswith(('=', '~')): item = item[1:] if '=' in item: (cap, value) = item.split('=', 1) if cap == 'sts': self._onCapSts(value, msg) self.state.capabilities_ls[cap] = value else: if item == 'sts': log.error('Got "sts" capability without value. Aborting ' 'connection.') self.driver.reconnect(wait=True) 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], msg) elif len(msg.args) == 3: # End of LS self._addCapabilities(msg.args[2], msg) if self.state.fsm.state == IrcStateFsm.States.SHUTTING_DOWN: return self.state.fsm.expect_state([ # Normal case: IrcStateFsm.States.INIT_CAP_NEGOTIATION, # Should only happen if a plugin sends a CAP LS (which they # shouldn't do): IrcStateFsm.States.CONNECTED, IrcStateFsm.States.CONNECTED_SASL, ]) # Normally at this point, self.state.capabilities_ack should be # empty; but let's just make sure we're not requesting the same # caps twice for no reason. want_capabilities = self.REQUEST_CAPABILITIES if conf.supybot.protocols.irc.experimentalExtensions(): want_capabilities |= self.REQUEST_EXPERIMENTAL_CAPABILITIES new_caps = ( set(self.state.capabilities_ls) & want_capabilities - self.state.capabilities_ack) # NOTE: Capabilities are requested in alphabetic order, because # sets are unordered, and their "order" is nondeterministic. # This is needed for the tests. if new_caps: self.requestCapabilities(new_caps) else: self.endCapabilityNegociation(msg) 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): # Note that in theory, this method may be called at any time, even # before CAP END (or even before the initial CAP LS). 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], msg) if self.state.fsm.state == IrcStateFsm.States.SHUTTING_DOWN: return common_supported_unrequested_capabilities = ( set(self.state.capabilities_ls) & self.REQUEST_CAPABILITIES - self.state.capabilities_ack) if common_supported_unrequested_capabilities: self.requestCapabilities(common_supported_unrequested_capabilities) def requestCapabilities(self, caps): """Takes an iterable of IRCv3 capabilities, and requests them to the server using CAP REQ. This is mostly just used during connection registration or when the server sends CAP NEW; but plugins may use it as well to request custom capabilities. They should make sure these capabilities cannot negatively impact other plugins, though.""" caps = list(sorted(caps)) cap_lines = [] if 'echo-message' in caps \ and 'labeled-response' not in self.state.capabilities_ack: # Make sure echo-message is never requested unless we either have # labeled-response already, or we request it *on the same line* # so they are both accepted or both rejected). The reason for this # is that this is required to properly deal with PRIVMSGs sent to # oneself. # See "When a client sends a private message to its own nick" at # caps.remove('echo-message') if 'labeled-response' in caps: caps.remove('labeled-response') # This makes sure they are always on the same line (which # happens to be the first): caps = ['echo-message', 'labeled-response'] + caps self.state.capabilities_req |= set(caps) caps = ' '.join(caps) # textwrap works here because in ASCII, all chars are 1 bytes: cap_lines = textwrap.wrap( caps, MAX_LINE_SIZE-len('CAP REQ :'), break_long_words=False, break_on_hyphens=False) for cap_line in cap_lines: self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('REQ', cap_line))) 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 if nick not in self.triedNicks: self.triedNicks.add(nick) return nick 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 ret in self.triedNicks: L[random.randrange(len(L))] = utils.iter.choice('0123456789') ret = ''.join(L) self.triedNicks.add(ret) 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 do375(self, msg): self.state.fsm.on_start_motd(self, msg) log.info('Got start of MOTD from %s', self.server) def do376(self, msg): self.state.fsm.on_end_motd(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()[:] self._setUmodes() do377 = do422 = do376 def _setUmodes(self): # Get the configured umodes umodes = conf.supybot.networks.get(self.network).umodes() if umodes == '': umodes = conf.supybot.protocols.irc.umodes() # Add the bot mode if the server advertizes one; # and if the configured umode doesn't already have it # explicitly set or unset bot_mode = self.state.supported.get("BOT") if bot_mode and len(bot_mode) == 1: if bot_mode not in umodes: umodes += "+" + bot_mode # Filter out umodes not supported by the server supported = self.state.supported.get('umodes') if supported: acceptedchars = supported.union('+-') umodes = ''.join([m for m in umodes if m in acceptedchars]) # Send the umodes if umodes: log.info('Sending user modes to %s: %s', self.network, umodes) self.sendMsg(ircmsgs.mode(self.nick, umodes)) def do43x(self, msg, problem): if not self.afterConnect: self.triedNicks.add(self.nick) newNick = self._getNextNick() assert newNick != self.nick, \ (self.nick, self.alternateNicks, self.triedNicks) 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/ircmsgs.py0000644000175000017500000011403314535072470014732 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010, James McCoy # Copyright (c) 2010-2021, 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. ### """ 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 sys 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)) _server_tag_unescape = {k: v for (v, k) in SERVER_TAG_ESCAPE} _escape_sequence_pattern = re.compile(r'\\.?') def _unescape_replacer(m): escape_sequence = m.group(0) unescaped = _server_tag_unescape.get(escape_sequence) if unescaped is None: # Matches both a lone \ at the end and a \ followed by an "invalid" # character. In both cases, the \ must be dropped. return escape_sequence[1:] return unescaped def unescape_server_tag_value(s): return _escape_sequence_pattern.sub(_unescape_replacer, s) def _parse_server_tags(s): server_tags = {} for tag in s.split(';'): if '=' not in tag: server_tags[sys.intern(tag)] = None else: (key, value) = tag.split('=', 1) value = unescape_server_tag_value(value) if value == '': # "Implementations MUST interpret empty tag values (e.g. foo=) # as equivalent to missing tag values (e.g. foo)." value = None server_tags[sys.intern(key)] = 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) def split_args(s, maxsplit=-1): """Splits on spaces, treating consecutive spaces as one.""" return list(filter(bool, s.split(' ', maxsplit=maxsplit))) 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) .. attribute:: command The IRC command of the message (eg. PRIVMSG, NOTICE, MODE, QUIT, ...). In case of "split" commands (eg. CAP LS), this is only the first part, and the other parts are in `args`. .. attribute:: args Arguments of the IRC command (including subcommands). For example, for a PRIVMSG, `args = ('#channel', 'content of the message')`. .. attribute:: channel The name of the channel this message was received on or will be sent to; or None if this is not a channel message (PRIVMSG to a nick, QUIT, etc.) `msg.args[0]` was formerly used to get the channel, but it had several pitfalls (such as needing server-specific channel vs nick detection, and needing to strip statusmsg characters). .. attribute:: prefix `nick!user@host` of the author of the message, or None. .. attribute:: nick Nickname of the author of the message, or None. .. attribute:: user Username/ident of the author of the message, or None. .. attribute:: host Hostname of the author of the message, or None. .. attribute:: time Float timestamp of the moment the message was sent by the server. If the server does not support `server-time`, this falls back to the value of `time.time()` when the message was received. .. attribute:: server_tags Dictionary of IRCv3 message tags. `None` values indicate the tag is present but has no value. This includes client tags; the name is meant to disambiguate wrt the `tags` attribute, which are tags used internally by Supybot/Limnoria. .. attribute:: reply_env (Mutable) dictionary of internal key:value pairs, all of which must be strings. Several plugins offer string templating, such as the 'echo' command in the Misc plugin; which replace `$variable` with a value. Adding values to this dictionary allows access to these values from these commands; this is especially useful when nesting commands. .. attribute:: tags (Mutable) dictionary of internal key:value pairs on this message. This is not to be confused with IRCv3 message tags; these are stored as `server_tags` (including the client tags). """ # 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', 'channel') def __init__(self, s='', command='', args=(), prefix='', server_tags=None, 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 ' :' in s: # Note the space: IPV6 addresses are bad w/o it. s, last = s.split(' :', 1) self.args = split_args(s) self.args.append(last.rstrip('\r\n')) else: self.args = split_args(s.rstrip('\r\n')) if self.args[0][0] == ':': self.prefix = self.args.pop(0)[1:] else: self.prefix = '' 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() if server_tags is None: self.server_tags = msg.server_tags.copy() else: self.server_tags = 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 if server_tags is None: self.server_tags = {} else: self.server_tags = server_tags self.prefix = sys.intern(self.prefix) self.command = sys.intern(self.command) 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: s = ':%s %s %s :%s\r\n' % ( self.prefix, self.command, ' '.join(self.args[:-1]), self.args[-1]) else: if self.args: s = ':%s %s :%s\r\n' % ( self.prefix, self.command, self.args[0]) else: s = ':%s %s\r\n' % (self.prefix, self.command) else: if len(self.args) > 1: s = '%s %s :%s\r\n' % ( self.command, ' '.join(self.args[:-1]), self.args[-1]) else: if self.args: s = '%s :%s\r\n' % (self.command, self.args[0]) else: s = '%s\r\n' % self.command if self.server_tags: s = _format_server_tags(self.server_tags) + ' ' + s self._str = s 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 and \ self.server_tags == other.server_tags __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(server_tags=%r, prefix=%q, command=%q, args=%r)', self.server_tags, self.prefix, self.command, self.args) return self._repr def __reduce__(self): return (self.__class__, (str(self),)) def tag(self, tag, value=True): """Affect an internal key:value pair to this message. This is not to be confused with IRCv3 message tags; these are stored as `server_tags` (including the client tags).""" self.tags[tag] = value def tagged(self, tag): """Get the value affected to a tag, or None if it is not set..""" 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 s.""" 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 s. """ 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 s.""" 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 s.""" 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 s.""" 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 s.""" 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 s.""" 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 s.""" 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/ircutils.py0000644000175000017500000012434214535072470015125 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009,2011,2015 James McCoy # Copyright (c) 2010-2021, 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. ### """ 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 uuid import base64 import random import string import textwrap import functools import collections.abc 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) def warning(s, *args): """Prints a debug string. Most likely replaced by our logging debug.""" print('###', s % args) userHostmaskRe = re.compile(r'^(?P\S+?)!(?P\S+)@(?P\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.""" m = userHostmaskRe.match(hostmask) assert m is not None, hostmask nick = m.group("nick") user = m.group("user") host = m.group("host") if set("!@") & set(nick+user+host): # There should never be either of these characters in the part. # As far as I know it never happens in practice aside from networks # broken by design. warning("Invalid hostmask format: %s", hostmask) # TODO: error if strictRfc is True 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 _compileHostmaskPattern(pattern): try: return _patternCache[pattern] 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(r'[\[{]') 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 _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: matched = _compileHostmaskPattern(pattern)(hostmask) is not None _hostmaskPatternEqualCache[(pattern, hostmask)] = matched return matched class HostmaskSet(collections.abc.MutableSet): """Stores a set of hostmasks and caches their pattern as compiled by _compileHostmaskPattern. This is an alternative to hostmaskPatternEqual for sets of patterns that do not change often, such as ircdb.IrcUser. ircdb.IrcUser used to store a real set, of hostmasks as strings, then call hostmaskPatternEqual on each of these strings. This is good enough most of the time, as hostmaskPatternEqual has a cache. Unfortunately, it is a LRU cache, and hostmasks are checked in order. This means that as soon as you have most hostmasks than the size of the cache, EVERY call to hostmaskPatternEqual will be a cache miss, so the regexp will need to be recompile every time. This is VERY expensive, because building the regexp is slow, and re.compile() is even slower.""" def __init__(self, hostmasks=()): self.data = {} # {hostmask_str: _compileHostmaskPattern(hostmask_str)} for hostmask in hostmasks: self.add(hostmask) def add(self, hostmask): self.data[hostmask] = _compileHostmaskPattern(hostmask) def discard(self, hostmask): self.data.pop(hostmask, None) def __contains__(self, hostmask): return hostmask in self.data def __iter__(self): return iter(self.data) def __len__(self): return len(self.data) def match(self, hostname): # Potential optimization: join all the patterns into a single one. for (pattern, compiled_pattern) in self.data.items(): if compiled_pattern(hostname) is not None: return pattern return None def __repr__(self): return 'HostmaskSet(%r)' % (list(self.data),) class ExpiringHostmaskDict(collections.abc.MutableMapping): """Like HostmaskSet, but behaves like a dict with expiration timestamps as values.""" # To keep it thread-safe, add to self.patterns first, then # self.data; and remove from self.data first. # And never iterate on self.patterns def __init__(self, hostmasks=None): if isinstance(hostmasks, (list, tuple)): hostmasks = dict(hostmasks) self.data = hostmasks or {} self.patterns = HostmaskSet(list(self.data)) def __getitem__(self, hostmask): return self.data[hostmask] def __setitem__(self, hostmask, expiration): """For backward compatibility, in case any plugin depends on it being dict-like.""" self.patterns.add(hostmask) self.data[hostmask] = expiration def __iter__(self): return iter(self.data) def __len__(self): return len(self.data) def __delitem__(self, hostmask): del self.data[hostmask] self.patterns.discard(hostmask) def expire(self): now = time.time() for (hostmask, expiration) in list(self.data.items()): if now >= expiration and expiration: self.pop(hostmask, None) def match(self, hostname): self.expire() return self.patterns.match(hostname) def clear(self): self.data.clear() self.patterns.clear() def __repr__(self): return 'ExpiringHostmaskSet(%r)' % (self.expirations,) 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] args = list(args[1:]) ret = [] last = '+' 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 def size(self): """Returns the number of bytes needed to reproduce this context in an IRC string.""" prefix_size = self.bold + self.reverse + self.underline + \ bool(self.fg) + bool(self.bg) if self.fg and self.bg: prefix_size += 6 # '\x03xx,yy%s' elif self.fg or self.bg: prefix_size += 3 # '\x03xx%s' if prefix_size: return prefix_size + 1 # '\x0f' else: return 0 class FormatParser(object): def __init__(self, s): self.fd = minisix.io.StringIO(s) self.last = None self.max_context_size = 0 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 self.max_context_size = max( self.max_context_size, context.size()) elif c == '\x16': context.reverse = not context.reverse self.max_context_size = max( self.max_context_size, context.size()) elif c == '\x1f': context.underline = not context.underline self.max_context_size = max( self.max_context_size, context.size()) elif c == '\x0f': context.reset() elif c == '\x03': self.getColor(context) self.max_context_size = max( self.max_context_size, context.size()) 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): # Get the maximum number of bytes needed to format a chunk of the string # at any point. # This is an overapproximation of what each chunk will need, but it's # either that or make the code of byteTextWrap aware of contexts, and its # code is complicated enough as it is already. parser = FormatParser(s) parser.parse() format_overhead = parser.max_context_size processed = [] chunks = utils.str.byteTextWrap(s, length - format_overhead) 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 msg.channel: # if message was sent to +#channel, we want to reply to +#channel; # or unvoiced channel users will see the bot reply without the # origin query 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.""" __slots__ = ('lowered',) 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.""" __slots__ = () def key(self, s): if s is not None: s = toLower(s) return s class CallableValueIrcDict(IrcDict): __slots__ = () 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.""" __slots__ = () 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 standardSubstitutionVariables(irc, msg, env=None): """Returns the dict-like object used to make standard substitutions on text sent to IRC. Usually you'll want to use :py:func:`standardSubstitute` instead, which runs the actual substitution itself.""" 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': 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: channel = msg.channel or '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) return vars def standardSubstitute(irc, msg, text, env=None): """Do the standard set of substitutions on text, and return it""" vars = standardSubstitutionVariables(irc, msg, 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)) def parseCapabilityKeyValue(s): """Parses a key-value string, in the format used by 'sts' and 'draft/multiline.""" d = {} for kv in s.split(','): if '=' in kv: (k, v) = kv.split('=', 1) d[k] = v else: d[kv] = None return d def parseStsPolicy(logger, policy, tls_connection): parsed_policy = parseCapabilityKeyValue(policy) for key in ('port', 'duration'): if key == 'duration' and not tls_connection: if key in parsed_policy: del parsed_policy[key] continue elif key == 'port' and tls_connection: if key in parsed_policy: del parsed_policy[key] continue if parsed_policy.get(key) is None: logger.error('Missing or empty "%s" key in STS policy. ' 'Ignoring policy.', key) return None try: parsed_policy[key] = int(parsed_policy[key]) except ValueError: logger.error('Expected integer as value for key "%s" in STS ' 'policy, got %r instead. Ignoring policy.', key, parsed_policy[key]) return None return parsed_policy def makeLabel(): """Returns a unique label for outgoing message tags. Unicity is not guaranteed across restarts. Returns should be handled as opaque strings, using only equality. This is used for """ return str(uuid.uuid4()) 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/log.py0000644000175000017500000004027514535072470014052 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 sys import time import types import atexit import logging import logging.handlers 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.handlers.WatchedFileHandler): def emit(self, record): try: try: super().emit(record) except (UnicodeError, TypeError): # the above line took care of calling reopenIfNeeded(), even # if it raised one of these exceptions. msg = self.format(record) self.stream.write(repr(msg)) self.stream.write(os.linesep) 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) def _mkDirs(): global _logDir, pluginLogDir _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) _mkDirs() 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. if sys.version_info >= (3, 8): formatter = Formatter( 'NEVER SEEN; IF YOU SEE THIS, FILE A BUG!', validate=False) pluginFormatter = PluginFormatter( 'NEVER SEEN; IF YOU SEE THIS, FILE A BUG!', validate=False) else: 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 ircutils.warning = warning 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) if sys.version_info >= (3, 8): _stdoutFormatter = ColorizedFormatter( 'IF YOU SEE THIS, FILE A BUG!', validate=False) else: _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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/plugin.py0000644000175000017500000002027314535072470014563 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 re import sys import time import os.path import linecache import importlib.util try: import pkg_resources except ImportError: pkg_resources = None if not hasattr(importlib.util, 'module_from_spec'): # Python < 3.5 import imp from . import callbacks, conf, log, registry ENTRYPOINT_GROUPS = [ 'limnoria.plugins', ] installDir = os.path.dirname(sys.modules[__name__].__file__) _pluginsDir = os.path.join(installDir, 'plugins') class Deprecated(ImportError): pass def loadPluginFromEntrypoint(name): if pkg_resources: for entrypoint_group in ENTRYPOINT_GROUPS: for entrypoint in pkg_resources.iter_entry_points(entrypoint_group): if entrypoint.name.lower() == name.lower(): return entrypoint.load() return None 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) module = None 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: module = loadPluginFromEntrypoint(name) if module is None: raise ImportError(name) if module is None: # Found by listing files; must now import it try: if hasattr(importlib.util, 'module_from_spec'): # Python >= 3.5 spec = importlib.machinery.PathFinder.find_spec(name, pluginDirs) if spec is None or spec.loader is None: # spec is None if 'name' can't be found; and # spec.loader might be None in some rare occasions as well # (eg. for namespace packages) raise ImportError(name) module = importlib.util.module_from_spec(spec) sys.modules[module.__name__] = module spec.loader.exec_module(module) else: # Python < 3.5 moduleInfo = imp.find_module(name, pluginDirs) 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.""" loadedAt = time.time() 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) loadTime = time.time() - loadedAt if loadTime > 1: log.warning("Loaded plugin %s in %s ms.", plugin, int(loadTime*1000)) else: log.debug("Loaded plugin %s in %s ms", plugin, int(loadTime*1000)) 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/questions.py0000644000175000017500000001233614535072470015320 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/registry.py0000644000175000017500000010272514535072470015140 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import os import time import json import codecs import string import textwrap from . import utils, i18n, ircutils 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) if hasattr(time, 'monotonic'): monotonic_time = time.monotonic else: # fallback for python < 3.3 monotonic_time = time.time _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, encoding='utf8') 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-continuation. 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._wasSet: 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.""" __slots__ = ('__parent', '_default', '_showDefault', '_help', '_callbacks', 'value', '_networkValue', '_channelValue', '_opSettable') 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 = [] self._networkValue = False self._channelValue = False if setDefault: self.setValue(default) def _makeChild(self, attr, s): v = self.__class__(self._default, self._help) v.set(s) v._wasSet = False if self._networkValue and self._channelValue: # If this is both a network-specific and channel-specific value, # then the child is (only) channel-specific. v._networkValue = False v._channelValue = True v._supplyDefault = True else: # Otherwise, the child is neither network-specific or # channel-specific. v._supplyDefault = False v._help = '' # Clear this so it doesn't print a bazillion times. self.register(attr, v) return v 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 getSpecific(self, network=None, channel=None, check=True, fallback_to_channel=True): """Gets the network-specific and/or channel-specific value of this Value. If ``check=True`` (the default), this will raise an error if ``network`` (resp. ``channel``) is provided but this Value is not network-specific (resp. channel-specific). If ``check=False``, then ``network`` and/or ``channel`` may be silently ignored. If ``fallback_to_channel=True`` (the default) and the network-specific + channel-specific value is not set, but the channel-specific value is set, it will return the latter. This is useful to upgrade from existing bot configuration, that did not support network-specific values; but it may be undesirable when setting new values. """ if network and not self._networkValue: if check: raise NonExistentRegistryEntry('%s is not network-specific' % self._name) else: network = None if channel and not self._channelValue: if check: raise NonExistentRegistryEntry('%s is not channel-specific' % self._name) else: channel = None # It may seem weird to check channel/network validity here, # but we need to prevent plugins from passing garbage values. # # For example, LinkRelay has an inFilter() function that called # self.registryValue('...', msg.args[0]) no matter the command. This # means that, every time the bot sends a 'PING :' from to # network, LinkRelayed called # self.registryValue('...', ''), which calls this # function. # # We then get the proper group, and do .get(''), which # causes a new variable to be registered. # And if the values have a high cardinality (eg. with timestamps), # this creates *a lot* of values, thereby leaking megabytes of memory # a week. # # Ideally, we would want to not register these variables, but it's # complicated for multiple reasons, including: # # 1. if two plugins .get() them, store them for a little while, then # both set them, we have to take care of it. # 2. if .network.channel is modified, then we need to register # both .channel and .network. (and what if two plugins are doing # this concurrently with different channels?) # 3. we could also have a task run in the upkeep function, but we have # issues again with plugins that keep a reference. # # All in all, this is not ideal, but afaict, this is the least bad # solution. And let's hope no one bruteforces valid channel names. # If you have a better solution, please let us know! # # Also, we're setting them to None instead of raising an error in # order not to break existing plugins. if channel and not ircutils.isChannel(channel): channel = None if network: from . import world # put here to work around circular dependencies if world.getIrc(network) is None: network = None if network and channel: # The complicated case. We want a net+chan specific value, # which may come in three different ways: # # 1. it was set explicitly net+chan # 2. it's inherited from a net specific value (which may itself be # inherited from the base value) # 3. it's inherited from the chan specific value (which is not a # actually a parent in the registry tree, but we need this to # load configuration from old bots). # 4. it was never set at all # # The choice between 2 and 3 is done by checking which of the # net-specific and chan-specific values was set explicitly by # a user/admin. In case both were, the net-specific value is used # (there is no particular reason for this, I just think it makes # more sense). network_value = self.get(':' + network) network_channel_value = network_value.get(channel) channel_value = self.get(channel) if network_value._wasSet or network_channel_value._wasSet: # cases 1 and 2 return network_channel_value elif channel_value._wasSet and fallback_to_channel: # case 3 return channel_value else: # case 4 return network_channel_value elif network: return self.get(':' + network) elif channel: return self.get(channel) else: return self def setName(self, *args): if self._name == 'unset': self._lastModified = 0 self.__parent.setName(*args) self._lastModified = monotonic_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) # redundant as setValue() already sets it, but it avoids really hard # bugs if subclasses mess with _setValue. self._wasSet = True 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._setValue(v, inherited=False) def _setValue(self, v, inherited): """Like setValue, but accepted an extra 'inherited' argument. inherited=True means the value is inherited from the parent, so if the parent gets a new value, this group will get the new value as well.""" self._lastModified = monotonic_time() self.value = v if self._supplyDefault: for (name, child) in list(self._children.items()): if not child._wasSet: child._setValue(v, inherited=True) # We call the callback once everything is clean for callback, args, kwargs in self._callbacks: callback(*args, **kwargs) self._wasSet = not inherited 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() 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).""" __slots__ = () 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() 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.""" __slots__ = () 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.""" __slots__ = () 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.""" __slots__ = () 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.""" __slots__ = () 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.""" __slots__ = () 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].""" __slots__ = ('__parent',) 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.""" __slots__ = () 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): # Don't call self(), it might be overridden; and this can cause various # problem (eg. registry.Json.__call__ returns non-strings, # conf.Directory.__call__ has filesystem side-effects that we shouldn't # trigger here, etc.) s = Value.__call__(self) if self._needsQuoting(s): s = repr(s) return s class OnlySomeStrings(String): __slots__ = ('__parent', '__dict__') # unfortunately, __dict__ is needed # to set __doc__. 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 v in self.validStrings: self.__parent.setValue(v) else: self.error(v) class NormalizedString(String): __slots__ = ('__parent') 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): __slots__ = () 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): __slots__ = () 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, default, *args, **kwargs): # We're not supposed to do convertions here, BUT this is needed # when the value is set programmatically because the value # plugins set (a string) is not the same as the one they get # (a compiled pattern object) default = self._convertFromString(default) super().__init__(default, *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 _convertFromString(self, s): if s: # We need to preserve the original string, as it's shown in # the user interface and the config file. # It might be tempting to set the original string as an # attribute, but doing so would result in inconsistent states # for childs of this variable, should they be reset, or the # value of there parent change. return (s, utils.str.perlReToPythonRe(s)) else: return None def set(self, s): try: v = self._convertFromString(s) except ValueError as e: self.error(e) else: super().set(v) def _setValue(self, v, *args, **kwargs): if v is not None and (not isinstance(v, tuple) or len(v) != 2): raise InvalidRegistryValue( 'Can\'t setValue a regexp, there would be an inconsistency ' 'between the regexp and the recorded string value. ' 'Use .set() instead.') super()._setValue(v, *args, **kwargs) def __call__(self): value = super().__call__() if value is None: return None else: return value[1] def __str__(self): value = super().__call__() if value is None: return '' else: return value[0] class SeparatedListOf(Value): __slots__ = () 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): __slots__ = () def splitter(self, s): return s.split() joiner = ' '.join class SpaceSeparatedListOfStrings(SpaceSeparatedListOf): __slots__ = () Value = String class SpaceSeparatedSetOfStrings(SpaceSeparatedListOfStrings): __slots__ = () List = set sorted = True class CommaSeparatedListOfStrings(SeparatedListOf): __slots__ = () Value = String def splitter(self, s): return re.split(r'\s*,\s*', s) joiner = ', '.join class CommaSeparatedSetOfStrings(SeparatedListOf): __slots__ = () List = set Value = String sorted = True def splitter(self, s): return re.split(r'\s*,\s*', s) joiner = ', '.join class TemplatedString(String): __slots__ = () 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): __slots__ = () # 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/schedule.py0000644000175000017500000001401414535072470015055 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ 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 makePeriodicWrapper( self, f, t, name=None, args=[], kwargs={}, count=None): """Returns a function that will run and re-schedule itself every t seconds.""" def wrapper(): nonlocal count try: f(*args, **kwargs) finally: # Even if it raises an exception, let's schedule it. if count is not None: count -= 1 if count is None or count > 0: return self.addEvent(wrapper, time.time() + t, name) return wrapper def addPeriodicEvent( self, f, t, name=None, now=True, args=[], kwargs={}, count=None): """Adds a periodic event that is called every t seconds.""" wrapper = self.makePeriodicWrapper( f, t, name, args, kwargs, 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: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3697546 limnoria-2023.11.18/src/scripts/0000755000175000017500000000000014535072535014400 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/__init__.py0000644000175000017500000000000014535072470016475 0ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria.py0000644000175000017500000003701614535072470016571 0ustar00valval### # Copyright (c) 2003-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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. ### """ This is the main program to run Supybot. """ import re import os import sys import atexit import shutil import signal from io import StringIO # Import this after version check since this will fail on Python 2 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 run(): import supybot.log as log import supybot.conf as conf import supybot.world as world import supybot.drivers as drivers import supybot.ircmsgs as ircmsgs import supybot.schedule as schedule import supybot.httpserver as httpserver # 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.') def 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 a security risk since it allows anyone to run ' 'any command on your bot, so we advise not to use this.') parser.add_option('', '--allow-root', action='store_true', dest='allowRoot', help='Determines whether the bot will be allowed to run ' 'as root. This should not be used except in special ' 'circumstances, such as running inside a containerized ' 'environment.') 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('Running as root is not supported by default (see --allow-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. 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) i18n.getLocaleFromRegistryCache() 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 # This may take some resources, and it does not need to run while booting, 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('run()', '%s-%i.prof' % (nick, time.time())) else: run() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria_adduser.py0000644000175000017500000001221214535072470020267 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot 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) != 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria_botchk.py0000644000175000017500000001251714535072470020122 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 sys import optparse import subprocess # 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. # from supybot import conf 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) def main(): 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) if __name__ == '__main__': main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria_plugin_create.py0000644000175000017500000002445014535072470021470 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 __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) 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 from supybot import utils, plugins, ircutils, callbacks from supybot.commands import * from supybot.i18n import PluginInternationalization _ = PluginInternationalization('%s') 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 from supybot import conf, 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 sys import supybot from supybot import world # Use this for the version of this plugin. __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 importlib 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() setupTemplate = ''' %s from supybot.setup import plugin_setup plugin_setup( %r, ) '''.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('-a', '--author', '--real-name', action='store', dest='realName', help='Determines who the copyright is ' 'assigned to.') parser.add_option('-d', '--desc', action='store', dest='desc', help='Short description of plugin.') (options, args) = parser.parse_args() if options.name: name = options.name threaded = options.threaded 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?') if options.realName: realName = options.realName else: realName = something(textwrap.dedent(""" What is your 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('setup.py', setupTemplate % (copyright, name)) 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) def 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.""") if __name__ == '__main__': main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria_plugin_doc.py0000644000175000017500000003402114535072470020765 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2005, Ali Afshar # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 sys import shutil import string import logging import supybot 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: sqlite3 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 TITLE_TEMPLATE = 'Documentation for the $name plugin for Supybot' class PluginDoc(object): def __init__(self, mod, titleTemplate): self.mod = mod self.inst = self.mod.Class(None) self.name = self.inst.name() self.appendExtraBlankLine = False self.titleTemplate = string.Template(titleTemplate) self.lines = [] def appendLine(self, line, indent=0): line = line.strip() indent = ' ' * indent # this looked like a good idea, but it's actually breaking lines # in the middle of ReST elements: #lines = textwrap.wrap(line, 79, # initial_indent=indent, # subsequent_indent=indent) lines = [indent + line.replace('\n', '\n' + indent)] self.lines.extend(lines) if self.appendExtraBlankLine: self.lines.append('') def renderRST(self): self.appendExtraBlankLine = False self.appendLine('.. _plugin-%s:' % self.name) self.lines.append('') s = self.titleTemplate.substitute(name=self.name) self.appendLine(s) self.appendLine('=' * len(s)) self.lines.append('') self.appendLine('Purpose') self.appendLine('-------') self.lines.append('') 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.lines.append('') # We add spaces at the beginning in case the docstring does not # start with a newline (the default, unfortunately) self.appendLine(textwrap.dedent(" "* 1000 + cdoc).lstrip()) self.lines.append('') commands = self.inst.listCommands() if len(commands): self.lines.append('.. _commands-%s:\n' % self.name) self.appendLine('Commands') self.appendLine('--------') self.lines.append('') for command in commands: log.debug('command: %s', command) self.lines.append('.. _command-%s-%s:\n' % (self.name.lower(), command.replace(' ', '.'))) 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.lines.append('.. _conf-%s:\n' % self.name) self.appendLine('Configuration') self.appendLine('-------------') self.lines.append('') 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, kind, help, default, indent) = confValues self.appendLine('.. _conf-%s:' % name, indent - 1) self.lines.append('\n') self.appendLine('%s' % name, indent - 1) if default: self.appendLine( ('This config variable defaults to %s, %s') % (default, kind), indent) else: self.appendLine('This %s' % kind, indent) if help: self.lines.append('') self.appendLine(help, indent) self.lines.append('') return '\n'.join(self.lines) + '\n' def renderSTX(self): self.appendExtraBlankLine = True self.appendLine(self.titleTemplate.substitute(name=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, kind, help, default, indent) = confValues self.appendLine('* %s' % name, indent - 1) if default: self.appendLine( ('This config variable defaults to %s, %s') % (default, kind), indent) else: self.appendLine('This %s' % kind, 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 if isinstance(v, registry.Value): try: default = utils.str.dqrepr(str(v)) help = v.help() except registry.NonExistentRegistryEntry: pass else: cv = '' if v._channelValue else ' not' nv = '' if v._networkValue else ' not' kind = ( 'is%s network-specific, and is%s channel-specific.' % (nv, cv) ) else: help = '' default = '' kind = 'is a group of:' yield (name, kind, help, default, indent) for confValues in self.genConfig(v, indent): yield confValues def genDoc(m, options): Plugin = PluginDoc(m, options.titleTemplate) print('Generating documentation for %s...' % Plugin.name) outputFilename = string.Template(options.outputFilename).substitute( name=Plugin.name, format=options.format) path = os.path.join(options.outputDir, outputFilename) 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() def 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('--output-filename', dest='outputFilename', default='$name.$format', help='Specifies the path of individual output files. ' 'If present in the value, "$name" will be substituted ' 'with the plugin\'s name and "$format" with the value ' 'if --format.') parser.add_option('-f', '--format', dest='format', choices=['rst', 'stx'], default='rst', 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.') parser.add_option('--title-template', default=TITLE_TEMPLATE, dest='titleTemplate', help='Template string for the title of generated files') (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 == '__pycache__': continue if pluginName.endswith('.py'): pluginName = pluginName[:-3] try: pluginModule = plugin.loadPluginModule(pluginName) except ImportError as e: s = 'Failed to load plugin %s: %s\n' \ '(pluginDirs: %s)' % (pluginName, e, conf.supybot.directories.plugins()) error(s) plugins.add(pluginModule) for Plugin in plugins: genDoc(Plugin, options) if options.clean: # We are about to remove the log dir; so trying to write anything # (such as "Shutdown initiated." and friends, from atexit callbacks) # would result in errors. log._handler.setLevel(logging.CRITICAL) shutil.rmtree(conf.supybot.directories.log()) shutil.rmtree(conf.supybot.directories.conf()) shutil.rmtree(conf.supybot.directories.data()) if __name__ == '__main__': main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=78: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/src/scripts/limnoria_reset_password.py0000644000175000017500000001054614535072477021723 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot 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='new password for the user.') (options, args) = parser.parse_args() if len(args) != 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) # Success! except KeyError: # Failure. No such user exists. Try another name. output('That user doesn\'t exist. Try another name.') name = '' else: try: _ = ircdb.users.getUser(options.name) name = options.name except KeyError: # Same as above. We exit here instead. output('That user doesn\'t exist. Try another name.') sys.exit(-1) if not options.password: password = getpass('Please enter new password for %s: ' % name) else: password = options.password user = ircdb.users.getUser(name) user.setPassword(password) ircdb.users.setUser(user) ircdb.users.flush() ircdb.users.close() print('User %s\'s password reset!' % name) def main(): try: _main() except KeyboardInterrupt: pass if __name__ == '__main__': main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria_test.py0000644000175000017500000002330714535072470017626 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2011, James McCoy # Copyright (c) 2010-2021, 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 sys import time import shutil import fnmatch 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.backup: /dev/null supybot.directories.conf: %(base_dir)s/test-conf supybot.directories.data: %(base_dir)s/test-data 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.networks.testnet1.server: should.not.need.this supybot.networks.testnet2.server: should.not.need.this supybot.networks.testnet3.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) def 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('--clean-after', action='store_true', default=False, dest='clean_after', help='Cleans the various data/conf/logs' 'directories after 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. if options.excludePlugins: exclusionPattern = re.compile('|'.join( fnmatch.translate(pat) for pat in options.excludePlugins )) else: exclusionPattern = None for pluginDir in options.pluginsDirs: for name in glob.glob(os.path.join(pluginDir, '*')): if (exclusionPattern is None or not exclusionPattern.match(name)) \ 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: 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()) log._mkDirs() 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 options.clean_after: log.setLevel(100) # don't log anything anymore shutil.rmtree(conf.supybot.directories.log()) shutil.rmtree(conf.supybot.directories.conf()) shutil.rmtree(conf.supybot.directories.data()) if result.wasSuccessful(): sys.exit(0) else: sys.exit(1) if __name__ == '__main__': main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/scripts/limnoria_wizard.py0000644000175000017500000011156614535072470020154 0ustar00valval#!/usr/bin/env python3 ### # Copyright (c) 2003-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, 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 __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) 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 ' 'as root. This should not be used except in special ' 'circumstances, such as running inside a containerized ' 'environment.') 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('', '--allow-bin', action='store_true', dest='allowBin', help='Determines whether the wizard will be allowed to ' 'run directly in a directory for binaries (eg. ' '/usr/bin, /usr/local/bin, or ~/.local/bin). ' 'You should not do this unless you want it to ' 'create multiple non-binary files in directory ' 'that should only contain binaries.') 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('Running as root is not supported by default (see --allow-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.split(os.getcwd())[-1] == 'bin') and not options.allowBin: error('Please, don\'t run this in a "bin" 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 = expect('What language do you want to use?', i18n.SUPPORTED_LANGUAGES, 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 irc.libera.chat, for instance, you should answer this question with 'libera' or 'liberachat' (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.networks..sasl.{username,password} output("""Most networks allow you to create an account, using services typically known as NickServ. If you already have one, you can configure the bot to use that account by proving your username and password (also known as "SASL plain"). It is usually not a requirement.""") if yn('Do you want your bot to use a network account?', default=False): while True: username = something('What account name should the bot use?') password = getpass('What is the password of that account?') if not username: if yn('Missing username. Do you want to try again?'): continue if not password: if yn('Missing password. Do you want to try again?'): continue network.sasl.username.set(username) network.sasl.password.set(password) break # 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_input = something('What channels?', default=defaultChannels) channels = [] error_ = False for channel in channels_input.split(): L = channel.split(',') if len(L) == 0: continue elif len(L) == 1: (channel,) = L channels.append(channel) elif len(L) == 2: (channel, key) = L channels.append(channel) network.channels.key.get(channel).setValue(key) else: output("""Too many commas in %s.""" % channel) error_ = True break if error_: break try: network.channels.set(' '.join(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(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(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 != '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)) def 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.""") if __name__ == '__main__': main() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/setup.py0000644000175000017500000001126614535072470014427 0ustar00valval### # Copyright (c) 2020-2021, 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 sys try: import setuptools except ImportError: setuptools = None from . import authors if setuptools: def plugin_setup(plugin, **kwargs): """Wrapper of setuptools.setup that auto-fills some fields for Limnoria plugins.""" if isinstance(plugin, str): if plugin in sys.modules: plugin = sys.modules[plugin] else: setup_path = sys.modules['__main__'].__file__ sys.path.insert(0, os.path.join(os.path.dirname(setup_path), '..')) plugin = __import__(plugin) author = plugin.__author__ version = plugin.__version__ url = plugin.__url__ maintainer = getattr(plugin, '__maintainer__', authors.unknown) kwargs.setdefault('package_data', {}).setdefault('', []).append('*.po') capitalized_name = plugin.Class.__name__ kwargs.setdefault( 'name', 'limnoria-%s' % capitalized_name.lower()) if version: kwargs.setdefault('version', version) if url: kwargs.setdefault('url', url) if 'long_description' not in kwargs: readme_files = [ ('text/x-rst', 'README.rst'), ('text/markdown', 'README.md'), ] for (mimetype, filename) in readme_files: readme_path = os.path.join( os.path.dirname(plugin.__file__), filename) if os.path.isfile(readme_path): with open(readme_path, 'r') as fd: kwargs['long_description'] = fd.read() kwargs['long_description_content_type'] = mimetype break module_name = kwargs['name'].replace('-', '_') if 'packages' not in kwargs: kwargs["packages"] = [module_name] + [ "%s.%s" % (module_name, package_name.replace('-', '_')) for package_name in setuptools.find_packages(where=".") ] kwargs.setdefault('package_dir', {module_name: '.'}) kwargs.setdefault('entry_points', { 'limnoria.plugins': '%s = %s' % (capitalized_name, module_name)}) kwargs.setdefault('install_requires', []).append('limnoria') kwargs.setdefault('classifiers', []).extend([ 'Environment :: Plugins', 'Programming Language :: Python :: 3', 'Topic :: Communications :: Chat', ]) if author is not authors.unknown: if author.name or author.nick: kwargs.setdefault('author', author.name or author.nick) if author.email: kwargs.setdefault('author_email', author.email) if maintainer is not authors.unknown: if maintainer.name or maintainer.nick: kwargs.setdefault( 'maintainer', maintainer.name or maintainer.nick) if maintainer.email: kwargs.setdefault('maintainer_email', maintainer.email) setuptools.setup( **kwargs) else: def plugin_setup(plugin, **kwargs): raise ImportError('setuptools') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/shlex.py0000644000175000017500000002000614535072470014402 0ustar00valval"""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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/test.py0000644000175000017500000006154414535072470014252 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2011, James McCoy # Copyright (c) 2010-2021, 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 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 real_time = time.time mock_time_offset = 0 def mockTime(): """Wrapper for time.time() that adds an offset, eg. for skipping after a timeout expired.""" return real_time() + mock_time_offset def timeFastForward(extra_offset): global mock_time_offset mock_time_offset += extra_offset def setupMockTime(): time.time = mockTime def teardownMockTime(): time.time = real_time 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(name='test'): irc = irclib.Irc(name) # 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) setupMockTime() unittest.TestCase.setUp(self) def tearDown(self): for irc in world.ircs[:]: irc._reallyDie() teardownMockTime() 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 = {} timeout = None def __init__(self, methodName='runTest'): if self.timeout is None: 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) # Choose a random port for tests using the HTTP server conf.supybot.servers.http.port.setValue(0) 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 = real_time() response = self.irc.takeMsg() while response is None and real_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.assertTrue(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.assertFalse(m.args[1].startswith('Error:'), '%r errored: %s' % (query, m.args[1])) self.assertFalse(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.assertTrue(msg in lastGetHelp, '%s is not the help (%s)' % (m.args[1], lastGetHelp)) return m def assertNoResponse(self, query, timeout=None, **kwargs): if timeout is None: timeout = 0 # timeout=0 does not wait at all for an answer after the command # function finished running. This is fine for non-threaded # plugins because they usually won't answer anything after that; # but not for threaded plugins. # TODO: also detect threaded commands for cb in self.irc.callbacks: if cb.threaded: timeout = self.timeout break m = self._feedMsg(query, timeout=timeout, **kwargs) self.assertFalse(m, 'Unexpected response: %r' % m) return m def assertSnarfNoResponse(self, query, timeout=None, **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.assertTrue(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.assertTrue(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.assertTrue(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.assertTrue(ircmsgs.isAction(m)) s = ircmsgs.unAction(m) self.assertTrue(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.assertTrue(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.assertTrue(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, nick, forceSetup) self.irc.feedMsg(ircmsgs.join(self.channel, prefix=self.prefix)) m = self.irc.takeMsg() self.assertFalse(m is None, 'No message back from joining channel.') self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.assertFalse(m is None, 'No message back from joining channel.') self.assertEqual(m.command, 'MODE') m = self.irc.takeMsg() self.assertFalse(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 = real_time() response = self.irc.takeMsg() while response is None and real_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): 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.BytesIO() rfile = minisix.io.BytesIO() connection = FakeHTTPConnection(wfile, rfile) connection.putrequest(method, url) connection.endheaders() rfile.seek(0) handler = TestRequestHandler(rfile, wfile) wfile.seek(0) 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/unpreserve.py0000644000175000017500000000743514535072470015470 0ustar00valval### # Copyright (c) 2004-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### class Reader(object): """Opens a file and reads it in blocks, using the `Creator` class to instantiate an object for each of the blocks. The format is of the form: ``` entry_type entry_id1 command1 arg1 arg1b command2 arg2 entry_type entry_id2 command3 arg3 arg13 ``` When reading this file, the `Creator` will be instantiated with the provided args and kwargs to a `creator` object, whose methods will then be called in this pattern: ``` creator.entry_type("entry_id1", 1) creator.command1("arg1 arg1b", 2) creator.command2("arg2", 3) creator.finish() creator.entry_type'entry_id2", 5) creator.command3("arg3 arg3b", 6) creator.finish() ``` """ 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, encoding='utf8')) 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: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3697546 limnoria-2023.11.18/src/utils/0000755000175000017500000000000014535072535014051 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/__init__.py0000644000175000017500000000501714535072470016163 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 . 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 # 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, time, transaction, web # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/crypt.py0000644000175000017500000000331514535072470015564 0ustar00valval### # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 hashlib import md5 from hashlib import sha1 as sha # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/error.py0000644000175000017500000000371014535072470015553 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/file.py0000644000175000017500000002240314535072470015341 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 time import codecs import random import shutil import os.path from . import crypt def sanitizeName(filename): """Removes / from filenames and escapes them if they are '.' or '..'.""" filename = filename.replace('/', '') if filename == '.': return '_' elif filename == '..': return '__' else: return filename 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'. If ``encoding`` is None (or not provided), files are open in `utf8` regardless of the system locale.""" 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/gen.py0000644000175000017500000002650314535072470015200 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 __future__ import print_function import os import sys import ast import textwrap import warnings import functools import traceback import collections.abc from . import crypt from .str import format from .file import mktemp from . import minisix # will be replaced by supybot.i18n.install() _ = lambda x: x 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 node.__class__ is ast.NameConstant: return True elif sys.version_info[0:2] >= (3, 8) and \ node.__class__ is ast.Constant: return True else: return False if checkNode(node): # 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. """ __slots__ = () 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.abc.MutableMapping): __slots__ = ('data',) 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): __slots__ = () 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/iter.py0000644000175000017500000001213214535072470015363 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 __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)) def grouper(iterable, n, fillvalue=None): """Collect data into fixed-length chunks or blocks grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx From https://docs.python.org/3/library/itertools.html#itertools-recipes""" args = [iter(iterable)] * n return zip_longest(*args, fillvalue=fillvalue) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/math_evaluator.py0000644000175000017500000001406314535072470017440 0ustar00valval### # Copyright (c) 2019-2021, 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. ### """A safe evaluator for math expressions using Python syntax. Unlike eval(), it can be run on untrusted input. """ import ast import math import cmath import operator class InvalidNode(Exception): pass def filter_module(module, safe_names): return dict([ (name, getattr(module, name)) for name in safe_names if hasattr(module, name) ]) UNARY_OPS = { ast.UAdd: lambda x: x, ast.USub: lambda x: -x, } BIN_OPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Mod: operator.mod, ast.Pow: operator.pow, ast.BitXor: operator.xor, ast.BitOr: operator.or_, ast.BitAnd: operator.and_, } MATH_CONSTANTS = 'e inf nan pi tau'.split() SAFE_MATH_FUNCTIONS = ( 'acos acosh asin asinh atan atan2 atanh copysign cos cosh degrees erf ' 'erfc exp expm1 fabs fmod frexp fsum gamma hypot ldexp lgamma ' 'log1p log2 modf pow radians remainder sin sinh tan tanh' ).split() SAFE_CMATH_FUNCTIONS = ( 'acos acosh asin asinh atan atanh cos cosh exp inf infj ' 'nanj phase polar rect sin sinh tan tanh tau' ).split() SAFE_ENV = filter_module(math, MATH_CONSTANTS + SAFE_MATH_FUNCTIONS) SAFE_ENV.update(filter_module(cmath, SAFE_CMATH_FUNCTIONS)) def _sqrt(x): if isinstance(x, complex) or x < 0: return cmath.sqrt(x) else: return math.sqrt(x) def _log(x): if isinstance(x, complex) or x < 0: return cmath.log(x) else: return math.log(x) def _log10(x): if isinstance(x, complex) or x < 0: return cmath.log10(x) else: return math.log10(x) def _cbrt(x): return math.pow(x, 1.0/3) def _factorial(x): if x<=10000: return float(math.factorial(x)) else: raise Exception('factorial argument too large') SAFE_ENV.update({ 'i': 1j, 'abs': abs, 'max': max, 'min': min, 'round': lambda x, y=0: round(x, int(y)), 'factorial': _factorial, 'sqrt': _sqrt, 'cbrt': _cbrt, 'log': _log, 'log10': _log10, 'ceil': lambda x: float(math.ceil(x)), 'floor': lambda x: float(math.floor(x)), }) UNSAFE_ENV = SAFE_ENV.copy() # Add functions that return integers UNSAFE_ENV.update(filter_module(math, 'ceil floor factorial gcd'.split())) # It would be nice if ast.literal_eval used a visitor so we could subclass # to extend it, but it doesn't, so let's reimplement it entirely. class SafeEvalVisitor(ast.NodeVisitor): def __init__(self, allow_ints, variables=None): self._allow_ints = allow_ints self._env = UNSAFE_ENV if allow_ints else SAFE_ENV if variables: self._env = self._env.copy() self._env.update(variables) def _convert_num(self, x): """Converts numbers to complex if ints are not allowed.""" if self._allow_ints: return x else: x = complex(x) if x.imag == 0: x = x.real # Need to use string-formatting here instead of str() because # use of str() on large numbers loses information: # str(float(33333333333333)) => '3.33333333333e+13' # float('3.33333333333e+13') => 33333333333300.0 return float('%.16f' % x) else: return x def visit_Expression(self, node): return self.visit(node.body) def visit_Num(self, node): return self._convert_num(node.n) def visit_Name(self, node): id_ = node.id.lower() if id_ in self._env: return self._env[id_] else: raise NameError(node.id) def visit_Call(self, node): func = self.visit(node.func) args = map(self.visit, node.args) # TODO: keywords? return func(*args) def visit_UnaryOp(self, node): op = UNARY_OPS.get(node.op.__class__) if op: return op(self.visit(node.operand)) else: raise InvalidNode('illegal operator %s' % node.op.__class__.__name__) def visit_BinOp(self, node): op = BIN_OPS.get(node.op.__class__) if op: return op(self.visit(node.left), self.visit(node.right)) else: raise InvalidNode('illegal operator %s' % node.op.__class__.__name__) def generic_visit(self, node): raise InvalidNode('illegal construct %s' % node.__class__.__name__) def safe_eval(text, allow_ints, variables=None): node = ast.parse(text, mode='eval') return SafeEvalVisitor(allow_ints, variables=variables).visit(node) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/minisix.py0000644000175000017500000000751214535072470016106 0ustar00valval### # Copyright (c) 2014-2021, 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/net.py0000644000175000017500000001457614535072470015224 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2011, 2013, James McCoy # Copyright (c) 2010-2021, 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. ### """ Simple utility modules. """ import re import ssl import socket import hashlib import contextlib from .web import _ipAddr, _domain emailRe = re.compile(r"^\S+@(%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.') @contextlib.contextmanager def _prefix_ssl_error(prefix): try: yield except ssl.SSLError as e: raise ssl.SSLError( e.args[0], '%s failed: %s' % (prefix, e.args[1]), *e.args[2:]) \ from None def ssl_wrap_socket(conn, hostname, logger, certfile=None, trusted_fingerprints=None, verify=True, ca_file=None, **kwargs): with _prefix_ssl_error('creating SSL context'): context = ssl.create_default_context(**kwargs) if ca_file: with _prefix_ssl_error('loading CA certificate'): context.load_verify_locations(cafile=ca_file) elif trusted_fingerprints or not verify: # Do not use Certification Authorities context.check_hostname = False context.verify_mode = ssl.CERT_NONE if certfile: with _prefix_ssl_error('loading client certfile'): context.load_cert_chain(certfile) with _prefix_ssl_error('establishing TLS connection'): conn = context.wrap_socket(conn, server_hostname=hostname) if trusted_fingerprints: check_certificate_fingerprint(conn, trusted_fingerprints) return conn # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/python.py0000644000175000017500000001705614535072470015753 0ustar00valval### # Copyright (c) 2005-2009, Jeremiah Fincher # Copyright (c) 2009-2010, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import 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: try: attribute_names = dir(frame_locals[inspected]) except Exception: # For Python 2 and Pypy try: attribute_names = list( frame_locals[inspected].__dict__) except Exception: attribute_names = [] for attr_name in attribute_names: try: v = getattr(frame_locals[inspected], attr_name) except Exception: v = '' frame_locals['%s.%s' % (inspected, attr_name)] = v 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 Exception: 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/seq.py0000644000175000017500000001011114535072470015203 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/str.py0000644000175000017500000005256114535072470015242 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008-2009, James McCoy # Copyright (c) 2010-2021, 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. ### """ Simple utility functions related to strings. """ import re import sys import time import string import textwrap import functools from . import minisix from .iter import any from .structures import TwoWayDictionary try: from charade.universaldetector import UniversalDetector charadeLoaded = True except ImportError: charadeLoaded = False # will be replaced by supybot.i18n.install() _ = lambda x: x # used by supybot.i18n.reloadLocales() to (re)load the localized function of # these functions _localizedFunctions = {} _defaultFunctions = {} def internationalizeFunction(f): f_name = f.__name__ _localizedFunctions[f_name] = f _defaultFunctions[f_name] = f @functools.wraps(f) def newf(*args, **kwargs): f = _localizedFunctions[f_name] assert f is not None, "_localizedFunctions[%s] is None" % f_name return f(*args, **kwargs) return newf def _relocalizeFunctions(localizer): for f_name in list(_localizedFunctions): f = localizer.localizeFunction(f_name) or _defaultFunctions[f_name] _localizedFunctions[f_name] = f if minisix.PY3: def decode_raw_line(line): #first, try to decode using utf-8 try: line = line.decode('utf8', 'strict') except UnicodeError: # if this fails and charade is loaded, try to guess the correct encoding if charadeLoaded: u = UniversalDetector() u.feed(line) u.close() if u.result['encoding']: # try to use the guessed encoding try: line = line.decode(u.result['encoding'], 'strict') # on error, give up and replace the offending characters except UnicodeError: line = line.decode(errors='replace') else: # if no encoding could be guessed, fall back to utf-8 and # replace offending characters line = line.decode('utf8', 'replace') # if charade is not loaded, try to decode using utf-8 and replace any # offending characters else: line = line.decode('utf8', 'replace') return line else: def decode_raw_line(line): return line def rsplit(s, sep=None, maxsplit=-1): """Equivalent to str.split, except splitting from the right.""" return s.rsplit(sep, maxsplit) def normalizeWhitespace(s, removeNewline=True): r"""Normalizes the whitespace in a string; \s+ becomes one space.""" if not s: return str(s) # not the same reference starts_with_space = (s[0] in ' \n\t\r') ends_with_space = (s[-1] in ' \n\t\r') if removeNewline: newline_re = re.compile('[\r\n]+') s = ' '.join(filter(bool, newline_re.split(s))) s = ' '.join(filter(bool, s.split('\t'))) s = ' '.join(filter(bool, s.split(' '))) if starts_with_space: s = ' ' + s if ends_with_space: s += ' ' return s def distance(s, t): """Returns the levenshtein edit distance between two strings.""" n = len(s) m = len(t) if n == 0: return m elif m == 0: return n d = [] for i in range(n+1): d.append([]) for j in range(m+1): d[i].append(0) d[0][j] = j d[i][0] = i for i in range(1, n+1): cs = s[i-1] for j in range(1, m+1): ct = t[j-1] cost = int(cs != ct) d[i][j] = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+cost) return d[n][m] class MultipleReplacer: """Return a callable that replaces all dict keys by the associated value. More efficient than multiple .replace().""" # We use an object instead of a lambda function because it avoids the # need for using the staticmethod() on the lambda function if assigning # it to a class in Python 3. def __init__(self, dict_): self._dict = dict_ dict_ = dict([(re.escape(key), val) for key,val in dict_.items()]) self._matcher = re.compile('|'.join(dict_.keys())) def __call__(self, s): return self._matcher.sub(lambda m: self._dict[m.group(0)], s) def multipleReplacer(dict_): return MultipleReplacer(dict_) class MultipleRemover: """Return a callable that removes all words in the list. A bit more efficient than multipleReplacer""" # See comment of MultipleReplacer def __init__(self, list_): list_ = [re.escape(x) for x in list_] self._matcher = re.compile('|'.join(list_)) def __call__(self, s): return self._matcher.sub(lambda m: '', s) _soundextrans = MultipleReplacer(dict(list(zip(string.ascii_uppercase, '01230120022455012623010202')))) def soundex(s, length=4): """Returns the soundex hash of a given string. length=0 doesn't truncate the hash. """ s = s.upper() # Make everything uppercase. s = ''.join([x for x in s if x in string.ascii_uppercase]) if not s: raise ValueError('Invalid string for soundex: %s') firstChar = s[0] # Save the first character. s = _soundextrans(s) # Convert to soundex numbers. s = s.lstrip(s[0]) # Remove all repeated first characters. L = [firstChar] for c in s: if c != L[-1]: L.append(c) L = [c for c in L if c != '0'] s = ''.join(L) if length: s = s.ljust(length, '0')[:length] return s def dqrepr(s): """Returns a repr() of s guaranteed to be in double quotes.""" # The wankers-that-be decided not to use double-quotes anymore in 2.3. # return '"' + repr("'\x00" + s)[6:] encoding = 'string_escape' if minisix.PY2 else 'unicode_escape' if minisix.PY2 and isinstance(s, unicode): s = s.encode('utf8', 'replace') return '"%s"' % s.encode(encoding).decode().replace('"', '\\"') def quoted(s): """Returns a quoted s.""" return '"%s"' % s _openers = '{[(<' _closers = '}])>' def _getSep(s, allowBraces=False): if len(s) < 2: raise ValueError('string given to _getSep is too short: %r' % s) if allowBraces: braces = _closers else: braces = _openers + _closers if s.startswith('m') or s.startswith('s'): separator = s[1] else: separator = s[0] if separator.isalnum() or separator in braces: raise ValueError('Invalid separator: separator must not be alphanumeric or in ' \ '"%s"' % braces) return separator def perlReToPythonRe(s, allowG=False): """Converts a string representation of a Perl regular expression (i.e., m/^foo$/i or /foo|bar/) to a Python regular expression. """ opener = closer = _getSep(s, True) if opener in '{[(<': closer = _closers[_openers.index(opener)] opener = re.escape(opener) closer = re.escape(closer) matcher = re.compile(r'm?%s((?:\\.|[^\\])*)%s(.*)' % (opener, closer)) try: (regexp, flags) = matcher.match(s).groups() except AttributeError: # Unpack list of wrong size. raise ValueError('Must be of the form m/.../ or /.../') regexp = regexp.replace('\\'+opener, opener) if opener != closer: regexp = regexp.replace('\\'+closer, closer) flag = 0 g = False try: for c in flags.upper(): if c == 'G' and allowG: g = True continue flag |= getattr(re, c) except AttributeError: raise ValueError('Invalid flag: %s' % c) try: r = re.compile(regexp, flag) except re.error as e: raise ValueError(str(e)) if allowG: return (r, g) else: return r def perlReToFindall(s): """Converts a string representation of a Perl regular expression (i.e., m/^foo$/i or /foo|bar/) to a Python regular expression, with support for G flag """ (r, g) = perlReToPythonRe(s, allowG=True) if g: return lambda s: r.findall(s) else: return lambda s: r.search(s) and r.search(s).group(0) or '' def perlReToReplacer(s): """Converts a string representation of a Perl regular expression (i.e., s/foo/bar/g or s/foo/bar/i) to a Python function doing the equivalent replacement. """ sep = _getSep(s) escaped = re.escape(sep) matcher = re.compile(r's%s((?:\\.|[^\\])*)%s((?:\\.|[^\\])*)%s(.*)' % (escaped, escaped, escaped)) try: (regexp, replace, flags) = matcher.match(s).groups() except AttributeError: # Unpack list of wrong size. raise ValueError('Must be of the form s/.../.../') regexp = regexp.replace('\x08', r'\b') replace = replace.replace('\\'+sep, sep) for i in range(10): replace = replace.replace(chr(i), r'\%s' % i) g = False if 'g' in flags: g = True flags = list(filter('g'.__ne__, flags)) if isinstance(flags, list): flags = ''.join(flags) r = perlReToPythonRe(sep.join(('', regexp, flags))) if g: return lambda s: r.sub(replace, s) else: return lambda s: r.sub(replace, s, 1) _perlVarSubstituteRe = re.compile(r'\$\{([^}]+)\}|\$([a-zA-Z][a-zA-Z0-9]*)') def perlVariableSubstitute(vars, text): def replacer(m): (braced, unbraced) = m.groups() var = braced or unbraced try: x = vars[var] if callable(x): return x() else: try: return str(x) except UnicodeEncodeError: # Python 2 return str(x).encode('utf8') except KeyError: if braced: return '${%s}' % braced else: return '$' + unbraced return _perlVarSubstituteRe.sub(replacer, text) def splitBytes(word, size): # I'm going to hell for this function for i in range(4): # a character takes at most 4 bytes in UTF-8 try: word[size-i:].decode() except UnicodeDecodeError: continue else: return (word[0:size-i], word[size-i:]) assert False, (word, size) class ByteTextWrapper(textwrap.TextWrapper): def _wrap_chunks(self, words): words.reverse() # use it as a stack words = [w.encode() for w in words] lines = [b''] while words: word = words.pop(-1) if len(word) > self.width: (before, after) = splitBytes(word, self.width) words.append(after) word = before if len(lines[-1]) + len(word) <= self.width: lines[-1] += word else: lines.append(word) return [l.decode() for l in lines] def byteTextWrap(text, size, break_on_hyphens=False): """Similar to textwrap.wrap(), but considers the size of strings (in bytes) instead of their length (in characters).""" return ByteTextWrapper(width=size).wrap(text) def commaAndify(seq, comma=',', And=None): """Given a a sequence, returns an English clause for that sequence. I.e., given [1, 2, 3], returns '1, 2, and 3' """ if And is None: And = _('and') L = list(seq) if len(L) == 0: return '' elif len(L) == 1: return ''.join(L) # We need this because it raises TypeError. elif len(L) == 2: L.insert(1, And) return ' '.join(L) else: L[-1] = '%s %s' % (And, L[-1]) sep = '%s ' % comma return sep.join(L) _unCommaTheRe = re.compile(r'(.*),\s*(the)$', re.I) def unCommaThe(s): """Takes a string of the form 'foo, the' and turns it into 'the foo'.""" m = _unCommaTheRe.match(s) if m is not None: return '%s %s' % (m.group(2), m.group(1)) else: return s def ellipsisify(s, n): """Returns a shortened version of s. Produces up to the first n chars at the nearest word boundary. """ if len(s) <= n: return s else: return (textwrap.wrap(s, n-3)[0] + '...') plurals = TwoWayDictionary({}) def matchCase(s1, s2): """Matches the case of s1 in s2""" if s1.isupper(): return s2.upper() else: L = list(s2) for (i, char) in enumerate(s1[:len(s2)]): if char.isupper(): L[i] = L[i].upper() return ''.join(L) @internationalizeFunction def pluralize(s): """Returns the plural of s. Put any exceptions to the general English rule of appending 's' in the plurals dictionary. """ consonants = 'bcdfghjklmnpqrstvwxz' _pluralizeRegex = re.compile('[%s]y$' % consonants) lowered = s.lower() # Exception dictionary if lowered in plurals: return matchCase(s, plurals[lowered]) # Words ending with 'ch', 'sh' or 'ss' such as 'punch(es)', 'fish(es) # and miss(es) elif any(lowered.endswith, ['x', 'ch', 'sh', 'ss']): return matchCase(s, s+'es') # Words ending with a consonant followed by a 'y' such as # 'try (tries)' or 'spy (spies)' elif _pluralizeRegex.search(lowered): return matchCase(s, s[:-1] + 'ies') # In all other cases, we simply add an 's' to the base word else: return matchCase(s, s+'s') @internationalizeFunction def depluralize(s): """Returns the singular of s.""" consonants = 'bcdfghjklmnpqrstvwxz' _depluralizeRegex = re.compile('[%s]ies' % consonants) lowered = s.lower() if lowered in plurals: return matchCase(s, plurals[lowered]) elif any(lowered.endswith, ['ches', 'shes', 'sses']): return s[:-2] elif re.search(_depluralizeRegex, lowered): return s[:-3] + 'y' else: if lowered.endswith('s'): return s[:-1] # Chop off 's'. else: return s # Don't know what to do. def nItems(n, item, between=None): """Works like this: >>> 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 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 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 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(r'%((?:\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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/structures.py0000644000175000017500000004660014535072470016652 0ustar00valval### # Copyright (c) 2002-2009, Jeremiah Fincher # Copyright (c) 2010-2022, 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. ### """ Data structures for Python. """ import time import threading import collections.abc 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): """A queue whose elements are dropped after a certain time.""" __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): self._clearOldElements() # You may think re-checking _getTimeout() after we just called # _clearOldElements is redundant, 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.abc.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 ExpiringDict(collections.abc.MutableMapping): """An efficient dictionary that MAY drop its items when they are too old. For guaranteed expiry, use TimeoutDict. Currently, this is implemented by internally alternating two "generation" dicts, which are dropped after a certain time. """ __slots__ = ('_lock', 'old_gen', 'new_gen', 'timeout', '_last_switch') def __init__(self, timeout, items=None): self._lock = threading.Lock() self.old_gen = {} self.new_gen = {} if items is None else items self.timeout = timeout self._last_switch = time.time() def __reduce__(self): return (self.__class__, (self.timeout, dict(self))) def __repr__(self): return 'ExpiringDict(%s, %r)' % (self.timeout, dict(self)) def __getitem__(self, key): try: # Check the new_gen first, as it contains the most recent # insertion. # We must also check them in this order to be thread-safe when # _expireGenerations() runs. return self.new_gen[key] except KeyError: try: return self.old_gen[key] except KeyError: raise KeyError(key) from None def __contains__(self, key): # the two clauses must be in this order to be thread-safe when # _expireGenerations() runs. return key in self.new_gen or key in self.old_gen def __setitem__(self, key, value): self._expireGenerations() self.new_gen[key] = value def _expireGenerations(self): with self._lock: now = time.time() if self._last_switch + self.timeout < now: # We last wrote to self.old_gen a long time ago # (ie. more than self.timeout); let's drop the old_gen and # make new_gen become the old_gen # self.old_gen must be written before self.new_gen for # __getitem__ and __contains__ to be able to run concurrently # to this function. self.old_gen = self.new_gen self.new_gen = {} self._last_switch = now def clear(self): self.old_gen.clear() self.new_gen.clear() def __delitem__(self, key): self.old_gen.pop(key, None) self.new_gen.pop(key, None) def __iter__(self): # order matters keys = set(self.new_gen.keys()) | set(self.old_gen.keys()) return iter(keys) def __len__(self): # order matters return len(set(self.new_gen.keys()) | set(self.old_gen.keys())) class TimeoutDict: # Don't inherit from MutableMapping: not thread-safe """A dictionary that drops its items after they have been in the dict for a certain time. Use ExpiringDict for a more efficient implementation that doesn't require guaranteed timeout. """ __slots__ = ('_lock', 'd', 'timeout') def __init__(self, timeout, items=None): expiry = time.time() + timeout self._lock = threading.Lock() self.d = {k: (expiry, v) for (k, v) in (items or {}).items()} self.timeout = timeout def __reduce__(self): return (self.__class__, (self.timeout, dict(self))) def __repr__(self): return 'TimeoutDict(%s, %r)' % (self.timeout, dict(self)) def __getitem__(self, key): with self._lock: try: (expiry, value) = self.d[key] if expiry < time.time(): del self.d[key] raise KeyError except KeyError: raise KeyError(key) from None return value def __setitem__(self, key, value): with self._lock: self.d[key] = (time.time() + self.timeout, value) def clear(self): with self._lock: self.d.clear() def __delitem__(self, key): with self._lock: del self.d[key] def _items(self): now = time.time() with self._lock: return [ (k, v) for (k, (expiry, v)) in self.d.items() if expiry >= now] def keys(self): return [k for (k, v) in self._items()] def values(self): return [v for (k, v) in self._items()] def items(self): return self._items() def __iter__(self): return (k for (k, v) in self._items()) def __len__(self): return len(self._items()) def __eq__(self, other): return self._items() == list(other.items()) def __ne__(self, other): return not (self == other) class TruncatableSet(collections.abc.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= (3, 9): import zoneinfo else: zoneinfo = None try: import pytz except ImportError: pytz = None _IANA_TZ_RE = re.compile(r"([\w_-]+/)*[\w_-]+") class TimezoneException(Exception): pass class MissingTimezoneLibrary(TimezoneException): pass class UnknownTimeZone(TimezoneException): pass def iana_timezone(name): """Returns a :class:datetime.tzinfo object, given an IANA timezone name, eg. ``"Europe/Paris"``. This uses :class:``zoneinfo.ZoneInfo`` if available, :func:``pytz.timezone`` otherwise. May raise instances of :exc:`TimezoneException`. """ if not _IANA_TZ_RE.match(name): raise UnknownTimeZone(name) if zoneinfo: try: return zoneinfo.ZoneInfo(name) except zoneinfo.ZoneInfoNotFoundError as e: raise UnknownTimeZone(e.args[0]) from None elif pytz: try: return pytz.timezone(name) except pytz.UnknownTimeZoneError as e: raise UnknownTimeZone(e.args[0]) from None else: raise MissingTimezoneLibrary( "Could not find a timezone library. " "Update Python to version 3.9 or newer, or install pytz." ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/transaction.py0000644000175000017500000002117214535072470016751 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/utils/web.py0000644000175000017500000002464614535072470015212 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import re import 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, urlunparse from htmlentitydefs import entitydefs, name2codepoint from HTMLParser import HTMLParser from cgi import escape as html_escape 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, urlunparse from html.entities import entitydefs, name2codepoint from html.parser import HTMLParser from html import escape as html_escape 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) # Used to build defaultHeaders baseDefaultHeaders = { 'User-agent': 'Mozilla/5.0 (compatible; utils.web Limnoria module)' } # overridable by other modules/plugins. defaultHeaders = baseDefaultHeaders.copy() # 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) from e except sockerrors as e: raise Error(strError(e)) from e except InvalidURL as e: raise Error('Invalid URL: %s' % e) from e except HTTPError as e: raise Error(strError(e)) from e except URLError as e: raise Error(strError(e.reason)) from e # Raised when urllib doesn't recognize the url type except ValueError as e: raise Error(strError(e)) from 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 # From beautifulsoup (version 4.10.0, bs4/builder/__init__.py, line 391) _block_elements = set(["address", "article", "aside", "blockquote", "canvas", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", "noscript", "ol", "output", "p", "pre", "section", "table", "tfoot", "ul", "video"]) _block_elements.update({"br"}) 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=None): self.data = [] self.tagReplace = tagReplace super(HtmlToText, self).__init__() def append(self, data): self.data.append(data) def getTagReplace(self, tag): if self.tagReplace is None: if tag in _block_elements: return ' ' else: return '' else: return self.tagReplace def handle_starttag(self, tag, attr): self.append(self.getTagReplace(tag)) def handle_endtag(self, tag): self.append(self.getTagReplace(tag)) 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=None): """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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131037.0 limnoria-2023.11.18/src/version.py0000644000175000017500000000025714535072535014754 0ustar00valvalversion = '2023.11.18' try: # For import from setup.py import supybot.utils.python supybot.utils.python._debug_software_version = version except ImportError: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/src/world.py0000644000175000017500000001675414535072470014425 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### """ 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, 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) commandsProcessed = 0 ircs = [] # A list of all the IRCs. def getIrc(network): """Returns Irc object of the given network. is string and not case-sensitive.""" 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 # skipping the checks log.warning('Expected cStringIO as stdout, got %r.', sys.stdout) else: 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.""" from . import 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: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702131037.3697546 limnoria-2023.11.18/test/0000755000175000017500000000000014535072535013101 5ustar00valval././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/__init__.py0000644000175000017500000000333214535072470015211 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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. ### # We're just masquerading as a plugin :) from . import test # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test.py0000644000175000017500000000433114535072470014431 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131007.0 limnoria-2023.11.18/test/test_callbacks.py0000644000175000017500000014130014535072477016435 0ustar00valval# -*- coding: utf8 -*- ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * 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): irc = getTestIrc() oldprefixchars = str(conf.supybot.reply.whenAddressedBy.chars) nick = irc.nick 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.assertFalse(callbacks.addressed(irc, badmsg)) badmsg = ircmsgs.privmsg('#foo', '%s^: foo' % nick) self.assertFalse(callbacks.addressed(irc, badmsg)) for msg in inChannel: self.assertEqual('foo', callbacks.addressed(irc, msg), msg) msg = ircmsgs.privmsg(nick, 'foo') irc._tagMsg(msg) self.assertEqual('foo', callbacks.addressed(irc, msg)) conf.supybot.reply.whenAddressedBy.chars.set(oldprefixchars) msg = ircmsgs.privmsg('#foo', '%s::::: bar' % nick) self.assertEqual('bar', callbacks.addressed(irc, msg)) msg = ircmsgs.privmsg('#foo', '%s: foo' % nick.upper()) self.assertEqual('foo', callbacks.addressed(irc, msg)) badmsg = ircmsgs.privmsg('#foo', '%s`: foo' % nick) self.assertFalse(callbacks.addressed(irc, badmsg)) def testAddressedLegacy(self): """Checks callbacks.addressed still accepts the 'nick' argument instead of 'irc'.""" irc = getTestIrc() 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) with self.assertWarnsRegex(DeprecationWarning, 'Irc object instead'): self.assertFalse(callbacks.addressed(nick, badmsg)) badmsg = ircmsgs.privmsg('#foo', '%s^: foo' % nick) with self.assertWarnsRegex(DeprecationWarning, 'Irc object instead'): self.assertFalse(callbacks.addressed(nick, badmsg)) for msg in inChannel: with self.assertWarns(DeprecationWarning): self.assertEqual('foo', callbacks.addressed(nick, msg), msg) msg = ircmsgs.privmsg(nick, 'foo') irc._tagMsg(msg) with self.assertWarns(DeprecationWarning): self.assertEqual('foo', callbacks.addressed(nick, msg)) conf.supybot.reply.whenAddressedBy.chars.set(oldprefixchars) msg = ircmsgs.privmsg('#foo', '%s::::: bar' % nick) with self.assertWarns(DeprecationWarning): self.assertEqual('bar', callbacks.addressed(nick, msg)) msg = ircmsgs.privmsg('#foo', '%s: foo' % nick.upper()) with self.assertWarns(DeprecationWarning): self.assertEqual('foo', callbacks.addressed(nick, msg)) badmsg = ircmsgs.privmsg('#foo', '%s`: foo' % nick) with self.assertWarns(DeprecationWarning): self.assertFalse(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): irc = getTestIrc() msg = ircmsgs.privmsg('#foo', 'bar: baz') irc._tagMsg(msg) 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') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('biff', msg, nicks=['bar']), 'baz') def testAddressedWithNickAtEnd(self): irc = getTestIrc() msg = ircmsgs.privmsg('#foo', 'baz, bar') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('bar', msg, whenAddressedByNickAtEnd=True), 'baz') # Test that it still works with trailing whitespace: msg = ircmsgs.privmsg('#foo', 'baz, bar \t') self.assertEqual(callbacks.addressed('bar', msg, whenAddressedByNickAtEnd=True), 'baz') def testAddressedPrefixCharsTakePrecedenceOverNickAtEnd(self): irc = getTestIrc() msg = ircmsgs.privmsg('#foo', '@echo foo') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('foo', msg, whenAddressedByNickAtEnd=True, prefixChars='@'), 'echo foo') def testReply(self): irc = getTestIrc() prefix = 'foo!bar@baz' channelMsg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) nonChannelMsg = ircmsgs.privmsg('supybot', 'bar baz', prefix=prefix) irc._tagMsg(channelMsg) irc._tagMsg(nonChannelMsg) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks._makeReply(irc, channelMsg, 'foo', private=True)) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks._makeReply(irc, nonChannelMsg, 'foo')) self.assertEqual(ircmsgs.privmsg(channelMsg.args[0], '%s: foo' % channelMsg.nick), callbacks._makeReply(irc, channelMsg, 'foo')) self.assertEqual(ircmsgs.privmsg(channelMsg.args[0], 'foo'), callbacks._makeReply(irc, channelMsg, 'foo', prefixNick=False)) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks._makeReply(irc, channelMsg, 'foo', notice=True, private=True)) def testReplyStatusmsg(self): irc = getTestIrc() prefix = 'foo!bar@baz' msg = ircmsgs.privmsg('+#foo', 'bar baz', prefix=prefix) irc._tagMsg(msg) # No statusmsg set, so understood as being a private message, so # private reply self.assertEqual(ircmsgs.notice(msg.nick, 'foo'), callbacks._makeReply(irc, msg, 'foo')) irc.state.supported['statusmsg'] = '+' msg = ircmsgs.privmsg('+#foo', 'bar baz', prefix=prefix) irc._tagMsg(msg) self.assertEqual(ircmsgs.privmsg('+#foo', '%s: foo' % msg.nick), callbacks._makeReply(irc, msg, 'foo')) def testReplyTo(self): irc = getTestIrc() prefix = 'foo!bar@baz' msg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) irc._tagMsg(msg) self.assertEqual(callbacks._makeReply(irc, msg, 'blah', to='blah'), ircmsgs.privmsg('#foo', 'blah: blah')) self.assertEqual(callbacks._makeReply(irc, 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.assertIsNotNone(m, 'm: %r' % m) self.assertTrue(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.assertIsNotNone(m) self.assertFalse(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.assertTrue(m, 'No message returned.') self.assertFalse(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.error.inPrivate.setValue(original) def testErrorNoArgumentIsSilent(self): self.assertResponse('eval irc.error()', 'None') 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.assertTrue(m, 'No message returned.') self.assertEqual(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.assertTrue(ircutils.isChannel(m.args[0])) conf.supybot.reply.error.inPrivate.set('True') m = self.getMsg(s) self.assertFalse(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) def testReplyInstant(self): self.assertNoResponse(' ') self.assertResponse( "eval 'foo '*300", "'" + "foo " * 110 + " \x02(2 more messages)\x02") self.assertNoResponse(" ", timeout=0.1) with conf.supybot.reply.mores.instant.context(2): self.assertResponse( "eval 'foo '*300", "'" + "foo " * 110) self.assertResponse( " ", "foo " * 111 + "\x02(1 more message)\x02") self.assertNoResponse(" ", timeout=0.1) with conf.supybot.reply.mores.instant.context(3): self.assertResponse( "eval 'foo '*300", "'" + "foo " * 110) self.assertResponse( " ", "foo " * 110 + "foo") self.assertResponse( " ", " " + "foo " * 79 + "'") self.assertNoResponse(" ", timeout=0.1) def testReplyPrivate(self): # Send from a very long nick, which should be taken into account when # computing the reply overhead. self.assertResponse( "eval irc.reply('foo '*300, private=True)", "foo " * 39 + "\x02(7 more messages)\x02", frm='foo'*100 + '!bar@baz') def testClientTagReply(self): self.irc.addCallback(self.First(self.irc)) # no CAP, no msgid, no experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick))) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) # CAP and msgid, not no experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) with conf.supybot.protocols.irc.experimentalExtensions.context(True): # no CAP, but msgid and experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) # msgid and experimentalExtensions, but no CAP -> no +reply # (note that in theory it's impossible to receive msgid without # the CAP, but the +reply spec explicitly requires to check it) self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) try: self.irc.state.capabilities_ack.add('message-tags') # no msgid, but CAP and experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick))) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) # all of CAP, msgid, experimentalExtensions -> yes +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick), server_tags={'+draft/reply': 'foobar'})) finally: self.irc.state.capabilities_ack.remove('message-tags') def testClientTagReplyChannel(self): self.irc.addCallback(self.First(self.irc)) try: conf.supybot.protocols.irc.experimentalExtensions.setValue(True) self.irc.state.capabilities_ack.add('message-tags') # Reply in channel to channel message -> +draft/channel-context # is absent self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick), server_tags={'+draft/reply': 'foobar'})) # Reply in private to channel message -> +draft/channel-context # is present with conf.supybot.reply.inPrivate.context(True): self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='NOTICE', args=(self.nick, 'foo'), server_tags={'+draft/reply': 'foobar', '+draft/channel-context': '#foo'})) # Reply in private to private message -> +draft/channel-context # is absent self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=(self.nick, 'firstcmd'), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='NOTICE', args=(self.nick, 'foo'), server_tags={'+draft/reply': 'foobar'})) finally: conf.supybot.protocols.irc.experimentalExtensions.setValue(False) self.irc.state.capabilities_ack.remove('message-tags') 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 doNotice(self, irc, msg): 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.assertFalse(m.args[1].startswith('\x01ACTION')) def testNotActionSecondReplyNotCommand(self): """Same as testNotActionSecondReply, but tests ReplyIrcProxy instead of NestedCommandsIrcProxy.""" self.irc.addCallback(self.TwoRepliesFirstAction(self.irc)) self.irc.feedMsg(ircmsgs.notice(self.channel, 'test action reply', prefix=self.prefix)) self.assertAction(' ', 'foo') m = self.getMsg(' ') self.assertFalse(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 MultilinePrivmsgTestCase(ChannelPluginTestCase): plugins = ('Utilities', 'Misc', 'Web', 'String') conf.allowEval = True def setUp(self): super().setUp() # Enable multiline self.irc.state.capabilities_ack.add('batch') self.irc.state.capabilities_ack.add('draft/multiline') self.irc.state.capabilities_ls['draft/multiline'] = 'max-bytes=4096' # Enable msgid and +draft/reply self.irc.state.capabilities_ack.add('message-tags') conf.supybot.protocols.irc.experimentalExtensions.setValue(True) def tearDown(self): conf.supybot.protocols.irc.experimentalExtensions.setValue(False) conf.supybot.reply.mores.instant.setValue(1) super().tearDown() def testReplyInstantSingle(self): self.assertIsNone(self.irc.takeMsg()) # Single message as reply, no batch self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110 + " \x02(2 more messages)\x02"))) self.assertIsNone(self.irc.takeMsg()) def testReplyInstantBatchPartial(self): """response is shared as a batch + (1 more message)""" self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(2) self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 111 + "\x02(1 more message)\x02"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testReplyInstantBatchFull(self): """response is entirely instant""" self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(3) self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, a PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 110 + "foo"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Fourth message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, " " + "foo " * 79 + "'"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testReplyInstantBatchFullMaxBytes(self): """response is entirely instant, but does not fit in a single batch""" self.irc.state.capabilities_ls['draft/multiline'] = 'max-bytes=900' self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(3) self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) # First message opens the first batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, a PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 110 + "foo"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # closes the first batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) # opens the second batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # last PRIVMSG (and also the first of its batch) m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, " " + "foo " * 79 + "'"), server_tags={'batch': batch_name})) # Last message, closes the second batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testReplyInstantBatchTags(self): """check a message's tags are 'lifted' to the initial BATCH command.""" self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(2) m = ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix) m.server_tags['msgid'] = 'initialmsgid' self.irc.feedMsg(m) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel), server_tags={'+draft/reply': 'initialmsgid'})) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 111 + "\x02(1 more message)\x02"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) class PluginRegexpTestCase(ChannelPluginTestCase): plugins = () class PCAR(callbacks.PluginRegexp): regexps = ("test", "test2") def test(self, irc, msg, args): "" raise callbacks.ArgumentError def test2(self, irc, msg, args): "" irc.reply("hello") def setUp(self): super().setUp() self.irc.addCallback(self.PCAR(self.irc)) def testNoEscapingArgumentError(self): self.assertResponse('test', 'test ') def testReply(self): self.irc.feedMsg(ircmsgs.IrcMsg( prefix=self.prefix, command='PRIVMSG', args=(self.channel, 'foo baz'))) self.assertResponse(' ', 'hello') def testIgnoreChathistory(self): self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('+123', 'chathistory', self.channel))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123'}, prefix=self.prefix, command='PRIVMSG', args=(self.channel, 'foo baz'))) self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('-123',))) self.assertNoResponse(' ') 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.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(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.assertEqual(m.command, 'NOTICE') self.assertFalse(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(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.assertEqual(m.command, 'NOTICE') self.assertFalse(ircutils.isChannel(m.args[0])) m = self.assertNotError('normal') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(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.assertEqual(m.command, 'NOTICE') m = self.getMsg(' ') m = self.assertNotError("eval irc.reply('y',to='#x',private=True)") self.assertNotEqual(m.command, 'NOTICE') finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(original) class ProxyTestCase(SupyTestCase): def testHashing(self): irc = getTestIrc() msg = ircmsgs.ping('0') irc._tagMsg(msg) irc = irclib.Irc('test') proxy = callbacks.SimpleProxy(irc, msg) # First one way... self.assertEqual(proxy, irc) self.assertEqual(proxy, irc) self.assertEqual(hash(proxy), hash(irc)) # Then the other! self.assertEqual(irc, proxy) self.assertEqual(irc, proxy) self.assertEqual(hash(irc), hash(proxy)) # And now dictionaries... d = {} d[irc] = 'foo' self.assertEqual(len(d), 1) self.assertEqual(d[irc], 'foo') self.assertEqual(d[proxy], 'foo') d[proxy] = 'bar' self.assertEqual(len(d), 1) self.assertEqual(d[irc], 'bar') self.assertEqual(d[proxy], 'bar') d[irc] = 'foo' self.assertEqual(len(d), 1) self.assertEqual(d[irc], 'foo') self.assertEqual(d[proxy], 'foo') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_commands.py0000644000175000017500000002145214535072470016315 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2015, James McCoy # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import 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'] = '#' realIrc._tagMsg(msg) 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.assertTrue(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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_conf.py0000644000175000017500000000537514535072470015447 0ustar00valval## # 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.registry as registry import supybot.ircutils as ircutils class SupyConfTestCase(SupyTestCase): def testJoinToOneChannel(self): orig = conf.supybot.networks.test.channels() channels = ircutils.IrcSet() channels.add("#bar") conf.supybot.networks.test.channels.setValue(channels) msgs = conf.supybot.networks.test.channels.joins() self.assertEqual(msgs[0].args, ("#bar",)) conf.supybot.networks.test.channels.setValue(orig) def testJoinToManyChannels(self): orig = conf.supybot.networks.test.channels() channels = ircutils.IrcSet() input_list = [] for x in range(1, 30): name = "#verylongchannelname" + str(x) channels.add(name) input_list.append(name) conf.supybot.networks.test.channels.setValue(channels) msgs = conf.supybot.networks.test.channels.joins() # Double check we split the messages self.assertEqual(len(msgs), 2) # Ensure all channel names are present chan_list = (msgs[0].args[0] + ',' + msgs[1].args[0]).split(',') self.assertCountEqual(input_list, chan_list) conf.supybot.networks.test.channels.setValue(orig) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_drivers.py0000644000175000017500000001003214535072470016162 0ustar00valval## # Copyright (c) 2019-2021, 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 * import supybot.ircdb as ircdb import supybot.irclib as irclib import supybot.drivers as drivers class DriversTestCase(SupyTestCase): def tearDown(self): ircdb.networks.networks = {} def testValidStsPolicy(self): irc = irclib.Irc('test') net = ircdb.networks.getNetwork('test') net.addStsPolicy('example.com', 6697, 'duration=10,port=12345') net.addDisconnection('example.com') with conf.supybot.networks.test.servers.context( ['example.com:6667', 'example.org:6667']): driver = drivers.ServersMixin(irc) self.assertEqual( driver._getNextServer(), drivers.Server('example.com', 6697, None, True)) driver.die() self.assertEqual( driver._getNextServer(), drivers.Server('example.org', 6667, None, False)) driver.die() self.assertEqual( driver._getNextServer(), drivers.Server('example.com', 6697, None, True)) def testExpiredStsPolicy(self): irc = irclib.Irc('test') net = ircdb.networks.getNetwork('test') net.addStsPolicy('example.com', 6697, 'duration=10') net.addDisconnection('example.com') timeFastForward(16) with conf.supybot.networks.test.servers.context( ['example.com:6667']): driver = drivers.ServersMixin(irc) self.assertEqual( driver._getNextServer(), drivers.Server('example.com', 6667, None, False)) def testRescheduledStsPolicy(self): irc = irclib.Irc('test') net = ircdb.networks.getNetwork('test') net.addStsPolicy('example.com', 6697, 'duration=10') net.addDisconnection('example.com') with conf.supybot.networks.test.servers.context( ['example.com:6667', 'example.org:6667']): driver = drivers.ServersMixin(irc) timeFastForward(8) self.assertEqual( driver._getNextServer(), drivers.Server('example.com', 6697, None, True)) driver.die() self.assertEqual( driver._getNextServer(), drivers.Server('example.org', 6667, None, False)) driver.die() timeFastForward(8) self.assertEqual( driver._getNextServer(), drivers.Server('example.com', 6697, None, True)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_dynamicScope.py0000644000175000017500000000463214535072470017133 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_firewall.py0000644000175000017500000000623114535072470016317 0ustar00valval### # Copyright (c) 2008, Jeremiah Fincher # Copyright (c) 2010-2021, 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 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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_format.py0000644000175000017500000000360514535072470016004 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * class FormatTestCase(SupyTestCase): def test_t_acceptsNone(self): self.assertTrue(format('%t', None)) def testFloatingPoint(self): self.assertEqual(format('%.2f', 0.12345), '0.12') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_i18n.py0000644000175000017500000000542014535072470015270 0ustar00valval# -*- coding: utf8 -*- ### # Copyright (c) 2012-2021, 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_ircdb.py0000644000175000017500000006745314535072470015612 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import os import unittest import unittest.mock import supybot.conf as conf import supybot.world as world import supybot.ircdb as ircdb import supybot.ircutils as ircutils from supybot.utils.minisix import io 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.assertFalse(ircdb.isAntiCapability('foo')) self.assertFalse(ircdb.isAntiCapability('#foo,bar')) self.assertTrue(ircdb.isAntiCapability('-foo')) self.assertTrue(ircdb.isAntiCapability('#foo,-bar')) self.assertTrue(ircdb.isAntiCapability('#foo.bar,-baz')) def testIsChannelCapability(self): self.assertFalse(ircdb.isChannelCapability('foo')) self.assertTrue(ircdb.isChannelCapability('#foo,bar')) self.assertTrue(ircdb.isChannelCapability('#foo.bar,baz')) self.assertTrue(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.assertTrue(d.check('foo')) self.assertFalse(d.check('-foo')) d.add('bar') self.assertTrue(d.check('bar')) self.assertFalse(d.check('-bar')) d.add('-baz') self.assertFalse(d.check('baz')) self.assertTrue(d.check('-baz')) d.add('-bar') self.assertFalse(d.check('bar')) self.assertTrue(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.assertNotIn('foo', s) self.assertNotIn('-foo', s) s.add('foo') self.assertIn('foo', s) self.assertIn('-foo', s) s.remove('foo') self.assertNotIn('foo', s) self.assertNotIn('-foo', s) s.add('-foo') self.assertIn('foo', s) self.assertIn('-foo', s) def testCheck(self): s = ircdb.CapabilitySet() self.assertRaises(KeyError, s.check, 'foo') self.assertRaises(KeyError, s.check, '-foo') s.add('foo') self.assertTrue(s.check('foo')) self.assertFalse(s.check('-foo')) s.remove('foo') self.assertRaises(KeyError, s.check, 'foo') self.assertRaises(KeyError, s.check, '-foo') s.add('-foo') self.assertFalse(s.check('foo')) self.assertTrue(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.assertFalse(s.check('foo')) self.assertTrue(s.check('-foo')) s.add('foo') self.assertTrue(s.check('foo')) self.assertFalse(s.check('-foo')) class UserCapabilitySetTestCase(SupyTestCase): def testOwnerHasAll(self): d = ircdb.UserCapabilitySet(('owner',)) self.assertFalse(d.check('-foo')) self.assertTrue(d.check('foo')) def testOwnerIsAlwaysPresent(self): d = ircdb.UserCapabilitySet() self.assertIn('owner', d) self.assertIn('-owner', d) self.assertFalse(d.check('owner')) d.add('owner') self.assertTrue(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.assertIn('foo', s) self.assertIn('-foo', s) self.assertTrue(s.check('owner')) self.assertFalse(s.check('-owner')) self.assertFalse(s.check('-foo')) self.assertTrue(s.check('foo')) ## def testWorksAfterReload(self): ## s = ircdb.UserCapabilitySet(['owner']) ## self.assertTrue(s.check('owner')) ## import sets ## reload(sets) ## self.assertTrue(s.check('owner')) class IrcUserTestCase(IrcdbTestCase): def testCapabilities(self): u = ircdb.IrcUser() u.addCapability('foo') self.assertTrue(u._checkCapability('foo')) self.assertFalse(u._checkCapability('-foo')) u.addCapability('-bar') self.assertTrue(u._checkCapability('-bar')) self.assertFalse(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.assertTrue(u.checkHostmask('foo!bar@baz')) u.addHostmask('foo!bar@baz') u.removeHostmask('foo!bar@baz') self.assertFalse(u.checkHostmask('foo!bar@baz')) def testOwner(self): u = ircdb.IrcUser() u.addCapability('owner') self.assertTrue(u._checkCapability('foo')) self.assertFalse(u._checkCapability('-foo')) def testInitCapabilities(self): u = ircdb.IrcUser(capabilities=['foo']) self.assertTrue(u._checkCapability('foo')) def testPassword(self): u = ircdb.IrcUser() u.setPassword('foobar') self.assertTrue(u.checkPassword('foobar')) self.assertFalse(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.assertTrue(u.checkHostmask('foo!bar@baz')) timeFastForward(2.1) self.assertFalse(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.assertTrue(u.checkHostmask('foo!bar@baz')) u.addAuth('foo!bar@baz') self.assertTrue(u.checkHostmask('foo!bar@baz')) self.assertEqual(len(u.auth), 1) u.addAuth('boo!far@fizz') self.assertTrue(u.checkHostmask('boo!far@fizz')) timeFastForward(2.1) self.assertFalse(u.checkHostmask('foo!bar@baz')) self.assertFalse(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.assertTrue(u.checkPassword('foobar')) self.assertFalse(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.assertFalse(u.checkHostmask(prefix)) for hostmask in hostmasks: u.addHostmask(hostmask) self.assertTrue(u.checkHostmask(prefix)) def testAuth(self): prefix = 'foo!bar@baz' u = ircdb.IrcUser() u.addAuth(prefix) self.assertTrue(u.auth) u.clearAuth() self.assertFalse(u.auth) def testIgnore(self): u = ircdb.IrcUser(ignore=True) self.assertFalse(u._checkCapability('foo')) self.assertTrue(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.assertFalse(c._checkCapability('op')) self.assertFalse(c._checkCapability('voice')) self.assertFalse(c._checkCapability('halfop')) self.assertFalse(c._checkCapability('protected')) def testCapabilities(self): c = ircdb.IrcChannel(defaultAllow=False) self.assertFalse(c._checkCapability('foo')) c.addCapability('foo') self.assertTrue(c._checkCapability('foo')) c.removeCapability('foo') self.assertFalse(c._checkCapability('foo')) def testDefaultCapability(self): c = ircdb.IrcChannel() c.setDefaultCapability(False) self.assertFalse(c._checkCapability('foo')) self.assertTrue(c._checkCapability('-foo')) c.setDefaultCapability(True) self.assertTrue(c._checkCapability('foo')) self.assertFalse(c._checkCapability('-foo')) def testLobotomized(self): c = ircdb.IrcChannel(lobotomized=True) self.assertTrue(c.checkIgnored('foo!bar@baz')) def testIgnored(self): prefix = 'foo!bar@baz' banmask = ircutils.banmask(prefix) c = ircdb.IrcChannel() self.assertFalse(c.checkIgnored(prefix)) c.addIgnore(banmask) self.assertTrue(c.checkIgnored(prefix)) c.removeIgnore(banmask) self.assertFalse(c.checkIgnored(prefix)) c.addBan(banmask) self.assertTrue(c.checkIgnored(prefix)) c.removeBan(banmask) self.assertFalse(c.checkIgnored(prefix)) class IrcNetworkTestCase(IrcdbTestCase): def testDefaults(self): n = ircdb.IrcNetwork() self.assertEqual(n.stsPolicies, {}) self.assertEqual(n.lastDisconnectTimes, {}) def testStsPolicy(self): n = ircdb.IrcNetwork() n.addStsPolicy('foo', 123, 'bar') n.addStsPolicy('baz', 456, 'qux') self.assertEqual(n.stsPolicies, { 'foo': (123, 'bar'), 'baz': (456, 'qux'), }) def testAddDisconnection(self): n = ircdb.IrcNetwork() min_ = int(time.time()) n.addDisconnection('foo') max_ = int(time.time()) self.assertTrue(min_ <= n.lastDisconnectTimes['foo'] <= max_) def testPreserve(self): n = ircdb.IrcNetwork() n.addStsPolicy('foo', 123, 'sts1') n.addStsPolicy('bar', 456,'sts2') n.addDisconnection('foo') n.addDisconnection('baz') disconnect_time_foo = n.lastDisconnectTimes['foo'] disconnect_time_baz = n.lastDisconnectTimes['baz'] fd = io.StringIO() n.preserve(fd, indent=' ') fd.seek(0) self.assertCountEqual(fd.read().split('\n'), [ ' stsPolicy foo 123 sts1', ' stsPolicy bar 456 sts2', ' lastDisconnectTime foo %d' % disconnect_time_foo, ' lastDisconnectTime baz %d' % disconnect_time_baz, '', '', ]) 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 NetworksDictionaryTestCase(IrcdbTestCase): filename = os.path.join(conf.supybot.directories.conf(), 'NetworksDictionaryTestCase.conf') def setUp(self): try: os.remove(self.filename) except: pass self.networks = ircdb.NetworksDictionary() IrcdbTestCase.setUp(self) def testGetSetNetwork(self): self.assertEqual(self.networks.getNetwork('foo').stsPolicies, {}) n = ircdb.IrcNetwork() self.networks.setNetwork('foo', n) self.assertEqual(self.networks.getNetwork('foo').stsPolicies, {}) def testPreserveOne(self): n = ircdb.IrcNetwork() n.addStsPolicy('foo', 123, 'sts1') n.addStsPolicy('bar', 456, 'sts2') n.addDisconnection('foo') n.addDisconnection('baz') disconnect_time_foo = n.lastDisconnectTimes['foo'] disconnect_time_baz = n.lastDisconnectTimes['baz'] self.networks.setNetwork('foonet', n) fd = io.StringIO() fd.close = lambda: None self.networks.filename = 'blah' original_Atomicfile = utils.file.AtomicFile with unittest.mock.patch( 'supybot.utils.file.AtomicFile', return_value=fd): self.networks.flush() lines = fd.getvalue().split('\n') self.assertEqual(lines.pop(0), 'network foonet') self.assertCountEqual(lines, [ ' stsPolicy foo 123 sts1', ' stsPolicy bar 456 sts2', ' lastDisconnectTime foo %d' % disconnect_time_foo, ' lastDisconnectTime baz %d' % disconnect_time_baz, '', '', ]) def testPreserveThree(self): n = ircdb.IrcNetwork() n.addStsPolicy('foo', 123, 'sts1') self.networks.setNetwork('foonet', n) n = ircdb.IrcNetwork() n.addStsPolicy('bar', 456, 'sts2') self.networks.setNetwork('barnet', n) n = ircdb.IrcNetwork() n.addStsPolicy('baz', 789, 'sts3') self.networks.setNetwork('baznet', n) fd = io.StringIO() fd.close = lambda: None self.networks.filename = 'blah' original_Atomicfile = utils.file.AtomicFile with unittest.mock.patch( 'supybot.utils.file.AtomicFile', return_value=fd): self.networks.flush() fd.seek(0) self.assertEqual(fd.getvalue(), 'network barnet\n' ' stsPolicy bar 456 sts2\n' '\n' 'network baznet\n' ' stsPolicy baz 789 sts3\n' '\n' 'network foonet\n' ' stsPolicy foo 123 sts1\n' '\n' ) 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.assertTrue(self.checkCapability(self.owner, self.cap)) self.assertFalse(self.checkCapability(self.owner, self.anticap)) self.assertTrue(self.checkCapability(self.owner, self.chancap)) self.assertFalse(self.checkCapability(self.owner, self.antichancap)) self.channels.setChannel(self.channel, self.channelanticap) self.assertTrue(self.checkCapability(self.owner, self.cap)) self.assertFalse(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.assertTrue(self.checkCapability(self.nothing, self.chancap)) self.assertFalse(self.checkCapability(self.nothing, self.antichancap)) self.channels.setChannel(self.channel, self.channelanticap) self.assertFalse(self.checkCapability(self.nothing, self.chancap)) self.assertTrue(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.assertTrue(self.checkCapability(self.justfoo, self.cap)) self.assertFalse(self.checkCapability(self.justfoo, self.anticap)) def testAntiFoo(self): self.assertTrue(self.checkCapability(self.antifoo, self.anticap)) self.assertFalse(self.checkCapability(self.antifoo, self.cap)) def testJustChanFoo(self): self.channels.setChannel(self.channel, self.channelnothing) self.assertTrue(self.checkCapability(self.justchanfoo, self.chancap)) self.assertFalse(self.checkCapability(self.justchanfoo, self.antichancap)) self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow self.assertTrue(self.checkCapability(self.justchanfoo, self.chancap)) self.assertFalse(self.checkCapability(self.justchanfoo, self.antichancap)) self.channels.setChannel(self.channel, self.channelanticap) self.assertTrue(self.checkCapability(self.justchanfoo, self.chancap)) self.assertFalse(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.assertTrue(self.checkCapability(self.nothing, self.chancap)) self.channels.setChannel(self.channel, self.channelnothing) self.assertTrue(self.checkCapability(self.nothing, self.chancap)) self.channelnothing.defaultAllow = not self.channelnothing.defaultAllow self.assertTrue(self.checkCapability(self.nothing, self.chancap)) def testAntiChanFoo(self): self.channels.setChannel(self.channel, self.channelnothing) self.assertFalse(self.checkCapability(self.antichanfoo, self.chancap)) self.assertTrue(self.checkCapability(self.antichanfoo, self.antichancap)) def testSecurefoo(self): self.assertTrue(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.assertFalse(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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_irclib.py0000644000175000017500000020427714535072470015770 0ustar00valval## # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 copy import pickle import textwrap import unittest.mock from supybot.test import * import supybot.conf as conf import supybot.irclib as irclib import supybot.drivers as drivers 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 CapNegMixin: """Utilities for handling the capability negotiation.""" def startCapNegociation(self, caps='sasl'): m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(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.assertEqual(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.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) class IrcCommandDispatcherTestCase(SupyTestCase): class DispatchedClass(irclib.IrcCommandDispatcher): def doPrivmsg(): pass def doCap(): pass def doFail(): pass class DispatchedClassSub(irclib.IrcCommandDispatcher): def doPrivmsg(): pass def doPrivmsgFoo(): pass def doCapLs(): pass def doFailFoo(): pass def testCommandDispatch(self): dispatcher = self.DispatchedClass() self.assertEqual( dispatcher.dispatchCommand('privmsg', ['foo']), dispatcher.doPrivmsg) self.assertEqual( dispatcher.dispatchCommand('cap', ['*', 'ls']), dispatcher.doCap) self.assertEqual( dispatcher.dispatchCommand('fail', ['foo', 'bar']), dispatcher.doFail) self.assertEqual( dispatcher.dispatchCommand('foobar', ['*', 'ls']), None) def testSubCommandDispatch(self): dispatcher = self.DispatchedClassSub() self.assertEqual( dispatcher.dispatchCommand('privmsg', ['foo']), dispatcher.doPrivmsg) self.assertEqual( dispatcher.dispatchCommand('cap', ['*', 'ls']), dispatcher.doCapLs) self.assertEqual( dispatcher.dispatchCommand('fail', ['foo', 'bar']), dispatcher.doFailFoo) self.assertEqual( dispatcher.dispatchCommand('foobar', ['*', 'ls']), None) def testCommandDispatchMissingArgs(self): dispatcher = self.DispatchedClass() self.assertEqual( dispatcher.dispatchCommand('privmsg', ['foo']), dispatcher.doPrivmsg) self.assertEqual( dispatcher.dispatchCommand('cap', ['*']), dispatcher.doCap) self.assertEqual( dispatcher.dispatchCommand('fail', []), dispatcher.doFail) self.assertEqual( dispatcher.dispatchCommand('foobar', ['*']), None) def testCommandDispatchLegacy(self): """Tests the legacy parameters of dispatchCommand, without the "args" argument.""" dispatcher = self.DispatchedClass() with self.assertWarnsRegex(DeprecationWarning, "'args'"): self.assertEqual( dispatcher.dispatchCommand('privmsg'), dispatcher.doPrivmsg) with self.assertWarnsRegex(DeprecationWarning, "'args'"): self.assertEqual( dispatcher.dispatchCommand('cap'), dispatcher.doCap) with self.assertWarnsRegex(DeprecationWarning, "'args'"): self.assertEqual( dispatcher.dispatchCommand('fail'), dispatcher.doFail) with self.assertWarnsRegex(DeprecationWarning, "'args'"): self.assertEqual( dispatcher.dispatchCommand('foobar'), None) 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.assertIn(self.msg, q) q.dequeue() self.assertIn(self.msg, q) q.dequeue() self.assertIn(self.msg, q) q.dequeue() self.assertNotIn(self.msg, 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.assertFalse(q) def testEnqueueDequeue(self): q = irclib.IrcMsgQueue() q.enqueue(self.msg) self.assertTrue(q) self.assertEqual(self.msg, q.dequeue()) self.assertFalse(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.assertFalse(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.assertNotIn('jemfinch', c.users) self.assertIn('jemfinch', c1.users) def testCopy(self): c = irclib.ChannelState() c.addUser('jemfinch') c1 = copy.deepcopy(c) c.removeUser('jemfinch') self.assertNotIn('jemfinch', c.users) self.assertIn('jemfinch', c1.users) def testAddUser(self): c = irclib.ChannelState() c.addUser('foo') self.assertIn('foo', c.users) self.assertNotIn('foo', c.ops) self.assertNotIn('foo', c.voices) self.assertNotIn('foo', c.halfops) c.addUser('+bar') self.assertIn('bar', c.users) self.assertIn('bar', c.voices) self.assertNotIn('bar', c.ops) self.assertNotIn('bar', c.halfops) c.addUser('%baz') self.assertIn('baz', c.users) self.assertIn('baz', c.halfops) self.assertNotIn('baz', c.voices) self.assertNotIn('baz', c.ops) c.addUser('@quuz') self.assertIn('quuz', c.users) self.assertIn('quuz', c.ops) self.assertNotIn('quuz', c.halfops) self.assertNotIn('quuz', c.voices) class IrcStateTestCase(SupyTestCase): class FakeIrc: nick = 'nick' prefix = 'nick!user@host' def isChannel(self, s): return ircutils.isChannel(s) 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.assertNotIn('#foo', 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.assertNotIn('bar', st.channels['#foo'].ops) def testNickChangesChangeChannelUsers(self): st = irclib.IrcState() st.channels['#foo'] = irclib.ChannelState() st.channels['#foo'].addUser('@bar') self.assertIn('bar', st.channels['#foo'].users) self.assertTrue(st.channels['#foo'].isOp('bar')) st.addMsg(self.irc, ircmsgs.IrcMsg(':bar!asfd@asdf.com NICK baz')) self.assertNotIn('bar', st.channels['#foo'].users) self.assertFalse(st.channels['#foo'].isOp('bar')) self.assertIn('baz', st.channels['#foo'].users) self.assertTrue(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.assertFalse(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 testClientTagDenied(self): state = irclib.IrcState() # By default, every tag is assumed allowed self.assertEqual(state.getClientTagDenied('foo'), False) self.assertEqual(state.getClientTagDenied('bar'), False) # Blacklist state.addMsg(self.irc, ircmsgs.IrcMsg( ':example.org 005 mybot CLIENTTAGDENY=foo :are supported by this server')) self.assertEqual(state.getClientTagDenied('foo'), True) self.assertEqual(state.getClientTagDenied('bar'), False) self.assertEqual(state.getClientTagDenied('+foo'), True) self.assertEqual(state.getClientTagDenied('+bar'), False) # Whitelist state.addMsg(self.irc, ircmsgs.IrcMsg( ':example.org 005 mybot CLIENTTAGDENY=*,-foo :are supported by this server')) self.assertEqual(state.getClientTagDenied('foo'), False) self.assertEqual(state.getClientTagDenied('bar'), True) self.assertEqual(state.getClientTagDenied('+foo'), False) self.assertEqual(state.getClientTagDenied('+bar'), True) def testShort004(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')) self.assertNotIn('umodes', state.supported) self.assertNotIn('chanmodes', state.supported) 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.assertNotIn('#foo', stateCopy.channels) def testJoin(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix)) self.assertIn('#foo', st.channels) self.assertIn(self.irc.nick, st.channels['#foo'].users) st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz')) self.assertIn('foo', st.channels['#foo'].users) st2 = st.copy() st2.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) self.assertNotIn('foo', st2.channels['#foo'].users) self.assertIn('foo', st.channels['#foo'].users) def testNickToHostmask(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz')) st.addMsg(self.irc, ircmsgs.join('#foo', prefix='bar!baz@qux')) self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') # QUIT erases the entry with self.subTest("QUIT"): st2 = st.copy() st2.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) with self.assertRaises(KeyError): st2.nickToHostmask('foo') self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') # NICK moves the entry with self.subTest("NICK"): st2 = st.copy() st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz', command='NICK', args=['foo2'])) with self.assertRaises(KeyError): st2.nickToHostmask('foo') self.assertEqual(st2.nickToHostmask('foo2'), 'foo2!bar@baz') self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') # NICK moves the entry (and overwrites if needed) with self.subTest("NICK with overwrite"): st2 = st.copy() st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz', command='NICK', args=['bar'])) with self.assertRaises(KeyError): st2.nickToHostmask('foo') self.assertEqual(st2.nickToHostmask('bar'), 'bar!bar@baz') self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') with self.subTest("PRIVMSG"): st2 = st.copy() st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar2@baz2', command='PRIVMSG', args=['#chan', 'foo'])) self.assertEqual(st2.nickToHostmask('foo'), 'foo!bar2@baz2') self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') with self.subTest("PRIVMSG with no host is ignored"): st2 = st.copy() st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo', command='PRIVMSG', args=['#chan', 'foo'])) self.assertEqual(st2.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') def testNickToHostmaskWho(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.IrcMsg(command='352', # RPL_WHOREPLY args=[self.irc.nick, '#chan', 'bar', 'baz', 'server.example', 'foo', 'H', '0 real name'])) self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') def testNickToHostmaskWhox(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.IrcMsg(command='354', # RPL_WHOSPCRPL args=[self.irc.nick, '1', 'bar', '127.0.0.1', 'baz', 'foo', 'H', '0', 'real name'])) self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') def testChghost(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz', command='CHGHOST', args=['bar2', 'baz2'])) self.assertEqual(st.nickToHostmask('foo'), 'foo!bar2@baz2') 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.assertNotIn('bar', st.channels['#foo'].ops) st.addMsg(self.irc, ircmsgs.op('#foo', 'bar')) self.assertIn('bar', st.channels['#foo'].ops) st.addMsg(self.irc, ircmsgs.deop('#foo', 'bar')) self.assertNotIn('bar', st.channels['#foo'].ops) self.assertNotIn('bar', st.channels['#foo'].voices) st.addMsg(self.irc, ircmsgs.voice('#foo', 'bar')) self.assertIn('bar', st.channels['#foo'].voices) st.addMsg(self.irc, ircmsgs.devoice('#foo', 'bar')) self.assertNotIn('bar', st.channels['#foo'].voices) self.assertNotIn('bar', st.channels['#foo'].halfops) st.addMsg(self.irc, ircmsgs.halfop('#foo', 'bar')) self.assertIn('bar', st.channels['#foo'].halfops) st.addMsg(self.irc, ircmsgs.dehalfop('#foo', 'bar')) self.assertNotIn('bar', st.channels['#foo'].halfops) def testDoModeOnlyChannels(self): st = irclib.IrcState() self.assertTrue(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1) def testNamreply(self): """RPL_NAMREPLY / reply to NAMES""" # Just nicks (à la RFC 1459 + some common prefix chars) st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.IrcMsg(command='353', args=('*', '=', '#chan', 'nick1 @nick2 ~@nick3'))) chan_st = st.channels['#chan'] self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', 'nick3'])) self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2', 'nick3'])) self.assertEqual(st.nicksToHostmasks, ircutils.IrcDict({})) # with userhost-in-names st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.IrcMsg(command='353', args=('*', '=', '#chan', 'nick1!u1@h1 @nick2!u2@h2 ~@nick3!u3@h3'))) chan_st = st.channels['#chan'] self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', 'nick3'])) self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2', 'nick3'])) self.assertEqual(st.nicksToHostmasks['nick1'], 'nick1!u1@h1') self.assertEqual(st.nicksToHostmasks['nick2'], 'nick2!u2@h2') self.assertEqual(st.nicksToHostmasks['nick3'], 'nick3!u3@h3') # Prefixed with chars not in ISUPPORT PREFIX st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.IrcMsg(command='005', args=('*', 'PREFIX=(ov)@+', 'are supported'))) st.addMsg(self.irc, ircmsgs.IrcMsg(command='353', args=('*', '=', '#chan', 'nick1!u1@h1 @nick2!u2@h2 ~@nick3!u3@h3'))) chan_st = st.channels['#chan'] self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', '~@nick3'])) self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2'])) self.assertEqual(st.nicksToHostmasks['nick1'], 'nick1!u1@h1') self.assertEqual(st.nicksToHostmasks['nick2'], 'nick2!u2@h2') self.assertEqual(st.nicksToHostmasks['~@nick3'], '~@nick3!u3@h3') class IrcCapsTestCase(SupyTestCase, CapNegMixin): def testReqLineLength(self): self.irc = irclib.Irc('test') m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.REQUEST_CAPABILITIES = set(['a'*400, 'b'*400]) caps = ' '.join(self.irc.REQUEST_CAPABILITIES) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', '*', 'a'*400))) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'b'*400))) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'a'*400) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'b'*400) def testNoEchomessageWithoutLabeledresponse(self): self.irc = irclib.Irc('test') m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'account-notify echo-message'))) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'account-notify') m = self.irc.takeMsg() self.assertIsNone(m) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'ACK', 'account-notify'))) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) def testEchomessageLabeledresponseGrouped(self): self.irc = irclib.Irc('test') m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.REQUEST_CAPABILITIES = set([ 'account-notify', 'a'*490, 'echo-message', 'labeled-response']) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=( '*', 'LS', 'account-notify ' + 'a'*490 + ' echo-message labeled-response'))) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'echo-message labeled-response') m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'a'*490) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'account-notify') m = self.irc.takeMsg() self.assertIsNone(m) def testCapNew(self): self.irc = irclib.Irc('test') self.assertEqual(self.irc.sasl_current_mechanism, None) self.assertEqual(self.irc.sasl_next_mechanisms, []) self.startCapNegociation(caps='') self.endCapNegociation() while self.irc.takeMsg(): pass self.irc.feedMsg(ircmsgs.IrcMsg(command='422')) # ERR_NOMOTD m = self.irc.takeMsg() self.assertIsNone(m) self.irc.feedMsg(ircmsgs.IrcMsg( command='CAP', args=['*', 'NEW', 'account-notify'])) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='CAP', args=['REQ', 'account-notify'])) self.irc.feedMsg(ircmsgs.IrcMsg( command='CAP', args=['*', 'ACK', 'account-notify'])) self.assertIn('account-notify', self.irc.state.capabilities_ack) def testCapNewEchomessageLabeledResponse(self): self.irc = irclib.Irc('test') self.assertEqual(self.irc.sasl_current_mechanism, None) self.assertEqual(self.irc.sasl_next_mechanisms, []) self.startCapNegociation(caps='') self.endCapNegociation() while self.irc.takeMsg(): pass self.irc.feedMsg(ircmsgs.IrcMsg(command='422')) # ERR_NOMOTD m = self.irc.takeMsg() self.assertIsNone(m) self.irc.feedMsg(ircmsgs.IrcMsg( command='CAP', args=['*', 'NEW', 'echo-message'])) m = self.irc.takeMsg() self.assertIsNone(m) self.irc.feedMsg(ircmsgs.IrcMsg( command='CAP', args=['*', 'NEW', 'labeled-response'])) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg( command='CAP', args=['REQ', 'echo-message labeled-response'])) self.irc.feedMsg(ircmsgs.IrcMsg( command='CAP', args=['*', 'ACK', 'echo-message labeled-response'])) self.assertIn('echo-message', self.irc.state.capabilities_ack) self.assertIn('labeled-response', self.irc.state.capabilities_ack) class StsTestCase(SupyTestCase): def setUp(self): self.irc = irclib.Irc('test') m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.driver = unittest.mock.Mock() def tearDown(self): ircdb.networks.networks = {} def _testStsInSecureConnection(self, cap_value): self.irc.driver.anyCertValidationEnabled.return_value = True self.irc.driver.ssl = True self.irc.driver.currentServer = drivers.Server('irc.test', 6697, None, False) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'sts=' + cap_value))) self.assertEqual(ircdb.networks.getNetwork('test').stsPolicies, { 'irc.test': (6697, cap_value)}) self.irc.driver.reconnect.assert_not_called() def testStsInSecureConnectionWithPort(self): self._testStsInSecureConnection('duration=42,port=12345') def testStsInSecureConnectionWithoutPort(self): self._testStsInSecureConnection('duration=42') def testStsInSecureConnectionMissingDuration(self): # "A persistence policy, expressed via the duration key. REQUIRED on a # secure connection" self.irc.driver.anyCertValidationEnabled.return_value = True self.irc.driver.ssl = True self.irc.driver.currentServer = drivers.Server('irc.test', 6697, None, False) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'sts=port=12345'))) self.assertEqual(ircdb.networks.getNetwork('test').stsPolicies, {}) self.irc.driver.reconnect.assert_not_called() def _testStsInInsecureTlsConnection(self, cap_value): self.irc.driver.anyCertValidationEnabled.return_value = False self.irc.driver.ssl = True self.irc.driver.currentServer = drivers.Server('irc.test', 6697, None, False) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'sts=' + cap_value))) self.assertEqual(ircdb.networks.getNetwork('test').stsPolicies, {}) self.irc.driver.reconnect.assert_called_once_with( server=drivers.Server('irc.test', 6697, None, True), wait=True) def testStsInInsecureTlsConnectionWithPort(self): self._testStsInInsecureTlsConnection('duration=42,port=6697') def testStsInInsecureTlsConnectionWithoutPort(self): self._testStsInInsecureTlsConnection('duration=42') def _testStsInCleartextConnection(self, cap_value): self.irc.driver.anyCertValidationEnabled.return_value = False self.irc.driver.ssl = False self.irc.driver.currentServer = drivers.Server('irc.test', 6667, None, False) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'sts=' + cap_value))) self.assertEqual(ircdb.networks.getNetwork('test').stsPolicies, {}) self.irc.driver.reconnect.assert_called_once_with( server=drivers.Server('irc.test', 6697, None, True), wait=True) def testStsInCleartextConnectionWithDuration(self): self._testStsInCleartextConnection('duration=42,port=6697') def testStsInCleartextConnectionWithoutDuration(self): self._testStsInCleartextConnection('port=6697') def testStsInCleartextConnectionInvalidDuration(self): # "Servers MAY send this key to all clients, but insecurely # connected clients MUST ignore it." self._testStsInCleartextConnection('duration=foo,port=6697') def testStsInCleartextConnectionMissingPort(self): self.irc.driver.anyCertValidationEnabled.return_value = False self.irc.driver.ssl = False self.irc.driver.currentServer = drivers.Server('irc.test', 6667, None, False) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'sts=duration=42'))) self.assertEqual(ircdb.networks.getNetwork('test').stsPolicies, {}) self.irc.driver.reconnect.assert_not_called() class IrcTestCase(SupyTestCase): def setUp(self): self.irc = irclib.Irc('test') #m = self.irc.takeMsg() #self.assertTrue(m.command == 'PASS', 'Expected PASS, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() self.assertEqual(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.assertEqual(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.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) m = self.irc.takeMsg() self.assertIsNone(m, 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.assertTrue(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.assertTrue(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.assertEqual(msg.command, 'PRIVMSG') msg = self.irc.takeMsg() self.assertEqual(msg.command, 'NOTICE') def testNoMsgLongerThan512(self): self.irc.queueMsg(ircmsgs.privmsg('whocares', 'x'*1000)) msg = self.irc.takeMsg() self.assertEqual( len(msg), 512, 'len(msg) was %s (msg=%r)' % (len(msg), msg)) # Server tags don't influence the size limit of the rest of the # message. self.irc.queueMsg(ircmsgs.IrcMsg( command='PRIVMSG', args=['whocares', 'x'*1000], server_tags={'y': 'z'*500})) msg2 = self.irc.takeMsg() self.assertEqual( len(msg2), 512+504, 'len(msg2) was %s (msg2=%r)' % (len(msg2), msg2)) self.assertEqual(msg.args, msg2.args) def testReset(self): for msg in msgs: try: self.irc.feedMsg(msg) except: pass self.irc.reset() self.assertFalse(self.irc.state.history) self.assertFalse(self.irc.state.channels) self.assertFalse(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 testMultipleMotd(self): self.irc.reset() self.irc.feedMsg(ircmsgs.IrcMsg(command='422')) self.irc.feedMsg(ircmsgs.IrcMsg(command='422')) self.irc.feedMsg(ircmsgs.IrcMsg(command='375', args=['nick'])) self.irc.feedMsg(ircmsgs.IrcMsg(command='372', args=['nick', 'some message'])) self.irc.feedMsg(ircmsgs.IrcMsg(command='376', args=['nick'])) def testSetUmodes(self): def assertSentModes(modes): self.assertEqual( self.irc.takeMsg(), ircmsgs.IrcMsg(command='MODE', args=['test', modes]), ) self.irc.reset() while self.irc.takeMsg(): pass self.irc.state.supported["BOT"] = "" # invalid self.irc._setUmodes() self.assertIsNone(self.irc.takeMsg()) self.irc.state.supported["BOT"] = "bB" # invalid too self.irc._setUmodes() self.assertIsNone(self.irc.takeMsg()) del self.irc.state.supported["BOT"] self.irc._setUmodes() self.assertIsNone(self.irc.takeMsg()) self.irc.state.supported["BOT"] = "B" self.irc._setUmodes() assertSentModes("+B") self.irc.state.supported["BOT"] = "b" self.irc._setUmodes() assertSentModes("+b") # merge with configured umodes with conf.supybot.protocols.irc.umodes.context("+B"): self.irc._setUmodes() assertSentModes("+B+b") # no duplicate if char is the same with conf.supybot.protocols.irc.umodes.context("+b"): self.irc._setUmodes() assertSentModes("+b") # no duplicate if explicitly disabled with conf.supybot.protocols.irc.umodes.context("-b"): self.irc._setUmodes() assertSentModes("-b") def testMsgChannel(self): self.irc.reset() self.irc.state.supported['statusmsg'] = '@' self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG #linux :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux') self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG @#linux2 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux2') self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG +#linux3 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, None) self.irc.state.supported['statusmsg'] = '+@' self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG #linux :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux') self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG @#linux2 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux2') self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG +#linux3 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux3') del self.irc.state.supported['statusmsg'] self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG #linux :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux') self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG @#linux2 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, None) self.irc.feedMsg(ircmsgs.IrcMsg('PRIVMSG +#linux3 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, None) # Test msg.channel is set only for PRIVMSG and NOTICE self.irc.state.supported['statusmsg'] = '+@' self.irc.feedMsg(ircmsgs.IrcMsg('NOTICE @#linux :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux') self.irc.feedMsg(ircmsgs.IrcMsg('NOTICE @#linux2 :foo bar baz!')) self.assertEqual(self.irc.state.history[-1].channel, '#linux2') self.irc.feedMsg(ircmsgs.IrcMsg('MODE @#linux3 +v foo')) self.assertEqual(self.irc.state.history[-1].channel, None) def testFilterErrors(self): self.irc.reset() while self.irc.takeMsg() is not None: pass class BuggyCallback(irclib.IrcCallback): infilter_called = False outfilter_called = False def name(self): return 'buggycallback' def inFilter(self, irc, msg): self.infilter_called = True raise Exception('test exception') def outFilter(self, irc, msg): self.outfilter_called = True raise Exception('test exception') class Callback(irclib.IrcCallback): channels_set = None def name(self): return 'testcallback' def doCommand(self, irc, msg): irc.sendMsg(ircmsgs.privmsg('#foo', 'bar')) def inFilter(self, irc, msg): self.infilter_called = True raise Exception('test exception') def outFilter(self, irc, msg): self.outfilter_called = True raise Exception('test exception') bc = BuggyCallback() self.irc.addCallback(bc) c = Callback() self.irc.addCallback(c) self.irc.feedMsg(ircmsgs.IrcMsg('COMMAND blah')) m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.privmsg('#foo', 'bar')) self.assertTrue(bc.infilter_called) self.assertTrue(bc.outfilter_called) self.assertTrue(c.infilter_called) self.assertTrue(c.outfilter_called) 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')) m1 = ircmsgs.IrcMsg(':host BATCH +name netjoin') self.irc.feedMsg(m1) m2 = ircmsgs.IrcMsg('@batch=name :someuser2 JOIN #foo') self.irc.feedMsg(m2) self.irc.feedMsg(ircmsgs.IrcMsg(':someuser3 JOIN #foo')) m3 = ircmsgs.IrcMsg('@batch=name :someuser4 JOIN #foo') self.irc.feedMsg(m3) 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: m4 = ircmsgs.IrcMsg(':host BATCH -name') self.irc.feedMsg(m4) finally: self.irc.removeCallback(c.name()) self.assertEqual( c.batch, irclib.Batch('name', 'netjoin', (), [m1, m2, m3, m4], None), repr(c.batch) ) def testBatchNested(self): self.irc.reset() logs = textwrap.dedent(''' :irc.host BATCH +outer example.com/foo @batch=outer :irc.host BATCH +inner example.com/bar @batch=inner :nick!user@host PRIVMSG #channel :Hi @batch=outer :irc.host BATCH -inner :irc.host BATCH -outer ''') msgs = [ircmsgs.IrcMsg(s) for s in logs.split('\n') if s] # Feed 'BATCH +outer', it should be added in state self.irc.feedMsg(msgs[0]) outer = irclib.Batch('outer', 'example.com/foo', (), msgs[0:1], None) self.assertEqual(self.irc.state.batches, {'outer': outer}) msg1 = self.irc.state.history[-1] self.assertEqual(msg1.tagged('batch'), outer) self.assertEqual(self.irc.state.getParentBatches(msg1), [outer]) # Feed 'BATCH +inner', it should be added in state self.irc.feedMsg(msgs[1]) outer = irclib.Batch('outer', 'example.com/foo', (), msgs[0:2], None) inner = irclib.Batch('inner', 'example.com/bar', (), msgs[1:2], outer) self.assertIs(self.irc.state.batches['inner'].parent_batch, self.irc.state.batches['outer']) self.assertEqual(dict(self.irc.state.batches), {'outer': outer, 'inner': inner}) msg2 = self.irc.state.history[-1] self.assertEqual(msg2.tagged('batch'), inner) self.assertEqual(self.irc.state.getParentBatches(msg2), [inner, outer]) # Feed 'PRIVMSG' self.irc.feedMsg(msgs[2]) outer = irclib.Batch('outer', 'example.com/foo', (), msgs[0:3], None) inner = irclib.Batch('inner', 'example.com/bar', (), msgs[1:3], outer) self.assertIs(self.irc.state.batches['inner'].parent_batch, self.irc.state.batches['outer']) self.assertEqual(self.irc.state.batches, {'outer': outer, 'inner': inner}) msg3 = self.irc.state.history[-1] self.assertEqual(msg3.tagged('batch'), None) self.assertEqual(self.irc.state.getParentBatches(msg3), [inner, outer]) # Feed 'BATCH -inner', it should be remove from state self.irc.feedMsg(msgs[3]) outer = irclib.Batch('outer', 'example.com/foo', (), msgs[0:4], None) inner = irclib.Batch('inner', 'example.com/bar', (), msgs[1:4], outer) self.assertEqual(self.irc.state.batches, {'outer': outer}) self.assertIs(self.irc.state.history[-1].tagged('batch').parent_batch, self.irc.state.batches['outer']) msg4 = self.irc.state.history[-1] self.assertEqual(msg4.tagged('batch'), inner) self.assertEqual(self.irc.state.getParentBatches(msg4), [inner, outer]) # Feed 'BATCH -outer', it should be remove from state self.irc.feedMsg(msgs[4]) outer = irclib.Batch('outer', 'example.com/foo', (), msgs[0:5], None) inner = irclib.Batch('inner', 'example.com/bar', (), msgs[1:4], outer) self.assertEqual(self.irc.state.batches, {}) msg5 = self.irc.state.history[-1] self.assertEqual(msg5.tagged('batch'), outer) self.assertEqual(self.irc.state.getParentBatches(msg5), [outer]) def testBatchError(self): # Checks getting an ERROR message in a batch does not cause issues # due to deinitializing the connection at the same time. self.irc.reset() m1 = ircmsgs.IrcMsg(':host BATCH +name batchtype') self.irc.feedMsg(m1) m2 = ircmsgs.IrcMsg('@batch=name :someuser2 NOTICE * :oh no') self.irc.feedMsg(m2) m3 = ircmsgs.IrcMsg('@batch=name ERROR :bye') self.irc.feedMsg(m3) class Callback(irclib.IrcCallback): batch = None def __call__(self, *args, **kwargs): return super().__call__(*args, **kwargs) def name(self): return 'testcallback' def doBatch(self2, irc, msg): self2.batch = msg.tagged('batch') # would usually be called by the driver upon reconnect() trigged # by the ERROR: self.irc.reset() c = Callback() self.irc.addCallback(c) try: m4 = ircmsgs.IrcMsg(':host BATCH -name') self.irc.feedMsg(m4) finally: self.irc.removeCallback(c.name()) self.assertEqual( c.batch, irclib.Batch('name', 'batchtype', (), [m1, m2, m3, m4], None), repr(c.batch) ) def testTruncate(self): self.irc = irclib.Irc('test') while self.irc.takeMsg(): pass # over 512 bytes msg = ircmsgs.IrcMsg(command='PRIVMSG', args=('#test2', 'é'*256)) self.irc.sendMsg(msg) m = self.irc.takeMsg() remaining_payload = 'é' * ((512 - len('PRIVMSG #test2 :\r\n'))//2) self.assertEqual( str(m), 'PRIVMSG #test2 :%s\r\n' % remaining_payload) # over 512 bytes, make sure it doesn't truncate in the middle of # a character msg = ircmsgs.IrcMsg(command='PRIVMSG', args=('#test', 'é'*256)) self.irc.sendMsg(msg) m = self.irc.takeMsg() remaining_payload = 'é' * ((512 - len('PRIVMSG #test :\r\n'))//2) self.assertEqual( str(m), 'PRIVMSG #test :%s\r\n' % remaining_payload) class BatchTestCase(SupyTestCase): def setUp(self): self.irc = irclib.Irc('test') conf.supybot.protocols.irc.experimentalExtensions.setValue(True) while self.irc.takeMsg() is not None: self.irc.takeMsg() def tearDown(self): conf.supybot.protocols.irc.experimentalExtensions.setValue(False) def testQueueBatch(self): """Basic operation of queueBatch""" msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :there'), ircmsgs.IrcMsg('BATCH -label'), ] self.irc.queueBatch(copy.deepcopy(msgs)) for msg in msgs: self.assertEqual(msg, self.irc.takeMsg()) def testQueueBatchStartMinus(self): msgs = [ ircmsgs.IrcMsg('BATCH -label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH -label'), ] with self.assertRaises(ValueError): self.irc.queueBatch(msgs) self.assertIsNone(self.irc.takeMsg()) def testQueueBatchEndPlus(self): msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH +label'), ] with self.assertRaises(ValueError): self.irc.queueBatch(msgs) self.assertIsNone(self.irc.takeMsg()) def testQueueBatchMismatchStartEnd(self): msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH -label2'), ] with self.assertRaises(ValueError): self.irc.queueBatch(msgs) self.assertIsNone(self.irc.takeMsg()) def testQueueBatchMismatchInner(self): msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label2 PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH -label'), ] with self.assertRaises(ValueError): self.irc.queueBatch(msgs) self.assertIsNone(self.irc.takeMsg()) def testQueueBatchTwice(self): """Basic operation of queueBatch""" all_msgs = [] for label in ('label1', 'label2'): msgs = [ ircmsgs.IrcMsg('BATCH +%s batchtype' % label), ircmsgs.IrcMsg('@batch=%s PRIVMSG #channel :hello' % label), ircmsgs.IrcMsg('@batch=%s PRIVMSG #channel :there' % label), ircmsgs.IrcMsg('BATCH -%s' % label), ] all_msgs.extend(msgs) self.irc.queueBatch(copy.deepcopy(msgs)) for msg in all_msgs: self.assertEqual(msg, self.irc.takeMsg()) def testQueueBatchDuplicate(self): msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH -label'), ] self.irc.queueBatch(copy.deepcopy(msgs)) with self.assertRaises(ValueError): self.irc.queueBatch(copy.deepcopy(msgs)) for msg in msgs: self.assertEqual(msg, self.irc.takeMsg()) self.assertIsNone(self.irc.takeMsg()) def testQueueBatchReuse(self): """We can reuse the same label after the batch is closed.""" msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH -label'), ] self.irc.queueBatch(copy.deepcopy(msgs)) for msg in msgs: self.assertEqual(msg, self.irc.takeMsg()) self.irc.queueBatch(copy.deepcopy(msgs)) for msg in msgs: self.assertEqual(msg, self.irc.takeMsg()) def testBatchInterleaved(self): """Make sure it's not possible for an unrelated message to be sent while a batch is open""" msgs = [ ircmsgs.IrcMsg('BATCH +label batchtype'), ircmsgs.IrcMsg('@batch=label PRIVMSG #channel :hello'), ircmsgs.IrcMsg('BATCH -label'), ] msg = ircmsgs.IrcMsg('PRIVMSG #channel :unrelated message') with self.subTest('sendMsg called before "BATCH +" is dequeued'): self.irc.queueBatch(copy.deepcopy(msgs)) self.irc.sendMsg(msg) self.assertEqual(msg, self.irc.takeMsg()) self.assertEqual(msgs[0], self.irc.takeMsg()) self.assertEqual(msgs[1], self.irc.takeMsg()) self.assertEqual(msgs[2], self.irc.takeMsg()) self.assertIsNone(self.irc.takeMsg()) with self.subTest('sendMsg called after "BATCH +" is dequeued'): self.irc.queueBatch(copy.deepcopy(msgs)) self.assertEqual(msgs[0], self.irc.takeMsg()) self.irc.sendMsg(msg) self.assertEqual(msgs[1], self.irc.takeMsg()) self.assertEqual(msgs[2], self.irc.takeMsg()) self.assertEqual(msg, self.irc.takeMsg()) self.assertIsNone(self.irc.takeMsg()) class SaslTestCase(SupyTestCase, CapNegMixin): def setUp(self): self._default_mechanisms = conf.supybot.networks.test.sasl.mechanisms() def tearDown(self): conf.supybot.networks.test.sasl.mechanisms.setValue(self._default_mechanisms) conf.supybot.networks.test.sasl.username.setValue('') conf.supybot.networks.test.sasl.password.setValue('') conf.supybot.networks.test.certfile.setValue('') def testPlain(self): conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') conf.supybot.networks.test.sasl.mechanisms.setValue( ['scram-sha-256', 'plain']) self.irc = irclib.Irc('test') self.assertEqual(self.irc.sasl_current_mechanism, None) if irclib.scram: self.assertEqual(self.irc.sasl_next_mechanisms, ['scram-sha-256', 'plain']) self.startCapNegociation() m = self.irc.takeMsg() self.assertEqual(m, ircmsgs.IrcMsg(command='AUTHENTICATE', args=('SCRAM-SHA-256',))) self.irc.feedMsg(ircmsgs.IrcMsg(command='904', args=('mechanism not available',))) else: 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): conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') conf.supybot.networks.test.certfile.setValue('foo') conf.supybot.networks.test.sasl.mechanisms.setValue( ['external', 'plain']) self.irc = irclib.Irc('test') 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): conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') conf.supybot.networks.test.certfile.setValue('foo') conf.supybot.networks.test.sasl.mechanisms.setValue( ['external', 'plain']) self.irc = irclib.Irc('test') 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): conf.supybot.networks.test.sasl.username.setValue('jilles') conf.supybot.networks.test.sasl.password.setValue('sesame') conf.supybot.networks.test.sasl.mechanisms.setValue( ['external', 'plain']) self.irc = irclib.Irc('test') 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='422')) # ERR_NOMOTD 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) 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'))) m = self.irc.takeMsg() self.assertEqual(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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_ircmsgs.py0000644000175000017500000003340314535072470016162 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import time import copy import pickle import itertools 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.assertFalse(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.assertFalse(rawmsg != strmsg and \ strmsg.replace(':', '') == strmsg) def testEq(self): for msg in msgs: self.assertEqual(msg, msg) self.assertFalse(msgs and msgs[0] == []) # Comparison to unhashable type. def testNe(self): for msg in msgs: self.assertEqual(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.assertIsNone(m.time) 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.assertFalse(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 testSplit(self): msg = ircmsgs.IrcMsg(s=':foo bar baz :qux') self.assertEqual(msg.prefix, 'foo') self.assertEqual(msg.command, 'bar') self.assertEqual(msg.args, ('baz', 'qux')) msg = ircmsgs.IrcMsg(s=':foo\tbar baz') self.assertEqual(msg.prefix, 'foo\tbar') self.assertEqual(msg.command, 'baz') msg = ircmsgs.IrcMsg(s=':foo bar\tbaz') self.assertEqual(msg.prefix, 'foo') self.assertEqual(msg.command, 'bar\tbaz') 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.assertFalse(m.repliedTo) m.tag('repliedTo') self.assertTrue(m.repliedTo) m.tag('repliedTo') self.assertTrue(m.repliedTo) m.tag('repliedTo', 12) self.assertEqual(m.repliedTo, 12) def testServerTags(self): s = r'@aaa=b\:bb;ccc;example.com/ddd=ee\\se ' \ r':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') m._str = None # Clear the cache (set before parsing) tag_set = [r'aaa=b\:bb', r'ccc', r'example.com/ddd=ee\\se'] expected = [ '@' + ';'.join(tags) + ' :nick!ident@host.com PRIVMSG me :Hello\r\n' for tags in itertools.permutations(tag_set)] self.assertIn(str(m), expected) # bar\1 is equivalent to baz1 s = r'@foo=;bar=baz\1;qux= ' \ r':nick!ident@host.com PRIVMSG me :Hello' m = ircmsgs.IrcMsg(s) self.assertEqual(m.server_tags, { 'foo': None, 'bar': 'baz1', 'qux': None}) # bar\ is equivalent to baz s = r'@foo=;bar=baz\;qux= ' \ r':nick!ident@host.com PRIVMSG me :Hello' m = ircmsgs.IrcMsg(s) self.assertEqual(m.server_tags, { 'foo': None, 'bar': 'baz', 'qux': None}) s = r'@foo=;bar=baz;qux= ' \ r':nick!ident@host.com PRIVMSG me :Hello' m = ircmsgs.IrcMsg(s) self.assertEqual(m.server_tags, { 'foo': None, 'bar': 'baz', 'qux': None}) 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.assertTrue(ircmsgs.isAction(msg)) def testIsActionIsntStupid(self): m = ircmsgs.privmsg('#x', '\x01NOTANACTION foo\x01') self.assertFalse(ircmsgs.isAction(m)) m = ircmsgs.privmsg('#x', '\x01ACTION foo bar\x01') self.assertTrue(ircmsgs.isAction(m)) def testIsCtcp(self): self.assertTrue(ircmsgs.isCtcp(ircmsgs.privmsg('foo', '\x01VERSION\x01'))) self.assertFalse(ircmsgs.isCtcp(ircmsgs.privmsg('foo', '\x01'))) def testIsActionFalseWhenNoSpaces(self): msg = ircmsgs.IrcMsg('PRIVMSG #foo :\x01ACTIONfoobar\x01') self.assertFalse(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.assertTrue(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.assertTrue(ircmsgs.isSplit(m)) m = ircmsgs.IrcMsg(prefix="bzbot!Brad2901@ACC87473.ipt.aol.com", command="QUIT", args=('Read error: 110 (Connection timed out)',)) self.assertFalse(ircmsgs.isSplit(m)) m = ircmsgs.IrcMsg(prefix="JibberJim!~none@8212cl.b0nwbeoe.co.uk", command="QUIT", args=('"Bye!"',)) self.assertFalse(ircmsgs.isSplit(m)) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_ircutils.py0000644000175000017500000005445514535072470016363 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * 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.assertTrue(ircutils.hostmaskPatternEqual(s, s), '%r did not match itself.' % s) banmask = ircutils.banmask(s) self.assertTrue(ircutils.hostmaskPatternEqual(banmask, s), '%r did not match %r' % (s, banmask)) s = 'supybot!~supybot@dhcp065-024-075-056.columbus.rr.com' self.assertTrue(ircutils.hostmaskPatternEqual(s, s)) s = 'jamessan|work!~jamessan@209-6-166-196.c3-0.' \ 'abr-ubr1.sbo-abr.ma.cable.rcn.com' self.assertTrue(ircutils.hostmaskPatternEqual(s, s)) def testHostmaskSet(self): hs = ircutils.HostmaskSet() self.assertEqual(hs.match("nick!user@host"), None) hs.add("*!user@host") hs.add("*!user@host2") self.assertEqual(hs.match("nick!user@host"), "*!user@host") self.assertEqual(hs.match("nick!user@host2"), "*!user@host2") self.assertCountEqual(list(hs), ["*!user@host", "*!user@host2"]) hs.remove("*!user@host2") self.assertEqual(hs.match("nick!user@host"), "*!user@host") self.assertEqual(hs.match("nick!user@host2"), None) hs = ircutils.HostmaskSet(["*!user@host"]) self.assertEqual(hs.match("nick!user@host"), "*!user@host") def testExpiringHostmaskDict(self): hs = ircutils.ExpiringHostmaskDict() self.assertEqual(hs.match("nick!user@host"), None) time1 = time.time() + 15 time2 = time.time() + 10 hs["*!user@host"] = time1 hs["*!user@host2"] = time2 self.assertEqual(hs.match("nick!user@host"), "*!user@host") self.assertEqual(hs.match("nick!user@host2"), "*!user@host2") self.assertCountEqual(list(hs.items()), [("*!user@host", time1), ("*!user@host2", time2)]) del hs["*!user@host2"] self.assertEqual(hs.match("nick!user@host"), "*!user@host") self.assertEqual(hs.match("nick!user@host2"), None) timeFastForward(10) self.assertEqual(hs.match("nick!user@host"), "*!user@host") timeFastForward(10) self.assertEqual(hs.match("nick!user@host"), None) hs = ircutils.ExpiringHostmaskDict([("*!user@host", time.time() + 10)]) self.assertEqual(hs.match("nick!user@host"), "*!user@host") self.assertEqual(hs.match("nick!user@host2"), None) timeFastForward(11) self.assertEqual(hs.match("nick!user@host"), None) self.assertEqual(hs.match("nick!user@host2"), None) def testIsUserHostmask(self): self.assertTrue(ircutils.isUserHostmask(self.hostmask)) self.assertTrue(ircutils.isUserHostmask('a!b@c')) self.assertFalse(ircutils.isUserHostmask('!bar@baz')) self.assertFalse(ircutils.isUserHostmask('!@baz')) self.assertFalse(ircutils.isUserHostmask('!bar@')) self.assertFalse(ircutils.isUserHostmask('!@')) self.assertFalse(ircutils.isUserHostmask('foo!@baz')) self.assertFalse(ircutils.isUserHostmask('foo!bar@')) self.assertFalse(ircutils.isUserHostmask('')) self.assertFalse(ircutils.isUserHostmask('!')) self.assertFalse(ircutils.isUserHostmask('@')) self.assertFalse(ircutils.isUserHostmask('!bar@baz')) def testSplitHostmask(self): # This is the only valid case: self.assertEqual(ircutils.splitHostmask('foo!bar@baz'), ('foo', 'bar', 'baz')) # This ones are technically allowed by RFC1459, but never happens in # practice: self.assertEqual(ircutils.splitHostmask('foo!bar!qux@quux'), ('foo', 'bar!qux', 'quux')) self.assertEqual(ircutils.splitHostmask('foo!bar@baz@quux'), ('foo', 'bar@baz', 'quux')) self.assertEqual(ircutils.splitHostmask('foo!bar@baz!qux@quux'), ('foo', 'bar@baz!qux', 'quux')) # And this one in garbage, let's just make sure we don't crash: self.assertEqual(ircutils.splitHostmask('foo!bar@baz!qux'), ('foo', 'bar', 'baz!qux')) def testIsChannel(self): self.assertTrue(ircutils.isChannel('#')) self.assertTrue(ircutils.isChannel('&')) self.assertFalse(ircutils.isChannel('+')) self.assertTrue(ircutils.isChannel('+', chantypes='#&+!')) self.assertTrue(ircutils.isChannel('!')) self.assertTrue(ircutils.isChannel('#foo')) self.assertTrue(ircutils.isChannel('&foo')) self.assertFalse(ircutils.isChannel('+foo')) self.assertTrue(ircutils.isChannel('+foo', chantypes='#&+!')) self.assertTrue(ircutils.isChannel('!foo')) self.assertFalse(ircutils.isChannel('#foo bar')) self.assertFalse(ircutils.isChannel('#foo,bar')) self.assertFalse(ircutils.isChannel('#foobar\x07')) self.assertFalse(ircutils.isChannel('foo')) self.assertFalse(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 testWrap(self): pred = lambda s:len(s.encode()) s = ('foo bar baz qux ' * 100)[0:-1] r = ircutils.wrap(s, 10) self.assertLessEqual(max(map(pred, r)), 10) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = (''.join([chr(0x1f527), chr(0x1f527), chr(0x1f527), ' ']) * 100)\ [0:-1] r = ircutils.wrap(s, 20) self.assertLessEqual(max(map(pred, r)), 20, (max(map(pred, r)), repr(r))) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = ('foobarbazqux ' * 100)[0:-1] r = ircutils.wrap(s, 10) self.assertLessEqual(max(map(pred, r)), 10) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = ('foobarbazqux' * 100)[0:-1] r = ircutils.wrap(s, 10) self.assertLessEqual(max(map(pred, r)), 10) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = chr(233)*500 r = ircutils.wrap(s, 500) self.assertLessEqual(max(map(pred, r)), 500) r = ircutils.wrap(s, 139) self.assertLessEqual(max(map(pred, r)), 139) s = '\x02\x16 barbazqux' + ('foobarbazqux ' * 20)[0:-1] r = ircutils.wrap(s, 91) self.assertLessEqual(max(map(pred, r)), 91) 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.assertTrue(ircutils.isNick('jemfinch')) self.assertTrue(ircutils.isNick('jemfinch0')) self.assertTrue(ircutils.isNick('[0]')) self.assertTrue(ircutils.isNick('{jemfinch}')) self.assertTrue(ircutils.isNick('[jemfinch]')) self.assertTrue(ircutils.isNick('jem|finch')) self.assertTrue(ircutils.isNick('\\```')) self.assertTrue(ircutils.isNick('`')) self.assertTrue(ircutils.isNick('A')) self.assertFalse(ircutils.isNick('')) self.assertFalse(ircutils.isNick('8foo')) self.assertFalse(ircutils.isNick('10')) self.assertFalse(ircutils.isNick('-')) self.assertFalse(ircutils.isNick('-foo')) conf.supybot.protocols.irc.strictRfc.setValue(False) self.assertTrue(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.assertFalse(ircutils.isNick('foo bar')) conf.supybot.protocols.irc.strictRfc.setValue(False) self.assertFalse(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 irc = getTestIrc() msg = ircmsgs.IrcMsg(':%s PRIVMSG #channel :stuff' % self.hostmask) irc._tagMsg(msg) 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.assertTrue(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): irc = getTestIrc() prefix = 'foo!bar@baz' channel = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) private = ircmsgs.privmsg('jemfinch', 'bar baz', prefix=prefix) irc._tagMsg(channel) irc._tagMsg(private) 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.assertIn('#foo', d) d['#fOOBAR[]'] = None self.assertIn('#foobar{}', 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.assertEqual(d, copy.copy(d)) self.assertEqual(d, copy.deepcopy(d)) class IrcSetTestCase(SupyTestCase): def test(self): s = ircutils.IrcSet() s.add('foo') s.add('bar') self.assertIn('foo', s) self.assertIn('FOO', s) s.discard('alfkj') s.remove('FOo') self.assertNotIn('foo', s) self.assertNotIn('FOo', s) def testCopy(self): s = ircutils.IrcSet() s.add('foo') s.add('bar') s1 = copy.deepcopy(s) self.assertIn('foo', s) self.assertIn('FOO', s) s.discard('alfkj') s.remove('FOo') self.assertNotIn('foo', s) self.assertNotIn('FOo', s) self.assertIn('foo', s1) self.assertIn('FOO', s1) s1.discard('alfkj') s1.remove('FOo') self.assertNotIn('foo', s1) self.assertNotIn('FOo', 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.assertEqual(s1, s2) self.assertEqual(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: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_misc.py0000644000175000017500000000641514535072470015451 0ustar00valval### # Copyright (c) 2019, James Lu # Copyright (c) 2019-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot from supybot.test import * class MiscTestCase(SupyTestCase): def testAuthorExpand(self): # The standard 3 pair: name, nick, email self.assertEqual(str(supybot.authors.progval), 'Valentin Lorentz (progval) ') # All 3 provided, but name == nick self.assertEqual(str(supybot.Author('foobar', 'foobar', 'foobar@example.net')), 'foobar ') # Only name provided self.assertEqual(str(supybot.Author('somedev')), 'somedev') # Only nick provided self.assertEqual(str(supybot.Author(nick='somedev')), 'somedev') # Only name and nick provided self.assertEqual(str(supybot.Author('James Lu', 'tacocat')), 'James Lu (tacocat)') # Only name and nick provided, but name == nick self.assertEqual(str(supybot.Author('tacocat', 'tacocat')), 'tacocat') # Only name and email self.assertEqual(str(supybot.authors.jlu), 'James Lu ') # Only nick and email self.assertEqual(str(supybot.Author(nick='abcdef', email='abcdef@example.org')), 'abcdef ') # Only email? self.assertEqual(str(supybot.Author(email='xyzzy@localhost.localdomain')), 'Unknown author ') def testAuthorExpandShort(self): self.assertEqual(supybot.authors.progval.format(short=True), 'Valentin Lorentz (progval)') self.assertEqual(supybot.authors.jlu.format(short=True), 'James Lu') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_plugin.py0000644000175000017500000000374014535072470016012 0ustar00valval### # Copyright (c) 2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import supybot.plugin as plugin class FunctionsTestCase(SupyTestCase): def testLoadPluginModule(self): self.assertRaises(ImportError, plugin.loadPluginModule, 'asldj') self.assertTrue(plugin.loadPluginModule('Owner')) self.assertTrue(plugin.loadPluginModule('owner')) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_plugin_create.py0000644000175000017500000001134014535072470017330 0ustar00valval### # Copyright (c) 2018, James Lu # Copyright (c) 2018-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import supybot from supybot.test import * import subprocess import unittest import os import shutil import compileall try: from shutil import which except ImportError: # Python < 3.3 from distutils.spawn import find_executable as which TEST_PLUGIN_NAME = "TestPlugin" class PluginCreateTestCase(SupyTestCase): @staticmethod def _communicate(proc, text): outs, errs = proc.communicate(input=text) supybot.log.info("testPluginCreate: supybot-plugin-create outs:") for line in outs.splitlines(): supybot.log.info(" %s", line.decode()) supybot.log.info("testPluginCreate: supybot-plugin-create errs:") for line in errs.splitlines(): supybot.log.info(" %s", line.decode()) def _makeplugin(self): proc = subprocess.Popen(['supybot-plugin-create'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # In order: plugin name, threaded?, author, use Supybot license?, description cmdinput = TEST_PLUGIN_NAME.encode() + b""" n Test Case Runner y Dummy test plugin """ self._communicate(proc, cmdinput) def testPluginCreate(self): if not which('supybot-plugin-create'): # When tests are run automatically as part of a build process (e.g. in Debian), # don't assume that the bot is installed yet. print('Skipping supybot-plugin-create test because Limnoria has not been installed yet') return tmpdir = conf.supybot.directories.data.tmp() curdir = os.getcwd() try: os.chdir(tmpdir) if TEST_PLUGIN_NAME in os.listdir('.'): supybot.log.info("testPluginCreate: Removing old TestPlugin directory") shutil.rmtree(TEST_PLUGIN_NAME) self._makeplugin() self.assertIn(TEST_PLUGIN_NAME, os.listdir('.')) # Make sure that out generated plugin is valid compileall.compile_dir(TEST_PLUGIN_NAME) finally: os.chdir(curdir) class PluginCreateNoninteractiveTestCase(PluginCreateTestCase): def _makeplugin(self): with open(os.devnull, 'w') as devnull: # Compat with Python < 3.3 retcode = subprocess.call(['supybot-plugin-create', '-n', TEST_PLUGIN_NAME, '--author=skynet', '--desc=Some description'], stdin=devnull) self.assertFalse(retcode) # Check that the return code is 0 class PluginCreatePartialArgsTestCase(PluginCreateTestCase): def _makeplugin(self): # We passed in a subset of args, so the script should only prompt for the # ones not given proc = subprocess.Popen(['supybot-plugin-create', '-n', TEST_PLUGIN_NAME], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # In order: threaded?, author, use Supybot license?, description cmdinput = TEST_PLUGIN_NAME.encode() + b""" Test Case Runner y Dummy test plugin """ self._communicate(proc, cmdinput) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_plugins.py0000644000175000017500000000457614535072470016205 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy # Copyright (c) 2010-2021, 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 * import supybot.conf as conf import supybot.irclib as irclib import supybot.plugins as plugins class PluginsTestCase(SupyTestCase): def testMakeChannelFilename(self): self.assertEqual( plugins.makeChannelFilename('dir', '#foo'), conf.supybot.directories.data() + '/#foo/dir') self.assertEqual( plugins.makeChannelFilename('dir', '#f/../oo'), conf.supybot.directories.data() + '/#f..oo/dir') self.assertEqual( plugins.makeChannelFilename('dir', '/./'), conf.supybot.directories.data() + '/_/dir') self.assertEqual( plugins.makeChannelFilename('dir', '/../'), conf.supybot.directories.data() + '/__/dir') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_registry.py0000644000175000017500000003174314535072470016370 0ustar00valval### # Copyright (c) 2004, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * 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.assertTrue(v()) v.setValue(False) self.assertFalse(v()) v.set('True') self.assertTrue(v()) v.set('False') self.assertFalse(v()) v.set('On') self.assertTrue(v()) v.set('Off') self.assertFalse(v()) v.set('enable') self.assertTrue(v()) v.set('disable') self.assertFalse(v()) v.set('toggle') self.assertTrue(v()) v.set('toggle') self.assertFalse(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.assertTrue(v().match('foo')) v.set('') self.assertEqual(v(), None) def testRegexpSetValue(self): v = registry.Regexp(None, 'help') self.assertRaises(registry.InvalidRegistryValue, v.setValue, r'foo') self.assertRaises(registry.InvalidRegistryValue, v.setValue, re.compile(r'foo')) def testRegexpDefaultString(self): v = registry.Regexp('m/foo/', 'help') self.assertEqual(v(), re.compile('foo')) v = registry.Regexp('', 'help') self.assertEqual(v(), None) v = registry.Regexp(None, 'help') self.assertEqual(v(), None) def testBackslashesKeys(self): conf.supybot.reply.whenAddressedBy.strings.get(':foo').set('=/*') filename = conf.supybot.directories.conf.dirize('backslashes1.conf') registry.close(conf.supybot, filename) registry.open_registry(filename) value = conf.supybot.reply.whenAddressedBy.strings.get(':foo')() self.assertEqual(value, set(['=/*'])) def testBackslashesValues(self): conf.supybot.reply.whenAddressedBy.chars.set('\\') filename = conf.supybot.directories.conf.dirize('backslashes2.conf') registry.close(conf.supybot, filename) registry.open_registry(filename) self.assertEqual(conf.supybot.reply.whenAddressedBy.chars(), '\\') def testSpacesValues(self): with conf.supybot.networks.test.password.context(' foo '): self.assertEqual(conf.supybot.networks.test.password(), ' foo ') filename = conf.supybot.directories.conf.dirize('spaces.conf') registry.close(conf.supybot, filename) registry.open_registry(filename) self.assertEqual(conf.supybot.networks.test.password(), ' foo ') def testReload(self): import supybot.world as world with conf.supybot.reply.whenAddressedBy.chars.context('@'): with conf.supybot.reply.whenAddressedBy.chars\ .get(':testreloadnet').get('#testreloadchan').context('#'): with conf.supybot.reply.whenAddressedBy.chars \ .get(':testreloadnet2').context(','): newircs = [] for name in ("testreloadnet", "testreloadnet2", "testnet"): class newirc: network = name newircs.append(newirc) world.ircs.append(newirc) try: # separate function, to keep indent level to a sane # level self._testReload() finally: for newirc in newircs: world.ircs.remove(newirc) def _testReload(self): # sanity checks before the actual tests self.assertEqual(conf.supybot.reply.whenAddressedBy.chars(), '@') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testnet', channel='testchan')(), '@') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(channel='#testchan')(), '@') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testreloadnet2')(), ',') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testreloadnet', channel='#testreloadchan')(), '#') filename = conf.supybot.directories.conf.dirize('reload.conf') registry.close(conf.supybot, filename) with open(filename, 'at') as fd: fd.write('supybot.reply.whenAddressedBy.chars: !') registry.open_registry(filename) # new global value applies self.assertEqual(conf.supybot.reply.whenAddressedBy.chars(), '!') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testnet', channel='#testchan')(), '!') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testnet', channel='#testchan')(), '!') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(channel='#testchan')(), '!') # remain unchanged self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testreloadnet2')(), ',') self.assertEqual(conf.supybot.reply.whenAddressedBy.chars .getSpecific(network='testreloadnet', channel='#testreloadchan')(), '#') 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) class InheritanceTestCase(SupyTestCase): def testChild(self): parent = registry.String('foo', 'help') parent._supplyDefault = True self.assertTrue(parent._wasSet) self.assertEqual(parent(), 'foo') child = parent.get('child') self.assertFalse(child._wasSet) self.assertEqual(child(), 'foo') parent.setValue('bar') self.assertTrue(parent._wasSet) self.assertEqual(parent(), 'bar') self.assertFalse(child._wasSet) self.assertEqual(child(), 'bar') # Takes the new parent value child.setValue('baz') self.assertTrue(parent._wasSet) self.assertEqual(parent(), 'bar') self.assertTrue(child._wasSet) self.assertEqual(child(), 'baz') parent.setValue('qux') self.assertTrue(parent._wasSet) self.assertEqual(parent(), 'qux') self.assertTrue(child._wasSet) self.assertEqual(child(), 'baz') # Keeps its own value # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_schedule.py0000644000175000017500000001073714535072470016314 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * import time import supybot.schedule as schedule # Use a different driver name than the main Schedule, because Schedule # is a driver so it self-registers in the dict of drivers with its name. # So by keeping the name we overwrite the main Schedule in the dict of # drivers (which is supposed to be in sync with the one accessible # as supybot.schedule.schedule). class FakeSchedule(schedule.Schedule): def name(self): return 'FakeSchedule' class TestSchedule(SupyTestCase): def testSchedule(self): sched = FakeSchedule() 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) timeFastForward(1.2) sched.run() self.assertEqual(i[0], 1) timeFastForward(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) timeFastForward(3) self.assertEqual(i[0], 11) def testReschedule(self): sched = FakeSchedule() i = [0] def inc(): i[0] += 1 n = sched.addEvent(inc, time.time() + 1) sched.rescheduleEvent(n, time.time() + 3) timeFastForward(1.2) sched.run() self.assertEqual(i[0], 0) timeFastForward(2) sched.run() self.assertEqual(i[0], 1) def testPeriodic(self): sched = FakeSchedule() i = [0] def inc(): i[0] += 1 n = sched.addPeriodicEvent(inc, 1, name='test_periodic') timeFastForward(0.6) sched.run() # 0.6 self.assertEqual(i[0], 1) timeFastForward(0.6) sched.run() # 1.2 self.assertEqual(i[0], 2) timeFastForward(0.6) sched.run() # 1.8 self.assertEqual(i[0], 2) timeFastForward(0.6) sched.run() # 2.4 self.assertEqual(i[0], 3) sched.removePeriodicEvent(n) timeFastForward(1) sched.run() # 3.4 self.assertEqual(i[0], 3) def testCountedPeriodic(self): sched = FakeSchedule() i = [0] def inc(): i[0] += 1 n = sched.addPeriodicEvent(inc, 1, name='test_periodic', count=3) timeFastForward(0.6) sched.run() # 0.6 self.assertEqual(i[0], 1) timeFastForward(0.6) sched.run() # 1.2 self.assertEqual(i[0], 2) timeFastForward(0.6) sched.run() # 1.8 self.assertEqual(i[0], 2) timeFastForward(0.6) sched.run() # 2.4 self.assertEqual(i[0], 3) timeFastForward(1) sched.run() # 3.4 self.assertEqual(i[0], 3) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_standardSubstitute.py0000644000175000017500000000725714535072470020417 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, 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 * 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): @retry() def testStandardSubstitute(self): irc = getTestIrc() irc.state.channels = {'#foo': holder()} f = ircutils.standardSubstitute msg = ircmsgs.privmsg('#foo', 'filler', prefix='biff!quux@xyzzy') irc._tagMsg(msg) s = f(irc, msg, '$rand') try: int(s) except ValueError: self.fail('$rand wasn\'t an int.') s = f(irc, msg, '$randomInt') try: int(s) except ValueError: self.fail('$randomint wasn\'t an int.') self.assertEqual(f(irc, msg, '$botnick'), irc.nick) self.assertEqual(f(irc, msg, '$who'), msg.nick) self.assertEqual(f(irc, msg, '$WHO'), msg.nick, 'stand. sub. not case-insensitive.') self.assertEqual(f(irc, msg, '$nick'), msg.nick) self.assertNotEqual(f(irc, msg, '$randomdate'), '$randomdate') q = f(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(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(irc, msg, '$today'), '$today') self.assertNotEqual(f(irc, msg, '$now'), '$now') n = f(irc, msg, '$randnick') self.assertIn(n, irc.state.channels['#foo'].users) n = f(irc, msg, '$randomnick') self.assertIn(n, irc.state.channels['#foo'].users) n = f(irc, msg, '$randomnick '*100) L = n.split() self.assertFalse(all(L[0].__eq__, L), 'all $randomnicks were the same') c = f(irc, msg, '$channel') self.assertEqual(c, msg.args[0]) net = f(irc, msg, '$network') self.assertEqual(net, irc.network) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_utils.py0000644000175000017500000014030514535072470015653 0ustar00valval### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009,2011, James McCoy # Copyright (c) 2010-2022, 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 * import sys import time import pickle import supybot.conf as conf import supybot.utils as utils from supybot.utils.structures import * import supybot.utils.minisix as minisix 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 range(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.assertEqual(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.assertTrue(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.assertFalse(AL) AL[1] = 2 AL[2] = 3 AL[3] = 4 self.assertTrue(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.assertTrue(r[0] == '"' and r[-1] == '"', s) def testPerlReToPythonRe(self): f = utils.str.perlReToPythonRe r = f('m/foo/') self.assertTrue(r.search('foo')) r = f('/foo/') self.assertTrue(r.search('foo')) r = f('m/\\//') self.assertTrue(r.search('/')) r = f('m/cat/i') self.assertTrue(r.search('CAT')) self.assertRaises(ValueError, f, 'm/?/') def testP2PReDifferentSeparator(self): r = utils.str.perlReToPythonRe('m!foo!') self.assertTrue(r.search('foo')) r = utils.str.perlReToPythonRe('m{cat}') self.assertTrue(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(r'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(r'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.assertTrue(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 testCommaAndifyConfig(self): f = utils.str.commaAndify L = ['foo', 'bar'] with conf.supybot.reply.format.list.maximumItems.context(3): self.assertEqual(f(L), 'foo and bar') L.append('baz') self.assertEqual(f(L), 'foo, bar, and baz') L.append('qux') self.assertEqual(f(L), 'foo, bar, and 2 others') L.append('quux') self.assertEqual(f(L), 'foo, bar, and 3 others') 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.assertLessEqual(len(f('x'*35, 30)), 30) self.assertTrue(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 range(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.assertTrue(any(lambda i: i == 0, range(10))) self.assertFalse(any(None, range(1))) self.assertTrue(any(None, range(2))) self.assertFalse(any(None, [])) def testAll(self): all = utils.iter.all self.assertFalse(all(lambda i: i == 0, range(10))) self.assertFalse(all(lambda i: i % 2, range(2))) self.assertFalse(all(lambda i: i % 2 == 0, [1, 3, 5])) self.assertTrue(all(lambda i: i % 2 == 0, [2, 4, 6])) self.assertTrue(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.assertTrue(utils.file.mktemp()) self.assertTrue(utils.file.mktemp()) self.assertTrue(utils.file.mktemp()) def testSanitizeName(self): self.assertEqual(utils.file.sanitizeName('#foo'), '#foo') self.assertEqual(utils.file.sanitizeName('#f/../oo'), '#f..oo') class NetTest(SupyTestCase): def testEmailRe(self): emailRe = utils.net.emailRe self.assertTrue(emailRe.match('jemfinch@supybot.com')) def testIsIP(self): isIP = utils.net.isIP self.assertFalse(isIP('a.b.c')) self.assertFalse(isIP('256.0.0.0')) self.assertFalse(isIP('127.0.0.1 127.0.0.1')) self.assertTrue(isIP('0.0.0.0')) self.assertTrue(isIP('100.100.100.100')) self.assertTrue(isIP('255.255.255.255')) def testIsIPV6(self): f = utils.net.isIPV6 self.assertFalse(f('2001:: 2001::')) self.assertTrue(f('2001::')) self.assertTrue(f('2001:888:0:1::666')) class WebTest(SupyTestCase): def testHtmlToText(self): self.assertEqual( utils.web.htmlToText('foo

    barbazqux

    quux'), 'foo barbazqux quux') 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.assertEqual(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.assertFalse(b) b.append(1) self.assertTrue(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.assertIn(0, b) self.assertIn(1, b) self.assertIn(2, b) self.assertNotIn(3, 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.assertEqual(b[2:4], L[2:4]) for _ in range(len(b)): b.append(0) b[2:4] = L[2:4] self.assertEqual(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.assertNotEqual(b, list(range(3))) b1 = RingBuffer(3) self.assertNotEqual(b, b1) b1.append(0) self.assertNotEqual(b, b1) b1.append(1) self.assertNotEqual(b, b1) b1.append(2) self.assertEqual(b, b1) b = RingBuffer(100, range(10)) b1 = RingBuffer(10, range(10)) self.assertNotEqual(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 range(n): q.enqueue(i) for i in range(n): self.assertEqual(q[i], i) for i in range(n, 0, -1): self.assertEqual(q[-i], n-i) for i in range(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 range(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.assertFalse(q, 'queue not zero after initialization') q.enqueue(1) self.assertTrue(q, 'queue zero after adding element') q.dequeue() self.assertFalse(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.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') self.assertEqual(q1, q2, 'initialized queues not equal') q1.enqueue(1) self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') q2.enqueue(1) self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') self.assertEqual(q1, q2, 'queues not equal after identical enqueue') q1.dequeue() self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') self.assertNotEqual(q1, q2, 'queues equal after one dequeue') q2.dequeue() self.assertEqual(q1, q2, 'queues not equal after both are dequeued') self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(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.assertNotIn(1, q, 'empty queue cannot have elements') q.enqueue(1) self.assertIn(1, q, 'recent enqueued element not in q') q.enqueue(2) self.assertIn(1, q, 'original enqueued element not in q') self.assertIn(2, q, 'second enqueued element not in q') q.dequeue() self.assertNotIn(1, q, 'dequeued element in q') self.assertIn(2, q, 'not dequeued element not in q') q.dequeue() self.assertNotIn(2, 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 range(n): q.enqueue(i) for i in range(n): self.assertEqual(q[i], i) for i in range(n, 0, -1): self.assertEqual(q[-i], n-i) for i in range(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 range(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.assertFalse(q, 'queue not zero after initialization') q.enqueue(1) self.assertTrue(q, 'queue zero after adding element') q.dequeue() self.assertFalse(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.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') self.assertEqual(q1, q2, 'initialized queues not equal') q1.enqueue(1) self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') q2.enqueue(1) self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') self.assertEqual(q1, q2, 'queues not equal after identical enqueue') q1.dequeue() self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(q2, q2, 'queue not equal to itself') self.assertNotEqual(q1, q2, 'queues equal after one dequeue') q2.dequeue() self.assertEqual(q1, q2, 'queues not equal after both are dequeued') self.assertEqual(q1, q1, 'queue not equal to itself') self.assertEqual(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.assertNotIn(1, q, 'empty queue cannot have elements') q.enqueue(1) self.assertIn(1, q, 'recent enqueued element not in q') q.enqueue(2) self.assertIn(1, q, 'original enqueued element not in q') self.assertIn(2, q, 'second enqueued element not in q') q.dequeue() self.assertNotIn(1, q, 'dequeued element in q') self.assertIn(2, q, 'not dequeued element not in q') q.dequeue() self.assertNotIn(2, 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.assertIn('foo', d) self.assertIn('bar', d) d = TwoWayDictionary({1: 2}) self.assertIn(1, d) self.assertIn(2, d) def testSetitem(self): d = TwoWayDictionary() d['foo'] = 'bar' self.assertIn('foo', d) self.assertIn('bar', d) def testDelitem(self): d = TwoWayDictionary(foo='bar') del d['foo'] self.assertNotIn('foo', d) self.assertNotIn('bar', d) d = TwoWayDictionary(foo='bar') del d['bar'] self.assertNotIn('bar', d) self.assertNotIn('foo', 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) timeFastForward(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) timeFastForward(1.1) self.assertEqual(len(q), 0) self.assertEqual(sum(q), 0) def testContains(self): q = TimeoutQueue(1) q.enqueue(1) self.assertIn(1, q) self.assertIn(1, q) # For some reason, the second one might fail. self.assertNotIn(2, q) timeFastForward(1.1) self.assertNotIn(1, q) def testIter(self): q = TimeoutQueue(1) q.enqueue(1) it1 = iter(q) timeFastForward(0.5) q.enqueue(2) it2 = iter(q) self.assertEqual(next(it1), 1) self.assertEqual(next(it2), 1) self.assertEqual(next(it2), 2) with self.assertRaises(StopIteration): next(it2) timeFastForward(0.6) self.assertEqual(next(it1), 2) with self.assertRaises(StopIteration): next(it1) it3 = iter(q) self.assertEqual(next(it3), 2) with self.assertRaises(StopIteration): next(it3) def testReset(self): q = TimeoutQueue(10) q.enqueue(1) self.assertIn(1, q) q.reset() self.assertNotIn(1, q) def testClean(self): def iter_and_next(q): next(iter(q)) def contains(q): 42 in q for f in (len, repr, list, iter_and_next, contains): print(f) with self.subTest(f=f.__name__): q = TimeoutQueue(1) q.enqueue(1) timeFastForward(0.5) q.enqueue(2) self.assertEqual([x for (_, x) in q.queue], [1, 2]) f(q) self.assertEqual([x for (_, x) in q.queue], [1, 2]) timeFastForward(0.6) self.assertEqual([x for (_, x) in q.queue], [1, 2]) # not cleaned yet f(q) self.assertEqual([x for (_, x) in q.queue], [2]) # now it is class TestCacheDict(SupyTestCase): def testMaxNeverExceeded(self): max = 10 d = CacheDict(10) for i in range(max**2): d[i] = i self.assertLessEqual(len(d), max) self.assertIn(i, d) self.assertEqual(d[i], i) class TestExpiringDict(SupyTestCase): def testInit(self): d = ExpiringDict(10) self.assertEqual(dict(d), {}) d['foo'] = 'bar' d['baz'] = 'qux' self.assertEqual(dict(d), {'foo': 'bar', 'baz': 'qux'}) def testExpire(self): d = ExpiringDict(10) self.assertEqual(dict(d), {}) d['foo'] = 'bar' timeFastForward(11) d['baz'] = 'qux' # Moves 'foo' to the old gen self.assertEqual(dict(d), {'foo': 'bar', 'baz': 'qux'}) timeFastForward(11) self.assertEqual(dict(d), {'foo': 'bar', 'baz': 'qux'}) d['quux'] = 42 # removes the old gen and moves 'baz' to the old gen self.assertEqual(dict(d), {'baz': 'qux', 'quux': 42}) def testEquality(self): d1 = ExpiringDict(10) d2 = ExpiringDict(10) self.assertEqual(d1, d2) d1['foo'] = 'bar' self.assertNotEqual(d1, d2) timeFastForward(5) # check they are equal despite the time difference d2['foo'] = 'bar' self.assertEqual(d1, d2) timeFastForward(7) d1['baz'] = 'qux' # moves 'foo' to the old gen (12 seconds old) d2['baz'] = 'qux' # does not move it (7 seconds old) self.assertEqual(d1, d2) class TestTimeoutDict(SupyTestCase): def testInit(self): d = TimeoutDict(10) self.assertEqual(dict(d), {}) d['foo'] = 'bar' d['baz'] = 'qux' self.assertEqual(dict(d), {'foo': 'bar', 'baz': 'qux'}) def testExpire(self): d = TimeoutDict(10) self.assertEqual(dict(d), {}) d['foo'] = 'bar' timeFastForward(11) d['baz'] = 'qux' self.assertEqual(dict(d), {'baz': 'qux'}) timeFastForward(11) self.assertEqual(dict(d), {}) d['quux'] = 42 self.assertEqual(dict(d), {'quux': 42}) def testEquality(self): d1 = TimeoutDict(10) d2 = TimeoutDict(10) self.assertEqual(d1, d2) d1['foo'] = 'bar' self.assertNotEqual(d1, d2) timeFastForward(5) # check they are equal despite the time difference d2['foo'] = 'bar' self.assertEqual(d1, d2) timeFastForward(7) self.assertNotEqual(d1, d2) self.assertEqual(d1, {}) self.assertEqual(d2, {'foo': 'bar'}) timeFastForward(7) self.assertEqual(d1, d2) self.assertEqual(d1, {}) self.assertEqual(d2, {}) d1['baz'] = 'qux' d2['baz'] = 'qux' self.assertEqual(d1, d2) class TestTruncatableSet(SupyTestCase): def testBasics(self): s = TruncatableSet(['foo', 'bar', 'baz', 'qux']) self.assertEqual(s, set(['foo', 'bar', 'baz', 'qux'])) self.assertIn('foo', s) self.assertIn('bar', s) self.assertNotIn('quux', s) s.discard('baz') self.assertIn('foo', s) self.assertNotIn('baz', s) s.add('quux') self.assertIn('quux', 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'])) class UtilsPythonTest(SupyTestCase): def test_dict(self): class Foo: def __hasattr__(self, n): raise Exception(n) def __getattr__(self, n): raise Exception(n) def f(): self = Foo() self.bar = 'baz' raise Exception('f') try: f() except: res = utils.python.collect_extra_debug_data() self.assertTrue(re.search('self.bar.*=.*baz', res), res) def test_slots(self): class Foo: __slots__ = ('bar',) def __hasattr__(self, n): raise Exception(n) def __getattr__(self, n): raise Exception(n) def f(): self = Foo() self.bar = 'baz' raise Exception('f') try: f() except: res = utils.python.collect_extra_debug_data() # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702131000.0 limnoria-2023.11.18/test/test_yn.py0000644000175000017500000000713014535072470015137 0ustar00valval### # Copyright (c) 2014, Artur Krysiak # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import sys import unittest from unittest import mock from supybot import questions from supybot.test import SupyTestCase # 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']) @unittest.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: