diff --git a/.gitignore b/.gitignore index 4984243..d70b37d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ *.egg *.egg-info .DS_Store +__pycache__ build +dist docs/_build diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..47e568b --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,31 @@ +v0.2 (released November 8, 2015): + +- Added a new command syntax allowing the caller to redirect replies to another + user. Example: "!dictionary >Fred earwig" +- Added unload() hooks to commands and tasks, called when they are killed + during a reload. +- Added 'rc' hook type to allow IRC commands to respond to RC watcher events. +- Added 'part' hook type as a counterpart to 'join'. +- Added !stalk/!watch. +- Added !watchers. +- Added !epoch as a subcommand of !time. +- Added !version as a subcommand of !help. +- Expanded and improved !remind. +- Improved general behavior of !access and !threads. +- Fixed API behavior when blocked, when using AssertEdit, and under other + circumstances. +- Added copyvio detector functionality: specifying a max time for checks; + improved exclusion support. URL loading and parsing is parallelized to speed + up check times, with a multi-threaded worker model that avoids concurrent + requests to the same domain. Improvements to the comparison algorithm. Fixed + assorted bugs. +- Added support for Wikimedia Labs when creating a config file. +- Added and improved lazy importing for various dependencies. +- Fixed a bug in job scheduling. +- Improved client-side SQL buffering; made Category objects iterable. +- Default to using HTTPS for new sites. +- Updated documentation. + +v0.1 (released August 31, 2012): + +- Initial release. diff --git a/LICENSE b/LICENSE index 104339b..f1e78b1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2009-2012 Ben Kurtovic +Copyright (C) 2009-2015 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 4a9e3b0..896c1a2 100644 --- a/README.rst +++ b/README.rst @@ -36,15 +36,18 @@ setup.py test`` from the project's root directory. Note that some tests require an internet connection, and others may take a while to run. Coverage is currently rather incomplete. -Latest release (v0.1) +Latest release (v0.2) ~~~~~~~~~~~~~~~~~~~~~ EarwigBot is available from the `Python Package Index`_, so you can install the latest release with ``pip install earwigbot`` (`get pip`_). +If you get an error while pip is installing dependencies, you may be missing +some header files. For example, on Ubuntu, see `this StackOverflow post`_. + You can also install it from source [1]_ directly:: - curl -Lo earwigbot.tgz https://github.com/earwig/earwigbot/tarball/v0.1 + curl -Lo earwigbot.tgz https://github.com/earwig/earwigbot/tarball/v0.2 tar -xf earwigbot.tgz cd earwig-earwigbot-* python setup.py install @@ -55,10 +58,10 @@ Development version ~~~~~~~~~~~~~~~~~~~ You can install the development version of the bot from ``git`` by using -setuptools/distribute's ``develop`` command [1]_, probably on the ``develop`` -branch which contains (usually) working code. ``master`` contains the latest -release. EarwigBot uses `git flow`_, so you're free to -browse by tags or by new features (``feature/*`` branches):: +setuptools's ``develop`` command [1]_, probably on the ``develop`` branch which +contains (usually) working code. ``master`` contains the latest release. +EarwigBot uses `git flow`_, so you're free to browse by tags or by new features +(``feature/*`` branches):: git clone git://github.com/earwig/earwigbot.git earwigbot cd earwigbot @@ -133,8 +136,8 @@ Custom IRC commands ~~~~~~~~~~~~~~~~~~~ Custom commands are subclasses of `earwigbot.commands.Command`_ that override -``Command``'s ``process()`` (and optionally ``check()`` or ``setup()``) -methods. +``Command``'s ``process()`` (and optionally ``check()``, ``setup()``, or +``unload()``) methods. The bot has a wide selection of built-in commands and plugins to act as sample code and/or to give ideas. Start with test_, and then check out chanops_ and @@ -144,7 +147,7 @@ Custom bot tasks ~~~~~~~~~~~~~~~~ Custom tasks are subclasses of `earwigbot.tasks.Task`_ that override ``Task``'s -``run()`` (and optionally ``setup()``) methods. +``run()`` (and optionally ``setup()`` or ``unload()``) methods. See the built-in wikiproject_tagger_ task for a relatively straightforward task, or the afc_statistics_ plugin for a more complicated one. @@ -188,8 +191,9 @@ Footnotes .. _several ongoing tasks: http://en.wikipedia.org/wiki/User:EarwigBot#Tasks .. _my instance of EarwigBot: http://en.wikipedia.org/wiki/User:EarwigBot .. _earwigbot-plugins: https://github.com/earwig/earwigbot-plugins -.. _Python Package Index: http://pypi.python.org +.. _Python Package Index: https://pypi.python.org/pypi/earwigbot .. _get pip: http://pypi.python.org/pypi/pip +.. _this StackOverflow post: http://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu/6504860#6504860 .. _git flow: http://nvie.com/posts/a-successful-git-branching-model/ .. _explanation of YAML: http://en.wikipedia.org/wiki/YAML .. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/develop/earwigbot/bot.py @@ -202,4 +206,4 @@ Footnotes .. _wikiproject_tagger: https://github.com/earwig/earwigbot/blob/develop/earwigbot/tasks/wikiproject_tagger.py .. _afc_statistics: https://github.com/earwig/earwigbot-plugins/blob/develop/tasks/afc_statistics.py .. _its code and docstrings: https://github.com/earwig/earwigbot/tree/develop/earwigbot/wiki -.. _Let me know: ben.kurtovic@verizon.net +.. _Let me know: ben.kurtovic@gmail.com diff --git a/docs/conf.py b/docs/conf.py index bd18ce3..a8e5cc6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,16 +41,16 @@ master_doc = 'index' # General information about the project. project = u'EarwigBot' -copyright = u'2009, 2010, 2011, 2012 Ben Kurtovic' +copyright = u'2009-2015 Ben Kurtovic' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = '0.2' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/customizing.rst b/docs/customizing.rst index 3336353..ed5e6f3 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -86,8 +86,9 @@ Custom IRC commands Custom commands are subclasses of :py:class:`earwigbot.commands.Command` that override :py:class:`~earwigbot.commands.Command`'s :py:meth:`~earwigbot.commands.Command.process` (and optionally -:py:meth:`~earwigbot.commands.Command.check` or -:py:meth:`~earwigbot.commands.Command.setup`) methods. +:py:meth:`~earwigbot.commands.Command.check`, +:py:meth:`~earwigbot.commands.Command.setup`, or +:py:meth:`~earwigbot.commands.Command.unload`) methods. :py:class:`~earwigbot.commands.Command`'s docstrings should explain what each attribute and method is for and what they should be overridden with, but these @@ -108,9 +109,10 @@ are the basics: - Class attribute :py:attr:`~earwigbot.commands.Command.hooks` is a list of the "IRC events" that this command might respond to. It defaults to ``["msg"]``, but options include ``"msg_private"`` (for private messages only), - ``"msg_public"`` (for channel messages only), and ``"join"`` (for when a user - joins a channel). See the afc_status_ plugin for a command that responds to - other hook types. + ``"msg_public"`` (for channel messages only), ``"join"`` (for when a user + joins a channel), ``"part"`` (for when a user parts a channel), and ``"rc"`` + (for recent change messages from the IRC watcher). See the afc_status_ plugin + for a command that responds to other hook types. - Method :py:meth:`~earwigbot.commands.Command.setup` is called *once* with no arguments immediately after the command is first loaded. Does nothing by @@ -153,6 +155,10 @@ are the basics: `, and :py:meth:`part(chan) `. +- Method :py:meth:`~earwigbot.commands.Command.unload` is called *once* with no + arguments immediately before the command is unloaded, such as when someone + uses ``!reload``. Does nothing by default. + Commands have access to :py:attr:`config.commands[command_name]` for config information, which is a node in :file:`config.yml` like every other attribute of :py:attr:`bot.config`. This can be used to store, for example, API keys or @@ -174,7 +180,8 @@ Custom bot tasks Custom tasks are subclasses of :py:class:`earwigbot.tasks.Task` that override :py:class:`~earwigbot.tasks.Task`'s :py:meth:`~earwigbot.tasks.Task.run` (and optionally -:py:meth:`~earwigbot.tasks.Task.setup`) methods. +:py:meth:`~earwigbot.tasks.Task.setup` or +:py:meth:`~earwigbot.tasks.Task.unload`) methods. :py:class:`~earwigbot.tasks.Task`'s docstrings should explain what each attribute and method is for and what they should be overridden with, but these @@ -219,6 +226,10 @@ are the basics: the task's code goes. For interfacing with MediaWiki sites, read up on the :doc:`Wiki Toolset `. +- Method :py:meth:`~earwigbot.tasks.Task.unload` is called *once* with no + arguments immediately before the task is unloaded. Does nothing by + default. + Tasks have access to :py:attr:`config.tasks[task_name]` for config information, which is a node in :file:`config.yml` like every other attribute of :py:attr:`bot.config`. This can be used to store, for example, edit summaries diff --git a/docs/index.rst b/docs/index.rst index 8d446dc..d61bae7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -EarwigBot v0.1 Documentation +EarwigBot v0.2 Documentation ============================ EarwigBot_ is a Python_ robot that edits Wikipedia_ and interacts with people diff --git a/docs/installation.rst b/docs/installation.rst index 12fc907..cc577ab 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,15 +13,18 @@ It's recommended to run the bot's unit tests before installing. Run some tests require an internet connection, and others may take a while to run. Coverage is currently rather incomplete. -Latest release (v0.1) +Latest release (v0.2) --------------------- EarwigBot is available from the `Python Package Index`_, so you can install the latest release with :command:`pip install earwigbot` (`get pip`_). +If you get an error while pip is installing dependencies, you may be missing +some header files. For example, on Ubuntu, see `this StackOverflow post`_. + You can also install it from source [1]_ directly:: - curl -Lo earwigbot.tgz https://github.com/earwig/earwigbot/tarball/v0.1 + curl -Lo earwigbot.tgz https://github.com/earwig/earwigbot/tarball/v0.2 tar -xf earwigbot.tgz cd earwig-earwigbot-* python setup.py install @@ -32,10 +35,10 @@ Development version ------------------- You can install the development version of the bot from :command:`git` by using -setuptools/`distribute`_'s :command:`develop` command [1]_, probably on the -``develop`` branch which contains (usually) working code. ``master`` contains -the latest release. EarwigBot uses `git flow`_, so you're free to browse by -tags or by new features (``feature/*`` branches):: +setuptools's :command:`develop` command [1]_, probably on the ``develop`` +branch which contains (usually) working code. ``master`` contains the latest +release. EarwigBot uses `git flow`_, so you're free to browse by tags or by new +features (``feature/*`` branches):: git clone git://github.com/earwig/earwigbot.git earwigbot cd earwigbot @@ -51,5 +54,5 @@ tags or by new features (``feature/*`` branches):: .. _earwigbot-plugins: https://github.com/earwig/earwigbot-plugins .. _Python Package Index: http://pypi.python.org .. _get pip: http://pypi.python.org/pypi/pip -.. _distribute: http://pypi.python.org/pypi/distribute +.. _this StackOverflow post: http://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu/6504860#6504860 .. _git flow: http://nvie.com/posts/a-successful-git-branching-model/ diff --git a/docs/tips.rst b/docs/tips.rst index 4f4052e..00a648f 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -42,5 +42,5 @@ Tips .. _logging: http://docs.python.org/library/logging.html .. _!git plugin: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/git.py -.. _Let me know: ben.kurtovic@verizon.net +.. _Let me know: ben.kurtovic@gmail.com .. _create an issue: https://github.com/earwig/earwigbot/issues diff --git a/earwigbot/__init__.py b/earwigbot/__init__.py index 696ce3f..b9b7bc3 100644 --- a/earwigbot/__init__.py +++ b/earwigbot/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -30,11 +30,11 @@ details. This documentation is also available `online """ __author__ = "Ben Kurtovic" -__copyright__ = "Copyright (C) 2009, 2010, 2011, 2012 Ben Kurtovic" +__copyright__ = "Copyright (C) 2009-2015 Ben Kurtovic" __license__ = "MIT License" -__version__ = "0.1" -__email__ = "ben.kurtovic@verizon.net" -__release__ = True +__version__ = "0.2" +__email__ = "ben.kurtovic@gmail.com" +__release__ = False if not __release__: def _get_git_commit_id(): @@ -45,7 +45,7 @@ if not __release__: commit_id = Repo(path).head.object.hexsha return commit_id[:8] try: - __version__ += ".git+" + _get_git_commit_id() + __version__ += "+git-" + _get_git_commit_id() except Exception: pass finally: @@ -64,5 +64,3 @@ managers = importer.new("earwigbot.managers") tasks = importer.new("earwigbot.tasks") util = importer.new("earwigbot.util") wiki = importer.new("earwigbot.wiki") - -del importer diff --git a/earwigbot/bot.py b/earwigbot/bot.py index 27dd9c0..df59950 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ import logging from threading import Lock, Thread, enumerate as enumerate_threads -from time import sleep, time +from time import gmtime, sleep from earwigbot import __version__ from earwigbot.config import BotConfig @@ -101,13 +101,10 @@ class Bot(object): def _start_wiki_scheduler(self): """Start the wiki scheduler in a separate thread if enabled.""" def wiki_scheduler(): + run_at = 15 while self._keep_looping: - time_start = time() self.tasks.schedule() - time_end = time() - time_diff = time_start - time_end - if time_diff < 60: # Sleep until the next minute - sleep(60 - time_diff) + sleep(60 + run_at - gmtime().tm_sec) if self.config.components.get("wiki_scheduler"): self.logger.info("Starting wiki scheduler") @@ -157,7 +154,7 @@ class Bot(object): tasks.append(thread.name) if tasks: log = "The following commands or tasks will be killed: {0}" - self.logger.warn(log.format(" ".join(tasks))) + self.logger.warn(log.format(", ".join(tasks))) @property def is_running(self): diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index ecb299c..67ba719 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -43,9 +43,9 @@ class Command(object): # be triggered by the command's name and its name only: commands = [] - # Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the - # default behavior; if you wish to override that, change the value in your - # command subclass: + # Hooks are "msg", "msg_private", "msg_public", "join", "part", and "rc". + # "msg" is the default behavior; if you wish to override that, change the + # value in your command subclass: hooks = ["msg"] def __init__(self, bot): @@ -120,3 +120,10 @@ class Command(object): command's body here. """ pass + + def unload(self): + """Hook called immediately before a command is unloaded. + + Does nothing by default; feel free to override. + """ + pass diff --git a/earwigbot/commands/access.py b/earwigbot/commands/access.py index 1132348..0b5bc43 100644 --- a/earwigbot/commands/access.py +++ b/earwigbot/commands/access.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -30,11 +30,8 @@ class Access(Command): commands = ["access", "permission", "permissions", "perm", "perms"] def process(self, data): - if not data.args: - self.reply(data, "Subcommands are self, list, add, remove.") - return permdb = self.config.irc["permissions"] - if data.args[0] == "self": + if not data.args or data.args[0] == "self": self.do_self(data, permdb) elif data.args[0] == "list": self.do_list(data, permdb) @@ -42,9 +39,11 @@ class Access(Command): self.do_add(data, permdb) elif data.args[0] == "remove": self.do_remove(data, permdb) + elif data.args[0] == "help": + self.reply(data, "Subcommands are self, list, add, and remove.") else: - msg = "Unknown subcommand \x0303{0}\x0F.".format(data.args[0]) - self.reply(data, msg) + msg = "Unknown subcommand \x0303{0}\x0F. Subcommands are self, list, add, remove." + self.reply(data, msg.format(data.args[0])) def do_self(self, data, permdb): if permdb.is_owner(data): @@ -59,9 +58,9 @@ class Access(Command): def do_list(self, data, permdb): if len(data.args) > 1: if data.args[1] in ["owner", "owners"]: - name, rules = "owners", permdb.data.get(permdb.OWNER) + name, rules = "owners", permdb.users.get(permdb.OWNER) elif data.args[1] in ["admin", "admins"]: - name, rules = "admins", permdb.data.get(permdb.ADMIN) + name, rules = "admins", permdb.users.get(permdb.ADMIN) else: msg = "Unknown access level \x0302{0}\x0F." self.reply(data, msg.format(data.args[1])) @@ -72,9 +71,9 @@ class Access(Command): msg = "No bot {0}.".format(name) self.reply(data, msg) else: - owners = len(permdb.data.get(permdb.OWNER, [])) - admins = len(permdb.data.get(permdb.ADMIN, [])) - msg = "There are {0} bot owners and {1} bot admins. Use '!{2} list owners' or '!{2} list admins' for details." + owners = len(permdb.users.get(permdb.OWNER, [])) + admins = len(permdb.users.get(permdb.ADMIN, [])) + msg = "There are \x02{0}\x0F bot owners and \x02{1}\x0F bot admins. Use '!{2} list owners' or '!{2} list admins' for details." self.reply(data, msg.format(owners, admins, data.command)) def do_add(self, data, permdb): @@ -113,7 +112,7 @@ class Access(Command): def get_user_from_args(self, data, permdb): if not permdb.is_owner(data): - msg = "You must be a bot owner to add users to the access list." + msg = "You must be a bot owner to add or remove users to the access list." self.reply(data, msg) return levels = ["owner", "owners", "admin", "admins"] diff --git a/earwigbot/commands/calc.py b/earwigbot/commands/calc.py index de16202..c3dd998 100644 --- a/earwigbot/commands/calc.py +++ b/earwigbot/commands/calc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -73,7 +73,7 @@ class Calc(Command): ('\$', 'USD '), (r'\bKB\b', 'kilobytes'), (r'\bMB\b', 'megabytes'), - (r'\bGB\b', 'kilobytes'), + (r'\bGB\b', 'gigabytes'), ('kbps', '(kilobits / second)'), ('mbps', '(megabits / second)') ] diff --git a/earwigbot/commands/chanops.py b/earwigbot/commands/chanops.py index ea54500..731f52b 100644 --- a/earwigbot/commands/chanops.py +++ b/earwigbot/commands/chanops.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/earwigbot/commands/crypt.py b/earwigbot/commands/crypt.py index 0123be1..f88472d 100644 --- a/earwigbot/commands/crypt.py +++ b/earwigbot/commands/crypt.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,10 +22,11 @@ import hashlib -from Crypto.Cipher import Blowfish - +from earwigbot import importer from earwigbot.commands import Command +Blowfish = importer.new("Crypto.Cipher.Blowfish") + class Crypt(Command): """Provides hash functions with !hash (!hash list for supported algorithms) and Blowfish encryption with !encrypt and !decrypt.""" @@ -66,7 +67,13 @@ class Crypt(Command): self.reply(data, msg.format(data.command)) return - cipher = Blowfish.new(hashlib.sha256(key).digest()) + try: + cipher = Blowfish.new(hashlib.sha256(key).digest()) + except ImportError: + msg = "This command requires the 'pycrypto' package: https://www.dlitz.net/software/pycrypto/" + self.reply(data, msg) + return + try: if data.command == "encrypt": if len(text) % 8: @@ -75,5 +82,5 @@ class Crypt(Command): self.reply(data, cipher.encrypt(text).encode("hex")) else: self.reply(data, cipher.decrypt(text.decode("hex"))) - except ValueError as error: + except (ValueError, TypeError) as error: self.reply(data, error.message) diff --git a/earwigbot/commands/ctcp.py b/earwigbot/commands/ctcp.py index 0ed6412..bc1130c 100644 --- a/earwigbot/commands/ctcp.py +++ b/earwigbot/commands/ctcp.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/earwigbot/commands/dictionary.py b/earwigbot/commands/dictionary.py index 407f5f3..9515979 100644 --- a/earwigbot/commands/dictionary.py +++ b/earwigbot/commands/dictionary.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,7 +28,7 @@ from earwigbot.commands import Command class Dictionary(Command): """Define words and stuff.""" name = "dictionary" - commands = ["dict", "dictionary", "define"] + commands = ["dict", "dictionary", "define", "def"] def process(self, data): if not data.args: @@ -65,6 +65,16 @@ class Dictionary(Command): if not languages: return u"Couldn't parse {0}!".format(page.url) + if "#" in term: # Requesting a specific language + lcase_langs = {lang.lower(): lang for lang in languages} + request = term.rsplit("#", 1)[1] + lang = lcase_langs.get(request.lower()) + if not lang: + resp = u"Language {0} not found in definition." + return resp.format(request) + definition = self.get_definition(languages[lang], level) + return u"({0}) {1}".format(lang, definition) + result = [] for lang, section in sorted(languages.items()): definition = self.get_definition(section, level) diff --git a/earwigbot/commands/editcount.py b/earwigbot/commands/editcount.py index 2adea14..2b33f05 100644 --- a/earwigbot/commands/editcount.py +++ b/earwigbot/commands/editcount.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -47,7 +47,7 @@ class Editcount(Command): return safe = quote_plus(user.name.encode("utf8")) - url = "http://toolserver.org/~tparis/pcount/index.php?name={0}&lang={1}&wiki={2}" + url = "http://tools.wmflabs.org/xtools-ec/index.php?user={0}&lang={1}&wiki={2}" fullurl = url.format(safe, site.lang, site.project) msg = "\x0302{0}\x0F has {1} edits ({2})." self.reply(data, msg.format(name, count, fullurl)) diff --git a/earwigbot/commands/help.py b/earwigbot/commands/help.py index 7c9bd7f..870cf6c 100644 --- a/earwigbot/commands/help.py +++ b/earwigbot/commands/help.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,17 +20,20 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from platform import python_version import re +from earwigbot import __version__ from earwigbot.commands import Command class Help(Command): - """Displays help information.""" + """Displays information about the bot.""" name = "help" + commands = ["help", "version"] def check(self, data): if data.is_command: - if data.command == "help": + if data.command in self.commands: return True if not data.command and data.trigger == data.my_nick: return True @@ -39,6 +42,8 @@ class Help(Command): def process(self, data): if not data.command: self.do_hello(data) + elif data.command == "version": + self.do_version(data) elif data.args: self.do_command_help(data) else: @@ -69,3 +74,7 @@ class Help(Command): def do_hello(self, data): self.say(data.chan, "Yes, {0}?".format(data.nick)) + + def do_version(self, data): + vers = "EarwigBot v{bot} on Python {python}: https://github.com/earwig/earwigbot" + self.reply(data, vers.format(bot=__version__, python=python_version())) diff --git a/earwigbot/commands/lag.py b/earwigbot/commands/lag.py index cee0ee1..5e902f1 100644 --- a/earwigbot/commands/lag.py +++ b/earwigbot/commands/lag.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -24,7 +24,7 @@ from earwigbot import exceptions from earwigbot.commands import Command class Lag(Command): - """Return the replag for a specific database on the Toolserver.""" + """Return replag or maxlag information on specific databases.""" name = "lag" commands = ["lag", "replag", "maxlag"] @@ -45,7 +45,7 @@ class Lag(Command): self.reply(data, msg) def get_replag(self, site): - return "Toolserver replag is {0}".format(self.time(site.get_replag())) + return "replag is {0}".format(self.time(site.get_replag())) def get_maxlag(self, site): return "database maxlag is {0}".format(self.time(site.get_maxlag())) diff --git a/earwigbot/commands/langcode.py b/earwigbot/commands/langcode.py index 860c32e..b1712b6 100644 --- a/earwigbot/commands/langcode.py +++ b/earwigbot/commands/langcode.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -23,14 +23,14 @@ from earwigbot.commands import Command class Langcode(Command): - """Convert a language code into its name and a list of WMF sites in that - language, or a name into its code.""" + """Convert a language code into its name (or vice versa), and give a list + of WMF sites in that language.""" name = "langcode" commands = ["langcode", "lang", "language"] def process(self, data): if not data.args: - self.reply(data, "Please specify a language code.") + self.reply(data, "Please specify a language code or name.") return code, lcase = data.args[0], data.args[0].lower() diff --git a/earwigbot/commands/link.py b/earwigbot/commands/link.py index 858ff35..a54ea51 100644 --- a/earwigbot/commands/link.py +++ b/earwigbot/commands/link.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/earwigbot/commands/notes.py b/earwigbot/commands/notes.py index a430ae7..b7f01f9 100644 --- a/earwigbot/commands/notes.py +++ b/earwigbot/commands/notes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -50,7 +50,8 @@ class Notes(Command): } if not data.args: - msg = "\x0302The Earwig Mini-Wiki\x0F: running v{0}. Subcommands are: {1}. You can get help on any with '!{2} help subcommand'." + msg = ("\x0302The Earwig Mini-Wiki\x0F: running v{0}. Subcommands " + "are: {1}. You can get help on any with '!{2} help subcommand'.") cmnds = ", ".join((commands)) self.reply(data, msg.format(self.version, cmnds, data.command)) return @@ -101,7 +102,7 @@ class Notes(Command): entries = [] if entries: - entries = [entry[0] for entry in entries] + entries = [entry[0].encode("utf8") for entry in entries] self.reply(data, "Entries: {0}".format(", ".join(entries))) else: self.reply(data, "No entries in the database.") @@ -123,8 +124,10 @@ class Notes(Command): except (sqlite.OperationalError, TypeError): title, content = slug, None + title = title.encode("utf8") if content: - self.reply(data, "\x0302{0}\x0F: {1}".format(title, content)) + msg = "\x0302{0}\x0F: {1}" + self.reply(data, msg.format(title, content.encode("utf8"))) else: self.reply(data, "Entry \x0302{0}\x0F not found.".format(title)) @@ -142,7 +145,7 @@ class Notes(Command): except IndexError: self.reply(data, "Please specify an entry to edit.") return - content = " ".join(data.args[2:]).strip() + content = " ".join(data.args[2:]).strip().decode("utf8") if not content: self.reply(data, "Please give some content to put in the entry.") return @@ -153,11 +156,11 @@ class Notes(Command): id_, title, author = conn.execute(query1, (slug,)).fetchone() create = False except sqlite.OperationalError: - id_, title, author = 1, data.args[1], data.host + id_, title, author = 1, data.args[1].decode("utf8"), data.host self.create_db(conn) except TypeError: id_ = self.get_next_entry(conn) - title, author = data.args[1], data.host + title, author = data.args[1].decode("utf8"), data.host permdb = self.config.irc["permissions"] if author != data.host and not permdb.is_admin(data): msg = "You must be an author or a bot admin to edit this entry." @@ -172,7 +175,8 @@ class Notes(Command): else: conn.execute(query4, (revid, id_)) - self.reply(data, "Entry \x0302{0}\x0F updated.".format(title)) + msg = "Entry \x0302{0}\x0F updated." + self.reply(data, msg.format(title.encode("utf8"))) def do_info(self, data): """Get info on an entry in the notes database.""" @@ -197,7 +201,7 @@ class Notes(Command): times = [datum[1] for datum in info] earliest = min(times) msg = "\x0302{0}\x0F: {1} edits since {2}" - msg = msg.format(title, len(info), earliest) + msg = msg.format(title.encode("utf8"), len(info), earliest) if len(times) > 1: latest = max(times) msg += "; last edit on {0}".format(latest) @@ -242,7 +246,8 @@ class Notes(Command): msg = "You must be an author or a bot admin to rename this entry." self.reply(data, msg) return - conn.execute(query2, (self.slugify(newtitle), newtitle, id_)) + args = (self.slugify(newtitle), newtitle.decode("utf8"), id_) + conn.execute(query2, args) msg = "Entry \x0302{0}\x0F renamed to \x0302{1}\x0F." self.reply(data, msg.format(data.args[1], newtitle)) @@ -280,7 +285,7 @@ class Notes(Command): def slugify(self, name): """Convert *name* into an identifier for storing in the database.""" - return name.lower().replace("_", "").replace("-", "") + return name.lower().replace("_", "").replace("-", "").decode("utf8") def create_db(self, conn): """Initialize the notes database with its necessary tables.""" diff --git a/earwigbot/commands/quit.py b/earwigbot/commands/quit.py index 0331d08..2c5d47e 100644 --- a/earwigbot/commands/quit.py +++ b/earwigbot/commands/quit.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/earwigbot/commands/registration.py b/earwigbot/commands/registration.py index 74d2ce0..6ba8fc0 100644 --- a/earwigbot/commands/registration.py +++ b/earwigbot/commands/registration.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,7 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import time +from datetime import datetime +from time import mktime from earwigbot import exceptions from earwigbot.commands import Command @@ -46,8 +47,9 @@ class Registration(Command): self.reply(data, msg.format(name)) return - date = time.strftime("%b %d, %Y at %H:%M:%S UTC", reg) - age = self.get_diff(time.mktime(reg), time.mktime(time.gmtime())) + dt = datetime.fromtimestamp(mktime(reg)) + date = dt.strftime("%b %d, %Y at %H:%M:%S UTC") + age = self.get_age(dt) if user.gender == "male": gender = "He's" @@ -59,14 +61,24 @@ class Registration(Command): msg = "\x0302{0}\x0F registered on {1}. {2} {3} old." self.reply(data, msg.format(name, date, gender, age)) - def get_diff(self, t1, t2): - parts = [("year", 31536000), ("day", 86400), ("hour", 3600), - ("minute", 60), ("second", 1)] + def get_age(self, birth): msg = [] - for name, size in parts: - num = int(t2 - t1) / size - t1 += num * size - if num: - chunk = "{0} {1}".format(num, name if num == 1 else name + "s") - msg.append(chunk) + def insert(unit, num): + if not num: + return + msg.append("{0} {1}".format(num, unit if num == 1 else unit + "s")) + + now = datetime.utcnow() + bd_passed = now.timetuple()[1:-3] < birth.timetuple()[1:-3] + years = now.year - birth.year - bd_passed + delta = now - birth.replace(year=birth.year + years) + insert("year", years) + insert("day", delta.days) + + seconds = delta.seconds + units = [("hour", 3600), ("minute", 60), ("second", 1)] + for unit, size in units: + num = seconds / size + seconds -= num * size + insert(unit, num) return ", ".join(msg) if msg else "0 seconds" diff --git a/earwigbot/commands/remind.py b/earwigbot/commands/remind.py index e8470cb..f61e3f4 100644 --- a/earwigbot/commands/remind.py +++ b/earwigbot/commands/remind.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2012 Ben Kurtovic +# Copyright (C) 2009-2015 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,43 +20,411 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from threading import Timer +import ast +from contextlib import contextmanager +from itertools import chain +import operator +import random +from threading import RLock, Thread import time from earwigbot.commands import Command +from earwigbot.irc import Data + +DISPLAY = ["display", "show", "list", "info", "details"] +CANCEL = ["cancel", "stop", "delete", "del", "stop", "unremind", "forget", + "disregard"] +SNOOZE = ["snooze", "delay", "reset", "adjust", "modify", "change"] class Remind(Command): """Set a message to be repeated to you in a certain amount of time.""" name = "remind" - commands = ["remind", "reminder"] + commands = ["remind", "reminder", "reminders", "snooze", "cancel", + "unremind", "forget"] - def process(self, data): - if not data.args: - msg = "Please specify a time (in seconds) and a message in the following format: !remind